<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Soldatov Serhii</title>
    <description>The latest articles on DEV Community by Soldatov Serhii (@soldatov-ss).</description>
    <link>https://dev.to/soldatov-ss</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2993823%2F602ceed8-1e1b-41e4-862e-342623a562a7.jpg</url>
      <title>DEV Community: Soldatov Serhii</title>
      <link>https://dev.to/soldatov-ss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/soldatov-ss"/>
    <language>en</language>
    <item>
      <title>From Django Library to AWS Cognito: My Journey Building Enterprise SSO</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 12 Apr 2026 08:33:36 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/from-django-library-to-aws-cognito-my-journey-building-enterprise-sso-30be</link>
      <guid>https://dev.to/soldatov-ss/from-django-library-to-aws-cognito-my-journey-building-enterprise-sso-30be</guid>
      <description>&lt;h2&gt;
  
  
  The Client Call That Started Everything
&lt;/h2&gt;

&lt;p&gt;"We need SSO. Multiple providers. Per client. Starting next sprint."&lt;/p&gt;

&lt;p&gt;That was the moment I realized I had no idea what I was actually getting into.&lt;/p&gt;

&lt;p&gt;I'd built auth flows before — JWT, session-based, OAuth2 callbacks. But enterprise SSO at scale, where each tenant brings their own identity provider, where role sync needs to be automatic, where a user getting removed from an Azure group means they lose access in your app within minutes? That's a different animal entirely.&lt;/p&gt;

&lt;p&gt;This is the story of how I went from frantically Googling "Django SSO library" to building a production system on AWS Cognito that handles Azure AD, Okta, and any OIDC provider — without touching a line of server code when a new provider is added.&lt;/p&gt;




&lt;h2&gt;
  
  
  V1: Wrestling a Library into Submission
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Starting Point
&lt;/h3&gt;

&lt;p&gt;My first search led me to &lt;code&gt;django-simple-sso&lt;/code&gt; — the only SSO-focused library I could find for Django at the time. The bones were reasonable. The problem? It was designed for one provider, one configuration, one-to-one everything.&lt;/p&gt;

&lt;p&gt;My requirements were the opposite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple providers per client (a client might have Azure AD &lt;em&gt;and&lt;/em&gt; Okta)&lt;/li&gt;
&lt;li&gt;Dynamic provider selection by email domain&lt;/li&gt;
&lt;li&gt;Automatic role mapping: &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;manager&lt;/code&gt;, etc., synced from the provider&lt;/li&gt;
&lt;li&gt;Per-tenant configuration, entirely isolated&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Rearchitecture
&lt;/h3&gt;

&lt;p&gt;So I stopped trying to configure the library and started reading its internals. Then I started rewriting them.&lt;/p&gt;

&lt;p&gt;The data model went from one-to-one relationships to many-to-many. The auth flow got rewritten for dynamic provider resolution — when a user types their email, the backend queries which SSO configs are associated with that domain, selects the right provider, and redirects accordingly. Role mapping got its own layer.&lt;/p&gt;

&lt;p&gt;Three weeks of heads-down work. By the end, it covered everything: multiple providers, automatic role sync, dynamic authentication. It was custom, it was complex, and it worked.&lt;/p&gt;

&lt;p&gt;Until the next client call.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem That Forced V2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Request
&lt;/h3&gt;

&lt;p&gt;"We need to add a new provider."&lt;/p&gt;

&lt;p&gt;Fine. I'd done it before. Except this one was an obscure, legacy enterprise identity system — the kind that's only used in certain American B2B markets, where getting test credentials means purchasing a suite of products and weeks of vendor coordination. We literally could not test against it without significant time and cost investment.&lt;/p&gt;

&lt;p&gt;And that's when the real problem became obvious: every new provider required code changes. Test access. Adjustment cycles. The library abstraction I'd built was still fundamentally tied to providers I could understand and validate myself.&lt;/p&gt;

&lt;p&gt;The client's roadmap had more providers coming. We needed something universal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Decision
&lt;/h3&gt;

&lt;p&gt;We removed everything. The library, the custom SSO code, all of it.&lt;/p&gt;

&lt;p&gt;And we moved to &lt;strong&gt;AWS Cognito&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  V2: Why Cognito Changed Everything
&lt;/h2&gt;

&lt;p&gt;AWS Cognito sits between your application and any external identity provider. It speaks OIDC to the providers (Azure AD, Okta, Google, SAML, whatever) and speaks a single, consistent OAuth2 flow back to your app.&lt;/p&gt;

&lt;p&gt;The transformation this unlocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before (V1):
App &amp;lt;--&amp;gt; Custom SSO code &amp;lt;--&amp;gt; [Azure-specific logic] &amp;lt;--&amp;gt; Azure AD
                          &amp;lt;--&amp;gt; [Okta-specific logic]  &amp;lt;--&amp;gt; Okta
                          &amp;lt;--&amp;gt; [???-specific logic]   &amp;lt;--&amp;gt; Unknown provider

After (V2):
App &amp;lt;--&amp;gt; Cognito &amp;lt;--&amp;gt; Azure AD
                &amp;lt;--&amp;gt; Okta
                &amp;lt;--&amp;gt; Any OIDC provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adding a new provider now requires zero server-side code changes.&lt;/strong&gt; Cognito normalises everything. The token your app receives is identical regardless of which provider the user authenticated against.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Built
&lt;/h3&gt;

&lt;p&gt;The Django application I'm open-sourcing handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic config discovery&lt;/strong&gt; — &lt;code&gt;GET /api/oidc/check_config_exists/&amp;lt;email&amp;gt;/&lt;/code&gt; returns the right Cognito client config for a given email domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth2 callback&lt;/strong&gt; — exchanges the authorization code, decodes the ID token, maps roles, creates/updates the user and their membership record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Cognito provisioning&lt;/strong&gt; — clients send their identity provider credentials via a web form; the backend calls AWS APIs to configure the Cognito User Pool, no manual AWS console work required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle events via webhooks&lt;/strong&gt; — Azure AD and Okta can notify the app when a user is removed from a group or deleted. The system soft-deletes their membership, revoking access automatically
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser          Django              Cognito          Azure AD / Okta
  |                 |                   |                   |
  |-- check email -&amp;gt;|                   |                   |
  |&amp;lt;- client_id  ---|                   |                   |
  |                 |                   |                   |
  |-------- redirect to Cognito -------&amp;gt;|                   |
  |                                     |--- OIDC flow ----&amp;gt;|
  |                                     |&amp;lt;-- user token ----|
  |&amp;lt;-------- redirect with ?code= ------|                   |
  |                 |                   |                   |
  |-- /callback  --&amp;gt;|                   |                   |
  |                 |-- exchange code -&amp;gt;|                   |
  |                 |&amp;lt;-- id_token ------|                   |
  |                 | decode + map roles                    |
  |                 | create/update User + Member           |
  |&amp;lt;- access_token--|                   |                   |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Article Series
&lt;/h2&gt;

&lt;p&gt;This repo comes with a full series of step-by-step guides. Think of them as the manual that I wish had existed when I started:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/soldatov-ss/how-to-configure-aws-cognito-for-sso-a-step-by-step-guide-3j44"&gt;1. How to Configure AWS Cognito for SSO: A Step-by-Step Guide&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
The foundation. User Pool setup, custom attributes (&lt;code&gt;custom:groups&lt;/code&gt;, &lt;code&gt;custom:roles&lt;/code&gt;), app client configuration, and the callback URL wiring. Start here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/soldatov-ss/azure-single-sign-on-sso-setup-a-step-by-step-guide-2fjd"&gt;2. Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Enterprise App registration in Azure AD, configuring OIDC claims, mapping Azure groups to Cognito attributes. Includes the Graph API setup for lifecycle webhooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/soldatov-ss/okta-single-sign-on-sso-setup-a-step-by-step-guide-3o4l"&gt;3. Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Okta application setup, OIDC configuration, group claims, and the Event Hook configuration for real-time deprovisioning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/soldatov-ss/how-to-set-up-sso-authentication-in-your-apps-admin-panel-36k3"&gt;4. How to Set Up SSO Authentication in Your App's Admin Panel&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
The Django side: the authentication backend, the callback handler, role mapping, and how the admin panel reflects SSO-managed memberships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/soldatov-ss/how-to-test-sso-authentication-with-cognito-and-oidc-providers-894"&gt;5. How to Test SSO Authentication with Cognito and OIDC Providers&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
End-to-end testing strategy — what to mock, what to hit for real, and the test patterns used in this repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repository
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/soldatov-ss/django-cognito-sso-demo" rel="noopener noreferrer"&gt;django-cognito-sso-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a reference implementation, not a drop-in package. The parts you'll want to adapt for your own system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/oidc_auth/services/sso.py&lt;/code&gt;&lt;/strong&gt; — core SSO orchestration: config lookup, token exchange, role mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/oidc_auth/backends.py&lt;/code&gt;&lt;/strong&gt; — Django authentication backend wired to Cognito&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/users/cognito.py&lt;/code&gt;&lt;/strong&gt; — boto3 wrapper for all Cognito API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/oidc_auth/services/notification.py&lt;/code&gt;&lt;/strong&gt; — webhook processing for Azure and Okta lifecycle events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One intentional simplification worth noting: &lt;code&gt;RightsHolderService.get_rights_holder_from_request&lt;/code&gt; resolves the tenant from the request user directly. In production, tenant resolution typically comes from middleware or a partnership context. Replace it with whatever your multi-tenancy layer provides.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell Past Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Don't start with a library.&lt;/strong&gt; Or rather: start with one to understand the problem space, but be honest about when you've outgrown it. The three weeks I spent rearchitecting &lt;code&gt;django-simple-sso&lt;/code&gt; taught me exactly what I needed to know to make Cognito work well — I don't regret the time, but I wish I'd recognised sooner that the constraint wasn't the library, it was the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "universal provider" requirement is the forcing function.&lt;/strong&gt; If you know upfront that you need to support arbitrary OIDC providers without code changes, skip straight to an identity broker. Cognito is the obvious choice in AWS environments. There are others (Auth0, Keycloak). The architecture is the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle events are not optional for enterprise.&lt;/strong&gt; User deprovisioning is a security requirement, not a nice-to-have. Budget for the webhook infrastructure and the soft-deletion logic early. It's much harder to retrofit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypt your client secrets.&lt;/strong&gt; &lt;code&gt;SSOAccountConfig&lt;/code&gt; uses &lt;code&gt;django-encrypted-model-fields&lt;/code&gt; for &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;. If you're storing provider credentials in your database — and you will be — this is non-negotiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Goes Next
&lt;/h2&gt;

&lt;p&gt;The system in this repo handles everything the original client needed. The architecture has stayed stable through adding new providers, new tenants, and changes in the underlying identity providers themselves.&lt;/p&gt;

&lt;p&gt;If you're building something similar and hit a wall, the article series has the detail. The repo has the code. And if neither answers your question, drop it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source code: &lt;a href="https://github.com/soldatov-ss/django-cognito-sso-demo" rel="noopener noreferrer"&gt;github.com/your-username/django-cognito-sso-demo&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>aws</category>
      <category>sso</category>
      <category>python</category>
    </item>
    <item>
      <title>How to Set Up SSO Authentication in Your App's Admin Panel</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 12 Apr 2026 08:01:36 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/how-to-set-up-sso-authentication-in-your-apps-admin-panel-36k3</link>
      <guid>https://dev.to/soldatov-ss/how-to-set-up-sso-authentication-in-your-apps-admin-panel-36k3</guid>
      <description>&lt;h2&gt;
  
  
  Prerequisites for all OIDC Providers
&lt;/h2&gt;

&lt;p&gt;This guide covers the admin setup for SSO authentication built with Django and AWS Cognito. The full reference implementation is available on GitHub: &lt;a href="https://github.com/soldatov-ss/django-cognito-sso-demo" rel="noopener noreferrer"&gt;django-cognito-sso-demo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How SSO Works in This Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The user authenticates via an external OIDC provider (e.g., Azure or Okta)
&lt;/li&gt;
&lt;li&gt;AWS Cognito federates the identity
&lt;/li&gt;
&lt;li&gt;Your application validates:

&lt;ul&gt;
&lt;li&gt;group membership
&lt;/li&gt;
&lt;li&gt;email domain
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Access is granted or denied
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Before configuring SSO, ensure the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add an OIDC Provider to your Cognito User Pool&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires: Client ID, Client Secret, Issuer ID
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configure the &lt;code&gt;groups&lt;/code&gt; claim in your OIDC provider&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This is required for group validation to function properly  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add a custom &lt;code&gt;groups&lt;/code&gt; attribute in Cognito&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The attribute name must match the &lt;code&gt;groups&lt;/code&gt; claim used by your OIDC providers  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Provider Setup Guides
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/soldatov-ss/azure-single-sign-on-sso-setup-a-step-by-step-guide-2fjd"&gt;Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;br&gt;
 &lt;br&gt;
&lt;a href="https://dev.to/soldatov-ss/okta-single-sign-on-sso-setup-a-step-by-step-guide-3o4l"&gt;Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Other OIDC providers typically follow a similar setup process.&lt;/p&gt;


&lt;h2&gt;
  
  
  Creating SSO Account Configuration
&lt;/h2&gt;

&lt;p&gt;After entering the admin panel, find SSO Account Configs under the OIDC Auth application and click Add.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2powvvs639n3y7nqq2n2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2powvvs639n3y7nqq2n2.png" alt=" " width="702" height="708"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up SSO Account Configuration
&lt;/h2&gt;

&lt;p&gt;To configure an SSO Client provider, you need the Client ID, Client Secret from you Cognito App Client, you can copy this in your App Client page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3xjchwvy0r6qyg3nzib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3xjchwvy0r6qyg3nzib.png" alt=" " width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Choose your Account.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Only accounts with the required permissions can be selected in this field by SSO requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enter your application Cognito Client ID, Cognito Client Secret.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enter Provider Names from your Cognito App client, one of the values in this list can be used as &lt;code&gt;identity_provider&lt;/code&gt; in &lt;code&gt;/oauth2/authorize&lt;/code&gt; Cognito URI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enable Is active to activate this provider.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Allowed Domains — users whose email domain matches one of these domains will be allowed to access your application via this SSO configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enter the OIDC Provider Groups that should have access your application via this SSO configuration.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Only users who belong to these groups will be able to sign in.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Warning:&lt;/strong&gt; The group name must exactly match the format in your OIDC provider’s ID token.&lt;br&gt;&lt;br&gt;
Make sure to check the correct group format (e.g., Okta = group name, Azure = group object ID).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, click Save.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is an example of a completed form for my Account and Cognito application client.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1piqv1cr73yzoan0ve3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1piqv1cr73yzoan0ve3.png" alt=" " width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  SSO Notification Configuration
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Azure
&lt;/h3&gt;

&lt;p&gt;To configure SSO Provider Notification Config for Azure, you need the Client ID, Client Secret, and Tenant ID  from your Azure application (copied during the final step of the Azure configuration documentation).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose Azure as a Provider Name.&lt;/li&gt;
&lt;li&gt;Choose your Azure SSO Provider.&lt;/li&gt;
&lt;li&gt;Enter your Azure application Client ID, Client Secret and Tenant ID.&lt;/li&gt;
&lt;li&gt;Enter Notification url to your application Notification URL Endpoint (in my example it is &lt;a href="https://dev.yourapp.com/api/oidc/notifications/azure" rel="noopener noreferrer"&gt;https://dev.yourapp.com/api/oidc/notifications/azure&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Finally, click Save.&lt;/li&gt;
&lt;/ol&gt;


&lt;h3&gt;
  
  
  Okta
&lt;/h3&gt;

&lt;p&gt;No additional configuration is required if Event Hooks are set up correctly.&lt;br&gt;&lt;br&gt;
Soft-delete functionality will work automatically.&lt;/p&gt;

&lt;p&gt;✅ You have now successfully created SSO Providers in your application and can test it!&lt;/p&gt;


&lt;h1&gt;
  
  
  Admin API (Automating Setup)
&lt;/h1&gt;
&lt;h2&gt;
  
  
  POST /api/oidc/cognito/setup/
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Purpose:
&lt;/h3&gt;

&lt;p&gt;These endpoints automate the setup of SSO in Cognito and your app, reducing manual configuration and ensuring consistent integration between external OIDC providers and Cognito App Clients.&lt;/p&gt;

&lt;p&gt;Creates one or more External Identity Providers inside the Cognito User Pool and then creates a Cognito App Client that is linked to all providers specified in the providers field.&lt;/p&gt;

&lt;p&gt;!Important: This endpoint requires admin or privileged user access.&lt;/p&gt;
&lt;h3&gt;
  
  
  Request Fields:
&lt;/h3&gt;
&lt;h4&gt;
  
  
  provider (required)
&lt;/h4&gt;

&lt;p&gt;An external OIDC provider to create in the Cognito User Pool.&lt;/p&gt;

&lt;p&gt;Each provider entry must include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client_id - Identity Provider Client ID&lt;/li&gt;
&lt;li&gt;client_secret - Identity Provider Client Secret&lt;/li&gt;
&lt;li&gt;oidc_issuer - Identity Provider Issuer URL&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  group
&lt;/h4&gt;

&lt;p&gt;A group name used for group validation during the SSO login flow if the user does not exist in your app.&lt;/p&gt;

&lt;p&gt;Make sure to check the correct group format in your provider first (for example, for Okta it’s the group name, while for Azure it’s the group object ID). Otherwise, group validation won’t be working.&lt;/p&gt;
&lt;h4&gt;
  
  
  allowed_domain
&lt;/h4&gt;

&lt;p&gt;Users with this email domain will be allowed to access your application via this SSO configuration.&lt;/p&gt;

&lt;p&gt;Example request for Okta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0oavpnkcas2Otd5s697"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Provider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YRlFfB8wXkEkIasdWlmxC_UQenDNWTbCLmhasdGGBoDKsF00njihZ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Provider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Secret&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"oidc_issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://integrator-2000932.okta.com/oauth2/default"&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Provider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Issuer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(Okta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Format)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ada2a854-1asdd-475b-8e00-c7841dc99147"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Okta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;group&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowed_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-company.com"&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Allowed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SSO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Provider&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example Request for Azure AD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b4asdaf-315c-4236-964b-d0d6ebbe6725"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Azure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XOQ8Q~iUoWahILy6mpzIvsoSsGZT7cii"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Azure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Secret&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"oidc_issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://login.microsoftonline.com/d277adb7-1729-42x2-98a0-asda0b7/v2.0"&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Azure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Issuer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asds2-1f3d-475b-8e00-c7841dc99147"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Azure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;AD&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowed_domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-company.com"&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Allowed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SSO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Provider&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible Errors:&lt;/p&gt;

&lt;p&gt;Provider does not exists in User Pool 400:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed to create Cognito client or SSO provider."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{'message': ErrorDetail(string='The provider TestProvider does not exist for User Pool eu-north-1_....', code='cognito_error')}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provider with same name already exists in User Pool 400:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed to create OIDC provider."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{'message': ErrorDetail(string='OktaTestProvider already exists for tenant eu-north-1_....', code='cognito_error')}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Successful Response 201:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OIDC provider created successfully."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Triggering Azure notification events
&lt;/h2&gt;

&lt;p&gt;To trigger a soft-delete of a user in your application, you need to either remove the user from an Azure group or fully delete the user from Azure IAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete user from group:
&lt;/h3&gt;

&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Azure AD group (the group with the objectId defined in your SSO Client Provider).&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to Group
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to:
Your Azure Active Directory instance → Groups → All groups&lt;/li&gt;
&lt;li&gt;Click on the Group to which you want to delete users.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkwh8dv668a5ppgfmnw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkwh8dv668a5ppgfmnw6.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Navigate to Members tab
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click the “Members” button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq939oauw9mlqu6zlmmv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq939oauw9mlqu6zlmmv0.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Remove members from the Groups
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Select the users you want to remove from the group.&lt;/li&gt;
&lt;li&gt;After you select all users you want to delete from the group. Click Remove.&lt;/li&gt;
&lt;li&gt;A modal window will appear with the configuration form. Click Yes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fld39ouubqjs8t4tulilv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fld39ouubqjs8t4tulilv.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcur3x1s9nwsiyh3yziz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcur3x1s9nwsiyh3yziz.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, if a member with this ObjectID exists in your app, the system will soft-delete the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete user from IAM:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to Users Page
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to:
Your Azure Active Directory instance → Users.&lt;/li&gt;
&lt;li&gt;Select the users you want to delete.&lt;/li&gt;
&lt;li&gt;Click the “Delete” button, then confirm by clicking “Ok” in the modal window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffkz71mz0t7abeq2uz0f2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffkz71mz0t7abeq2uz0f2.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cynsz08nph8616fl7di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cynsz08nph8616fl7di.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Navigate to Deleted users tab
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click the “Deleted users” button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsn4jk88p8aecke7qobah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsn4jk88p8aecke7qobah.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Permanently delete the User
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Select the user you delete in Step 1 or you want to delete.&lt;/li&gt;
&lt;li&gt;Click “Delete permanently”, then confirm by clicking “Ok” in the modal window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frp6b49e2cp6p5e2dmgbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frp6b49e2cp6p5e2dmgbu.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqu1u6qphvhfbdtzje8pw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqu1u6qphvhfbdtzje8pw.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, the selected users will be completely deleted from Azure IAM.&lt;br&gt;&lt;br&gt;
If a user with the same User ObjectID exists in your app, that user will also be soft-deleted from your app.&lt;/p&gt;

&lt;p&gt;Note: The User ObjectID is linked to a user in your app only after the user logs into your app via the Azure SSO Provider.&lt;/p&gt;




&lt;h2&gt;
  
  
  Okta: Triggering Notification Events
&lt;/h2&gt;

&lt;p&gt;To trigger a soft-delete of a user in your application, you need to either remove the user from an Okta group or fully delete the user from Okta IAM.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Delete user from group:
&lt;/h3&gt;

&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Okta group (the group with the Group name defined in your SSO Client Provider).&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the Groups page and click on the group which you want to remove users:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxa3egbkdwdks2mvfiz9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxa3egbkdwdks2mvfiz9.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next to the user you want to remove, click on the cross (x) icon and the user will be removed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7qegbakzt3le8uh2v5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7qegbakzt3le8uh2v5z.png" alt=" " width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the user is removed from the group, they will be soft-deleted in your application.&lt;/p&gt;

&lt;p&gt;Note: Users will be removed only when deleted from groups that are present in your app SSO Providers Group name, if not this event hook will be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete user from IAM:
&lt;/h3&gt;

&lt;p&gt;In the side menu, navigate to the People page and select the the user which you want to delete:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0now37fbzgof8tyutdne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0now37fbzgof8tyutdne.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be redirected to the User Profile page. Click on the More Actions button, then click Deactivate to deactivate the user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgxzug87xyetbil4dq5x2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgxzug87xyetbil4dq5x2.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with. Click Deactivate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjll4160hkcm6uxjmcqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjll4160hkcm6uxjmcqn.png" alt=" " width="687" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then after the user deactivated, the Delete button will appear. Click Delete to completely delete the user from IAM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b2xsd90v409kavz8n91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b2xsd90v409kavz8n91.png" alt=" " width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with. Click Delete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmom0awviruig72lg2f4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmom0awviruig72lg2f4.png" alt=" " width="768" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, the selected user will be completely deleted from Okta IAM.&lt;/p&gt;

&lt;p&gt;If a user with the same email exists in your application, that user will also be soft-deleted in your application.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>sso</category>
      <category>authentication</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Test SSO Authentication with Cognito and OIDC Providers</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:51:41 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/how-to-test-sso-authentication-with-cognito-and-oidc-providers-894</link>
      <guid>https://dev.to/soldatov-ss/how-to-test-sso-authentication-with-cognito-and-oidc-providers-894</guid>
      <description>&lt;h2&gt;
  
  
  Prerequisites for all OIDC Providers
&lt;/h2&gt;

&lt;p&gt;This guide covers the admin setup for SSO authentication built with Django and AWS Cognito. The full reference implementation is available on GitHub: &lt;a href="https://github.com/soldatov-ss/django-cognito-sso-demo" rel="noopener noreferrer"&gt;django-cognito-sso-demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To ensure SSO works correctly in your application, you must complete the following steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add OIDC Provider to the Cognito user pool to External Providers, for this you need &lt;strong&gt;Client ID, Client Secret&lt;/strong&gt; and &lt;strong&gt;Issuer ID&lt;/strong&gt; of OIDC Provider.&lt;/li&gt;
&lt;li&gt;Make sure that &lt;strong&gt;SSO Provider&lt;/strong&gt; with this App Client credentials exists on your application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure the “groups” claim&lt;/strong&gt; in the ID Token on your OIDC provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is required for group validation to function properly in SSO.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a custom &lt;strong&gt;group&lt;/strong&gt; claim to Cognito by creating a custom attribute named &lt;strong&gt;groups&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The name must match the “groups” claim used in all OIDC providers that will be included in your application SSO.&lt;/p&gt;

&lt;p&gt;After setting up the Admin Panel, we are ready to test SSO authentication.&lt;/p&gt;




&lt;h2&gt;
  
  
  Provider Setup Guides
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/soldatov-ss/azure-single-sign-on-sso-setup-a-step-by-step-guide-2fjd"&gt;Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;br&gt;
 &lt;br&gt;
&lt;a href="https://dev.to/soldatov-ss/okta-single-sign-on-sso-setup-a-step-by-step-guide-3o4l"&gt;Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Other OIDC providers typically follow a similar setup process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Log in via Cognito
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; To log in to your application via the Azure SSO Provider, send a GET request to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;cognito_domain&amp;gt;/login?client_id=&amp;lt;client_id&amp;gt;&amp;amp;redirect_uri=&amp;lt;redirect_uri&amp;gt;&amp;amp;response_type=code&amp;amp;scope=openid&amp;amp;state=&amp;lt;client_id&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;cognito_domain -&lt;/strong&gt; the AWS Cognito Domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;region&lt;/strong&gt; - the AWS region where your Cognito User Pool is hosted (e.g., eu-central-1);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;client_id&lt;/strong&gt; - the Cognito App Client ID for the specific tenant (client);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redirect_uri&lt;/strong&gt; - the callback URL configured in Cognito for your application (e.g., &lt;a href="https://yourapp.com/api/oidc/cognito/callback/" rel="noopener noreferrer"&gt;https://yourapp.com/api/oidc/cognito/callback/&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;state&lt;/strong&gt; - a parameter used to maintain state between the request and callback (here you must reuse the &lt;strong&gt;client_id&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Click on your Continue with your OIDC Provider name, that you enter in Cognito.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmamqjf5tyjpz6pja2ljy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmamqjf5tyjpz6pja2ljy.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; After sending the request, you will be redirected to the Your OIDC Provider login page, in my case it’s Microsoft login page. Click &lt;strong&gt;Receive Code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbo32a7yks5vdh5y3nsq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbo32a7yks5vdh5y3nsq4.png" alt=" " width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;:  Enter the code sent to &lt;strong&gt;user_email&lt;/strong&gt;, then click &lt;strong&gt;Yes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foim0x10w7yrmwbdddsc4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foim0x10w7yrmwbdddsc4.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; After successful authentication, you will receive a response with your authentication tokens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2pmctxgmsziwmp4tufbh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2pmctxgmsziwmp4tufbh.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; To check if a user was actually created after SSO login, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check via the Admin Panel:&lt;/strong&gt; Go to &lt;strong&gt;Admin Panel → Users&lt;/strong&gt; and search for the user by email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check via API:&lt;/strong&gt; Send a GET request to: &lt;code&gt;/api/users/me/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Testing Log out
&lt;/h2&gt;

&lt;p&gt;To log out, send a POST or GET request to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/api/oidc/cognito/logout/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After successful logout you will receive a response with your message “Logout successful”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcu1k1bhvavupzt85d1a2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcu1k1bhvavupzt85d1a2.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Group Validation
&lt;/h2&gt;

&lt;p&gt;If the user is &lt;strong&gt;not a member&lt;/strong&gt; of the IAM group, the &lt;strong&gt;group name&lt;/strong&gt; is incorrect in the Admin Panel for the SSO Provider, or the user’s &lt;strong&gt;email domain&lt;/strong&gt; is not linked with a rights holder of the &lt;strong&gt;app_id&lt;/strong&gt;, the login attempt will fail, showing an error, where &lt;strong&gt;provider 19&lt;/strong&gt; is &lt;strong&gt;SSO Client Provider ID&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ba0wfzrdf662570zhbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ba0wfzrdf662570zhbf.png" alt=" " width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;You have now successfully SSO authentication!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>sso</category>
      <category>authentication</category>
      <category>django</category>
    </item>
    <item>
      <title>How to Configure AWS Cognito for SSO: A Step-by-Step Guide</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:51:07 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/how-to-configure-aws-cognito-for-sso-a-step-by-step-guide-3j44</link>
      <guid>https://dev.to/soldatov-ss/how-to-configure-aws-cognito-for-sso-a-step-by-step-guide-3j44</guid>
      <description>&lt;h2&gt;
  
  
  Creating App Client for Each Customer
&lt;/h2&gt;

&lt;p&gt;According to the multi-tenant application model, we need to create a separate Cognito App Client for each customer and attach their corresponding external OIDC providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Creating Application Client
&lt;/h3&gt;

&lt;p&gt;To create App Client navigate to &lt;strong&gt;Application - App client,&lt;/strong&gt; click to &lt;strong&gt;Create App Client&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9012wley1uh3c337xpa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9012wley1uh3c337xpa.png" alt=" " width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the setup form, choose &lt;strong&gt;Traditional web application&lt;/strong&gt; as the application type.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydo5lhwmnvyubmeuqe17.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydo5lhwmnvyubmeuqe17.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Adding custom attribute
&lt;/h3&gt;

&lt;p&gt;To include user groups in the ID token, we need to add a custom attribute to the Cognito User Pool.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Authentication&lt;/strong&gt; &lt;strong&gt;-&lt;/strong&gt; &lt;strong&gt;Sign Up&lt;/strong&gt;, then click &lt;strong&gt;Add custom attribute:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6nmxhzb7wqbk0demya9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6nmxhzb7wqbk0demya9m.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the form, enter the attribute name as shown below, and then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ajnesz3waq325l566sp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ajnesz3waq325l566sp.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding roles custom attribute
&lt;/h4&gt;

&lt;p&gt;If “&lt;strong&gt;roles&lt;/strong&gt;” scope is configured in Customer OIDC provider, you can add a custom “&lt;strong&gt;roles&lt;/strong&gt;” attribute as well and include this attribute in External Cognito Provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipmbdxmrv6quij34zwfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipmbdxmrv6quij34zwfk.png" alt=" " width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3:  Creating External Provider
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Authentication - Social and external providers&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdkccpqymslur3zzrybhu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdkccpqymslur3zzrybhu.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In from enter your OIDC Provider credentials, &lt;strong&gt;Client ID&lt;/strong&gt;, &lt;strong&gt;Client Secret&lt;/strong&gt; and &lt;strong&gt;Issuer URI&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add custom “&lt;strong&gt;groups”&lt;/strong&gt; attributes that you created in the previous step and include &lt;strong&gt;email&lt;/strong&gt; attributes. (username will be added automatically after you created the provider).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4oklgide0yz1pxydr74.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4oklgide0yz1pxydr74.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmq2lbowvub3jbk46w2sh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmq2lbowvub3jbk46w2sh.png" alt=" " width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;!Important:&lt;/strong&gt; In the form, enter the OpenID Connect attribute to &lt;strong&gt;groups&lt;/strong&gt; exactly as shown below, and then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clarification:&lt;/strong&gt; We configure these attributes so that the group and role information (optional) is included in the &lt;strong&gt;ID Token&lt;/strong&gt; returned by the Identity Provider. If your application uses different scope names, make sure to update the values in the Cognito accordingly (for example, if Provider sends roles under &lt;strong&gt;app_groups&lt;/strong&gt; scope, you should set OpenID Connect attribute to &lt;strong&gt;app_groups&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Info:&lt;/strong&gt; If you added a “&lt;strong&gt;custom:roles&lt;/strong&gt;” attribute in the previous step, you can include the “&lt;strong&gt;roles&lt;/strong&gt;” attribute here as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjljd5zifmnw0n9v336fa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjljd5zifmnw0n9v336fa.png" alt=" " width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Attach External Provider to App client
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;App Clients - Your App - Login Pages&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Edit&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5414koprwmuw65m5bs2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5414koprwmuw65m5bs2.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the configuration form:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add allowed callback URLs (e.g.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://yourapp.com/api/oidc/cognito/callback/" rel="noopener noreferrer"&gt;https://yourapp.com/api/oidc/cognito/callback/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;or &lt;strong&gt;&lt;a href="http://localhost:8000/api/oidc/cognito/callback/" rel="noopener noreferrer"&gt;http://localhost:8000/api/oidc/cognito/callback/&lt;/a&gt;&lt;/strong&gt; for local testing).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach the identity providers you created in the previous steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;!Important&lt;/strong&gt;: Under &lt;strong&gt;OpenID Connect scopes&lt;/strong&gt;, select only the &lt;strong&gt;OpenId&lt;/strong&gt; scope.&lt;/li&gt;
&lt;li&gt;Click Save changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5b50c0ny5d0jssvqt43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5b50c0ny5d0jssvqt43.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After &lt;strong&gt;custom attributes&lt;/strong&gt; setup check in &lt;strong&gt;Attribute Permission&lt;/strong&gt; that &lt;strong&gt;Read&lt;/strong&gt; and &lt;strong&gt;Write&lt;/strong&gt; permissions are checked:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04k94g86vvipk6pstgsc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04k94g86vvipk6pstgsc.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Attach Login Page to the App client
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Your User Pool - Managed login.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create a style&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr6ujtc7hzr54ukqvodj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr6ujtc7hzr54ukqvodj.png" alt=" " width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the &lt;strong&gt;App client&lt;/strong&gt; you want to attach a &lt;strong&gt;login page&lt;/strong&gt; to.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevr5sfpowasgtiqyhbii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevr5sfpowasgtiqyhbii.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creation, the login page for your App Client will become available and can be accessed directly via its generated URL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw2wbt0l104ysmfnowkah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw2wbt0l104ysmfnowkah.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  How to connect to your application
&lt;/h1&gt;

&lt;p&gt;To enable &lt;strong&gt;Cognito login&lt;/strong&gt; for your users in &lt;strong&gt;your application&lt;/strong&gt;, you need to retrieve the following three parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Pool Cognito domain&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can find this data on our Cognito User Pool and App client pages.&lt;/p&gt;

&lt;p&gt;To copy &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt; Navigate to corresponding &lt;strong&gt;App client:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllzri1xt08hvv0suprez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllzri1xt08hvv0suprez.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get User Pool &lt;strong&gt;Cognito domain&lt;/strong&gt;, navigate to &lt;strong&gt;Domain&lt;/strong&gt; page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibydx2qptk6manr0e7gq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibydx2qptk6manr0e7gq.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Cognito Setup Complete.&lt;/strong&gt; &lt;strong&gt;Your Cognito configuration is now complete and ready for SSO testing.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>aws</category>
      <category>authentication</category>
      <category>security</category>
    </item>
    <item>
      <title>Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sat, 11 Apr 2026 15:36:12 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/okta-single-sign-on-sso-setup-a-step-by-step-guide-3o4l</link>
      <guid>https://dev.to/soldatov-ss/okta-single-sign-on-sso-setup-a-step-by-step-guide-3o4l</guid>
      <description>&lt;h2&gt;
  
  
  Configuring SSO authentication in Okta
&lt;/h2&gt;

&lt;p&gt;After logging into the Okta panel, you will be redirected to the dashboard. From here, we will configure Okta to enable Single Sign-On (SSO) authentication for our application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03nwa288isqd5961nphf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03nwa288isqd5961nphf.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Assigning users to group
&lt;/h1&gt;

&lt;p&gt;We need to add groups and add the required users to the appropriate group.&lt;/p&gt;

&lt;p&gt;!Note: Only users who are &lt;strong&gt;members&lt;/strong&gt; of this &lt;strong&gt;group&lt;/strong&gt; will be able to sign in to your application.&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Groups&lt;/strong&gt; page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi190q0iqe2m8njde1tf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi190q0iqe2m8njde1tf4.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;then click to the &lt;strong&gt;Add group&lt;/strong&gt; button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub7qhg3cviiri5dk6w8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub7qhg3cviiri5dk6w8d.png" alt=" " width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with the configuration form.&lt;/p&gt;

&lt;p&gt;Fill this form with the group name and description, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwmxbh7vmixgj8hfroaq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwmxbh7vmixgj8hfroaq.png" alt=" " width="773" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;People&lt;/strong&gt; page, then choose a particular person in the list and click to navigate to the &lt;strong&gt;Person&lt;/strong&gt; page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcics1x91nudpopohyvbe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcics1x91nudpopohyvbe.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Groups&lt;/strong&gt; subtitle to navigate to the profile page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feojb3koq6iyu2d4nz5fp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feojb3koq6iyu2d4nz5fp.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Search&lt;/strong&gt; field and enter the group name that you want to assign the user with, then click to the appearing group.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6kgwnkut4eu68kkuz0j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6kgwnkut4eu68kkuz0j.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating an application
&lt;/h3&gt;

&lt;p&gt;An application refers to a cloud or on-premises service integrated with Okta for single sign-on, enabling users to securely access and authenticate across multiple applications with a single set of credentials.&lt;/p&gt;

&lt;p&gt;Next, navigate to the &lt;strong&gt;Applications&lt;/strong&gt; page (as shown in the screenshot)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy74c2x9uags86zb3ywo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy74c2x9uags86zb3ywo2.png" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Create App Integration&lt;/strong&gt; button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhblvst3egccsok1ev5x3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhblvst3egccsok1ev5x3.png" alt=" " width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration options. Select the options as shown in the screenshot, then click the &lt;strong&gt;Next&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27btu7i8krqjedqap7jr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27btu7i8krqjedqap7jr.png" alt=" " width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be redirected to the configuration page:&lt;/p&gt;

&lt;p&gt;Follow the instructions below and configure the settings as marked in the next two screenshots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I will set my app name to "SSO OIDC", but you can use just "your application".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-in redirect URIs:&lt;/strong&gt; The format should be: 
&lt;code&gt;https://&amp;lt;your-user-pool-domain&amp;gt;/oauth2/idpresponse&lt;/code&gt;
where &lt;code&gt;&amp;lt;your-user-pool-domain&amp;gt;&lt;/code&gt; is your Cognito User Pool Domain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(example: &lt;strong&gt;&lt;a href="https://eu-north-2uasd2wr7mv.auth.eu-north-1.amazoncognito.com/oauth2/idpresponse" rel="noopener noreferrer"&gt;https://eu-north-2uasd2wr7mv.auth.eu-north-1.amazoncognito.com/oauth2/idpresponse&lt;/a&gt;&lt;/strong&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign-out redirect URIs:&lt;/strong&gt; This ensures users are properly signed out when logging out of our application. 
Input: &lt;code&gt;https://yourapp.com/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right holder access:&lt;/strong&gt; choose the &lt;strong&gt;Limit access to selected groups&lt;/strong&gt; option and enter group names you want to give access to &lt;strong&gt;your application&lt;/strong&gt; application.&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkpscr5avsggp255tjf9r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkpscr5avsggp255tjf9r.png" alt=" " width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfgos2a0hbxdqvjqkbo9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfgos2a0hbxdqvjqkbo9.png" alt=" " width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will then be redirected to the newly created application's page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5foi9bmx3ogp4tcjhswd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5foi9bmx3ogp4tcjhswd.png" alt=" " width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring a User Okta profile (Add Roles to Profile)
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Okta&lt;/strong&gt;, we can define roles for users to manage permissions within our application.&lt;/p&gt;

&lt;p&gt;These roles determine the level of access a user has, such as &lt;strong&gt;admin&lt;/strong&gt;, &lt;strong&gt;manager&lt;/strong&gt;, etc.&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Profile Editor&lt;/strong&gt; page&lt;/p&gt;

&lt;p&gt;Click to &lt;strong&gt;User (default)&lt;/strong&gt; link to open User Okta Profile edit page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnccspfoy8a6fcqmwxj5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnccspfoy8a6fcqmwxj5i.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Attribute&lt;/strong&gt; button to add a new attribute for roles in the Okta User profile:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8m09cu88sxie9s7ls29a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8m09cu88sxie9s7ls29a.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration form.&lt;/p&gt;

&lt;p&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv5maxgnn7hp0thq0b3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv5maxgnn7hp0thq0b3j.png" alt=" " width="677" height="1032"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring an OIDC client profile for the app
&lt;/h3&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Profile Editor&lt;/strong&gt; page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3j3r40ux3t0ljcth68g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3j3r40ux3t0ljcth68g.png" alt=" " width="275" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;SSO OIDC User&lt;/strong&gt; (&lt;strong&gt;name of our application&lt;/strong&gt;) link to navigate to the OIDC Client Profile edit page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqbkup50aoqsyynf6dz1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqbkup50aoqsyynf6dz1v.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Attribute&lt;/strong&gt; button to add a new attribute for roles in the OIDC Client Profile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupex1ez09su4ed5vuon6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupex1ez09su4ed5vuon6.png" alt=" " width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration form.&lt;/p&gt;

&lt;p&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xi1k8a1irisyjhgng5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xi1k8a1irisyjhgng5m.png" alt=" " width="676" height="909"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Mappings&lt;/strong&gt; button to configure the mapping between the Okta User Profile and the OIDC Client Profile:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jr47zx930mtjyrlpftv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jr47zx930mtjyrlpftv.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration fields for each relation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76x8vjusbi8kd34iwh7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76x8vjusbi8kd34iwh7c.png" alt=" " width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Slide to the last field, type in the field &lt;code&gt;user.userRoles&lt;/code&gt;. Click on the relation button which mark on the following screenshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fut4x8icog0qpi2eyyacb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fut4x8icog0qpi2eyyacb.png" alt=" " width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose an &lt;strong&gt;Apply mapping on user create and update&lt;/strong&gt; option:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjezs9uupc28vl5ri8kc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjezs9uupc28vl5ri8kc8.png" alt=" " width="374" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, this should look like as shown below. Click on the &lt;strong&gt;Save Mappings&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xaerfkuihwa4ah99fe8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xaerfkuihwa4ah99fe8.png" alt=" " width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring an API Authorization
&lt;/h2&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;API&lt;/strong&gt; page&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;default&lt;/strong&gt; link (I use &lt;strong&gt;default_2&lt;/strong&gt;, because &lt;strong&gt;default&lt;/strong&gt; in my account already configured), which is marked in the following screenshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa768x0vacd3t7kom8r25.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa768x0vacd3t7kom8r25.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring a group scope
&lt;/h4&gt;

&lt;p&gt;Now, let's configure scopes to define the permissions our application can request.&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Scopes&lt;/strong&gt; subtitle to navigate to the scopes configuration page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqtozj66qm9w28k96qgf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqtozj66qm9w28k96qgf.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Scope&lt;/strong&gt; button to add a new scope, which will be used for groups&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Few90cd1bpix9cr5jb3ad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Few90cd1bpix9cr5jb3ad.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;!Important&lt;/strong&gt;: &lt;strong&gt;Scope name&lt;/strong&gt; must be &lt;strong&gt;"groups"&lt;/strong&gt;, values in the &lt;strong&gt;"Display phrase"&lt;/strong&gt; and &lt;strong&gt;"Description"&lt;/strong&gt; fields are not that important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffcng39r1jepg0jqsnpol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffcng39r1jepg0jqsnpol.png" alt=" " width="666" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring role scope
&lt;/h4&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Scope&lt;/strong&gt; button to add a new scope, which will be used for roles&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m6slnfoh9xj2xfuhoid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m6slnfoh9xj2xfuhoid.png" alt=" " width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0m5mp3wwjh86miszc0p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0m5mp3wwjh86miszc0p.png" alt=" " width="730" height="758"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring a new groups claim
&lt;/h4&gt;

&lt;p&gt;Configuring a claim after the scope for Okta SSO ensures that the appropriate user attributes are securely included in the authentication token, enabling seamless access control and personalized experiences across integrated applications.&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Claims&lt;/strong&gt; subtitle to navigate to the claims configuration page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxq7dtrpa9z8upbtu25wq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxq7dtrpa9z8upbtu25wq.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Claim&lt;/strong&gt; button to add a new claim, which will be used for groups:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7oll0ozgyycymr1djya8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7oll0ozgyycymr1djya8.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;!Important&lt;/strong&gt;: &lt;strong&gt;Claim name&lt;/strong&gt; must be &lt;strong&gt;"groups"&lt;/strong&gt; and &lt;strong&gt;all&lt;/strong&gt; other values must be identical as on screenshot!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i8f26zuz8ucxn5ibeyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i8f26zuz8ucxn5ibeyk.png" alt=" " width="656" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring a new roles claim
&lt;/h4&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Claim&lt;/strong&gt; button to add a new claim, which will be used for &lt;strong&gt;roles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ohmpb3crvmli3zkskxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ohmpb3crvmli3zkskxx.png" alt=" " width="751" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring a new access policy
&lt;/h4&gt;

&lt;p&gt;Access policies and rules in Okta SSO allow administrators to define and enforce conditions for user authentication, ensuring secure and granular control over who can access applications based on factors like location, device, and user group.&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Access Policies&lt;/strong&gt; subtitle to navigate to the access policies configuration page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F202prd15ckhv0ok9xsbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F202prd15ckhv0ok9xsbb.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add Policy&lt;/strong&gt; button to add a new policy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5iuv1zq3fr98ldjgcet5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5iuv1zq3fr98ldjgcet5.png" alt=" " width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with a configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create Policy&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18c0n2ivuh2gmej8wr2h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18c0n2ivuh2gmej8wr2h.png" alt=" " width="724" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Add rule&lt;/strong&gt; button to add a new rule&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh69zwbvf0bp2whid2i5l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh69zwbvf0bp2whid2i5l.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with the configuration form. Fill in this form as shown in the screenshot.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;"User is"&lt;/strong&gt; section, select the group you want to allow access to your application.&lt;br&gt;
 In my case, it is the users from &lt;strong&gt;"test_group"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can also add individual users if needed.&lt;/p&gt;

&lt;p&gt;Then, click the &lt;strong&gt;Create rule&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;Only users who are members of the selected group (and assigned the app) will be able to sign in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe10nowgfhx8qccq6qgcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe10nowgfhx8qccq6qgcc.png" alt=" " width="665" height="1031"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring a new Event Hooks
&lt;/h4&gt;

&lt;p&gt;Event Hooks in &lt;strong&gt;Okta SSO&lt;/strong&gt; allow your application to track IAM related events.&lt;br&gt;
We will create an event hook that listens for two specific events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User removed from group&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User deleted&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events will trigger a &lt;strong&gt;soft delete&lt;/strong&gt; of the corresponding user in your application.&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Event hooks&lt;/strong&gt; page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0rae3flg5bhhmjt7xjl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0rae3flg5bhhmjt7xjl4.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Create Event Hook&lt;/strong&gt; button to add an event hook&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm727w39vbwggkuwmvyb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm727w39vbwggkuwmvyb.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with a configuration form.&lt;/p&gt;

&lt;p&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Save &amp;amp; Continue&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1g5jmayaebhxerun18y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1g5jmayaebhxerun18y.png" alt=" " width="800" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another more modal window will appear with a configuration form.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Verify&lt;/strong&gt; to confirm that the webhook is active and reachable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmf8sus6f7hich8p8esb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmf8sus6f7hich8p8esb4.png" alt=" " width="687" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will then be redirected to the newly created event hook page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19afbn6qxm63ffnh9wr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19afbn6qxm63ffnh9wr6.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Assigning user roles
&lt;/h1&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;People&lt;/strong&gt; page, then choose a particular person in the list and click to navigate to the &lt;strong&gt;Person&lt;/strong&gt; page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tuieftzv0luekz3v48v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tuieftzv0luekz3v48v.png" alt=" " width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Profile&lt;/strong&gt; subtitle to navigate to the profile page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0684jivimqb66iqxdq97.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0684jivimqb66iqxdq97.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Edit&lt;/strong&gt; button to enable editing attributes of the profile. Slide down to the bottom of the page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgkhlrj2yj0n8nr6rzc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgkhlrj2yj0n8nr6rzc1.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, you can choose any number of roles from the list for the particular user. Click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh51lscvwgz8yk2xguhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh51lscvwgz8yk2xguhi.png" alt=" " width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Deleting users
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Delete user from group:
&lt;/h3&gt;

&lt;p&gt;To trigger a &lt;strong&gt;soft-delete&lt;/strong&gt; of a user in your application, you need to either remove the user from an Okta group or fully delete the user from Okta IAM.&lt;/p&gt;

&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Okta group (the group with the &lt;strong&gt;Group name&lt;/strong&gt; defined in your SSO Client Provider).&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Groups&lt;/strong&gt; page and click on the group which you want to remove users:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxtex9mrsqnmmv8fsxnzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxtex9mrsqnmmv8fsxnzf.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next to the user you want to remove, click on the cross (x) icon and the user will be removed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn61kv4xcyzlltiquxf0c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn61kv4xcyzlltiquxf0c.png" alt=" " width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the user is removed from the group, they will be &lt;strong&gt;soft-deleted&lt;/strong&gt; in your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Users will be removed only when deleted from groups that are present in your app SSO Providers Group name, if not this event hook will be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete user from IAM:
&lt;/h3&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;People&lt;/strong&gt; page and select the the user which you want to delete:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10vglq37gi1myapjg9jc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10vglq37gi1myapjg9jc.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be redirected to the User Profile page. Click on the &lt;strong&gt;More Actions&lt;/strong&gt; button, then click &lt;strong&gt;Deactivate&lt;/strong&gt; to deactivate the user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6b6gfszwgwbjm6kq5s3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6b6gfszwgwbjm6kq5s3.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with. Click &lt;strong&gt;Deactivate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycyv5oags3cnqwiod10x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycyv5oags3cnqwiod10x.png" alt=" " width="687" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then after the user deactivated, &lt;strong&gt;the Delete&lt;/strong&gt; button will appear. Click &lt;strong&gt;Delete&lt;/strong&gt; to completely delete the user from IAM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdin4pos29uo01u7kif70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdin4pos29uo01u7kif70.png" alt=" " width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear with. Click &lt;strong&gt;Delete.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdso8cqg3it9vz9wdeq4g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdso8cqg3it9vz9wdeq4g.png" alt=" " width="768" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, the selected user will be completely deleted from &lt;strong&gt;Okta IAM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a user with the &lt;strong&gt;same email&lt;/strong&gt; exists in your application, that user will also be &lt;strong&gt;soft-deleted&lt;/strong&gt; in your application.&lt;/p&gt;

&lt;h1&gt;
  
  
  (Optional) Multi Factor Authentication
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Configuring Global Session Policy
&lt;/h3&gt;

&lt;p&gt;Multi-Factor Authentication (MFA) adds an additional layer of security to user login. When enabled, users must verify their identity using a second factor, such as the &lt;strong&gt;Okta Verify app&lt;/strong&gt;, in addition to their password.&lt;/p&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Global Session Policy&lt;/strong&gt; page. Сlick &lt;strong&gt;Add policy&lt;/strong&gt; button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8kc59jrgqwizdkf0vjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8kc59jrgqwizdkf0vjh.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear. Enter &lt;strong&gt;Policy name, Policy Description&lt;/strong&gt; and assign it to the group you want to enforce MFA for. In my case this will be a group of &lt;strong&gt;Everyone&lt;/strong&gt;. Click &lt;strong&gt;Create policy and add rule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewtu4aihp4cydpj83dk7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewtu4aihp4cydpj83dk7.png" alt=" " width="580" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another modal window will appear with adding policy rule. Enter &lt;strong&gt;Rule name,&lt;/strong&gt; Set &lt;strong&gt;MFA&lt;/strong&gt; to &lt;strong&gt;Required,&lt;/strong&gt; In &lt;strong&gt;User will be prompted for MFA,&lt;/strong&gt; select the desired option. Then click &lt;strong&gt;Create rule.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsoflplrzppjhdzeadc05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsoflplrzppjhdzeadc05.png" alt=" " width="800" height="999"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F587qqyw2pedkaz9vfnip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F587qqyw2pedkaz9vfnip.png" alt=" " width="800" height="1002"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring authentication policies
&lt;/h3&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Authentication Policies&lt;/strong&gt; page and click on &lt;strong&gt;App sign-in&lt;/strong&gt; button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8rtfir9eo99wq06bxjud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8rtfir9eo99wq06bxjud.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be redirected to the &lt;strong&gt;App sign-in policies&lt;/strong&gt; page. Click &lt;strong&gt;Create policy&lt;/strong&gt; to add a new policy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs6locqugyf86oczmef97.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs6locqugyf86oczmef97.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the modal window, provide a &lt;strong&gt;Policy name&lt;/strong&gt; and &lt;strong&gt;Description&lt;/strong&gt;, then click &lt;strong&gt;Create Policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpad9nysv3j1kupg2g15l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpad9nysv3j1kupg2g15l.png" alt=" " width="524" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be redirected to your policy page. Click &lt;strong&gt;Add rule.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5d1ns444uz1bkjtwaq9g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5d1ns444uz1bkjtwaq9g.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modal window will appear. &lt;br&gt;
Enter &lt;strong&gt;rule name&lt;/strong&gt;, select the &lt;strong&gt;Groups&lt;/strong&gt; you want to enforce MFA for.Configure other fields as shown in the screenshots below. Then click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j1ddjb584tcjnuz6g02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j1ddjb584tcjnuz6g02.png" alt=" " width="800" height="959"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtynkou6aziqfhriyy3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtynkou6aziqfhriyy3y.png" alt=" " width="800" height="952"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl56qvanbh1q6g8ze4v4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl56qvanbh1q6g8ze4v4p.png" alt=" " width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your new authentication policy rule will be added.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Authenticators
&lt;/h3&gt;

&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Authenticators&lt;/strong&gt; page and click on &lt;strong&gt;Enrollment&lt;/strong&gt; tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4m9nojtwc5mfdzm1wzv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4m9nojtwc5mfdzm1wzv2.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click &lt;strong&gt;Actions&lt;/strong&gt; next to the default policy and select &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe14qcvtatsj7gjvtk4xo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe14qcvtatsj7gjvtk4xo.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the modal window that appears, ensure that &lt;strong&gt;Okta Verify&lt;/strong&gt; is set to &lt;strong&gt;Required&lt;/strong&gt;, then click Save.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15tiggkwi1ctoouwu0sm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15tiggkwi1ctoouwu0sm.png" alt=" " width="576" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ That's it!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Okta Multi-Factor Authentication (MFA) is now successfully configured.&lt;/p&gt;

&lt;h1&gt;
  
  
  How to connect to your application
&lt;/h1&gt;

&lt;p&gt;To enable &lt;strong&gt;Okta login&lt;/strong&gt; for your users in &lt;strong&gt;your application&lt;/strong&gt;, you need to retrieve the following three parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Okta domain&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can find this data on our Application details page. In the side menu (Okta panel), navigate to the &lt;strong&gt;Applications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose an application which you created previously. In my case, it is an SSO OIDC (marked below)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F84pe3xp32hlpctvtvqmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F84pe3xp32hlpctvtvqmj.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instructions for copying the whole information that we should fill in your application can see below&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uk41rvl4h0t4kdkc6d6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uk41rvl4h0t4kdkc6d6.png" alt=" " width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjznia8an4ejp5hlmc5f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjznia8an4ejp5hlmc5f.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Copy a issuer URL
&lt;/h3&gt;

&lt;p&gt;To obtain the Issuer URL for your application, open: &lt;strong&gt;https://{yourOktaDomain}/.well-known/openid-configuration&lt;/strong&gt; (In my case it is: &lt;a href="https://integrator-1609932.okta.com/.well-known/openid-configuration" rel="noopener noreferrer"&gt;https://integrator-1609932.okta.com/.well-known/openid-configuration&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Then copy the value of the &lt;strong&gt;issuer&lt;/strong&gt; field from the returned JSON:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcmj5zx7tv3ick6ocbie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcmj5zx7tv3ick6ocbie.png" alt=" " width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Now, you have all the required credentials to connect to your application!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>okta</category>
      <category>sso</category>
      <category>authentication</category>
      <category>security</category>
    </item>
    <item>
      <title>Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sat, 11 Apr 2026 15:36:01 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/azure-single-sign-on-sso-setup-a-step-by-step-guide-2fjd</link>
      <guid>https://dev.to/soldatov-ss/azure-single-sign-on-sso-setup-a-step-by-step-guide-2fjd</guid>
      <description>&lt;p&gt;To enable Single Sign-On (SSO) for your application, we first need to register it in &lt;strong&gt;Azure Active&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;Navigate to Microsoft Entra ID:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqg8pxoz62onwptmtvad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqg8pxoz62onwptmtvad.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click to &lt;strong&gt;Add&lt;/strong&gt; button and select &lt;strong&gt;App registration&lt;/strong&gt;: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjwe8fq14mcgw5yswaf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjwe8fq14mcgw5yswaf3.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill in the Name field (you can choose any meaningful name).&lt;/p&gt;

&lt;p&gt;Set the &lt;strong&gt;Redirect URI&lt;/strong&gt;, which look like this: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://your-user-pool-domain/oauth2/idpresponse&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;where &lt;em&gt;your-user-pool-domain&lt;/em&gt; is your Cognito User Pool Domain &lt;/p&gt;

&lt;p&gt;(example: &lt;code&gt;https://eu-north-asr1mv.auth.eu-north-1.amazoncognito.com/oauth2/idpresponse&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jy7id9qzoz0wwvfuv2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jy7id9qzoz0wwvfuv2g.png" alt=" " width="800" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Other fields of this form fill as shown in the following screenshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8ui7wcaz0s1r6z4ws6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8ui7wcaz0s1r6z4ws6h.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Logout URL &amp;amp; Enabling ID Tokens
&lt;/h3&gt;

&lt;p&gt;To complete the authentication setup, we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable ID Tokens&lt;/strong&gt; - Since we are using &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; to authenticate users, ID tokens must be enabled.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set the Logout URL&lt;/strong&gt; - This ensures users are properly signed out when logging out of our application.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Steps to Configure:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to &lt;strong&gt;Authentication&lt;/strong&gt; in the registered app:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance&lt;/strong&gt; -&amp;gt; &lt;strong&gt;App registrations&lt;/strong&gt; -&amp;gt; &lt;strong&gt;&amp;lt; YourApp &amp;gt;&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Authentication&lt;/strong&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set the &lt;strong&gt;Logout URL&lt;/strong&gt; to:&lt;br&gt;&lt;br&gt;
&lt;a href="https://your-app-domain/api/oidc/logout" rel="noopener noreferrer"&gt;https://your-app-domain/api/oidc/logout&lt;/a&gt;/&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxqlmplr80fn59sekatn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxqlmplr80fn59sekatn.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating scopes
&lt;/h2&gt;

&lt;p&gt;After creating the application, you will be redirected to the &lt;strong&gt;App Overview&lt;/strong&gt; page. Now, let's configure scopes to define the permissions our application can request.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to Expose an API
&lt;/h4&gt;

&lt;p&gt;Click on &lt;strong&gt;Expose an API&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxc46xtjmw3amv18r20ti.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxc46xtjmw3amv18r20ti.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Set Application ID URI
&lt;/h4&gt;

&lt;p&gt;If this is your first time accessing this section, you will see a prompt asking you to add an &lt;strong&gt;Application ID URI&lt;/strong&gt; before proceeding. This step is required to define unique identifiers for your API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow the prompt to add an &lt;strong&gt;Application ID URI (generates automatically)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1bmtt3w3dtiahueasf6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1bmtt3w3dtiahueasf6.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Add a Group Scope
&lt;/h4&gt;

&lt;p&gt;Now that the &lt;strong&gt;Application ID URI&lt;/strong&gt; is set, we can define the scopes our application needs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click Add a Scope.&lt;/li&gt;
&lt;li&gt;Scopes in &lt;strong&gt;OIDC&lt;/strong&gt; determine what permissions the application can request from the identity provider. They allow access to specific user attributes, such as groups, roles, email, or profile information.&lt;/li&gt;
&lt;li&gt;In our case, we need the &lt;strong&gt;group&lt;/strong&gt; scope to retrieve user groups during authentication. This enables proper access control and permission management in our application.&lt;/li&gt;
&lt;li&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Add scope&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3y0e4p8cm7rtcw6e25gc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3y0e4p8cm7rtcw6e25gc.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Step 3.1: Add a Roles Scope
&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add a Scope&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Scopes in &lt;strong&gt;OIDC&lt;/strong&gt; determine what permissions the application can request from the identity provider. They allow access to specific user attributes, such as roles, email, or profile information.&lt;/li&gt;
&lt;li&gt;In our case, we need the &lt;strong&gt;roles&lt;/strong&gt; scope to retrieve user roles (e.g., admin, manager) during authentication. This enables proper access control and permission management in our application.&lt;/li&gt;
&lt;li&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Add scope&lt;/strong&gt; button&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0v9it9g8zyp80kwo9e7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0v9it9g8zyp80kwo9e7.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: Add a Client Application
&lt;/h4&gt;

&lt;p&gt;Once the scope is created, we need to associate it with our client application.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add a client application&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Client ID&lt;/strong&gt; can be found in the &lt;strong&gt;Application ID URI&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With these steps completed, our application is now set up to request and utilize user groups during authentication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feypprxeoukgqx8d4usoy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feypprxeoukgqx8d4usoy.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 5: Add a Groups to Token Configuration
&lt;/h4&gt;

&lt;p&gt;After all is created we must create group claim&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add group claim&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;Fill the form as shown in the following screenshot, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2bhcxsqhy2oj64w6b4r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2bhcxsqhy2oj64w6b4r.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it, &lt;strong&gt;the group's&lt;/strong&gt; scope and claim is configured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extending API Permissions
&lt;/h2&gt;

&lt;p&gt;Retrieving the &lt;strong&gt;email&lt;/strong&gt;, &lt;strong&gt;openid&lt;/strong&gt;, and &lt;strong&gt;profile&lt;/strong&gt; claims in the ID token, as well as creating notification subscriptions for soft delete support, requires enabling the appropriate &lt;strong&gt;API permissions&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; App registrations -&amp;gt; &amp;lt; YourApp &amp;gt; -&amp;gt; API Permissions&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Add permission&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facvsrq998fk05e66z5of.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facvsrq998fk05e66z5of.png" alt=" " width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the sidebar that appears, select &lt;strong&gt;APIs my organization uses&lt;/strong&gt;, then choose &lt;strong&gt;Microsoft Graph&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq3kprexy9a83muyfaafm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq3kprexy9a83muyfaafm.png" alt=" " width="800" height="852"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;Delegated permissions&lt;/strong&gt;, then enable: &lt;strong&gt;email&lt;/strong&gt;, &lt;strong&gt;openid&lt;/strong&gt;, &lt;strong&gt;profile&lt;/strong&gt;, &lt;strong&gt;User.Read&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vvt3dwtqey5h72eemw3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vvt3dwtqey5h72eemw3.png" alt=" " width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwael1d9ne4ofunrktp5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwael1d9ne4ofunrktp5o.png" alt=" " width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;Application permissions&lt;/strong&gt;, then enable: &lt;strong&gt;Directory.Read.All&lt;/strong&gt; and &lt;strong&gt;User.ReadAll&lt;/strong&gt;. Then Click on the &lt;strong&gt;Add permissions&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5oggeccpmz2is650l0e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5oggeccpmz2is650l0e.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibvujoizrg30cm0625qv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibvujoizrg30cm0625qv.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  This is what the correct configuration should look like:
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoxse7k9iu3jb1lkxpov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoxse7k9iu3jb1lkxpov.png" alt=" " width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating roles
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Azure Active Directory (AAD)&lt;/strong&gt;, we can define roles for users to manage permissions within our application. These roles determine the level of access a user has, such as &lt;strong&gt;admin&lt;/strong&gt;, &lt;strong&gt;manager&lt;/strong&gt;, etc.&lt;br&gt;&lt;br&gt;
In the Azure Active Directory we should create roles for users.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Access the Manifest&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To create roles, we need to edit the &lt;strong&gt;application's Manifest&lt;/strong&gt;, which is a JSON file defining the application's configuration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; App registrations -&amp;gt; &amp;lt; YourApp &amp;gt; -&amp;gt; Manifest&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmfrpmzyj18ane6a8l5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmfrpmzyj18ane6a8l5g.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Manifest&lt;/strong&gt;, you will find a section for role definitions. If you haven't created any roles yet, this section will be empty (highlighted in red in the screenshot).&lt;/p&gt;

&lt;p&gt;Example of one role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowedMemberTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Administrators can manage resources."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1234567-89ab-cdef-0123-456789abcdea"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each role must have a &lt;strong&gt;unique ID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;GUID generator&lt;/strong&gt; to create unique IDs. You can use this free tool:
👉&lt;a href="https://www.guidgenerator.com/" rel="noopener noreferrer"&gt;GUID Generator&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 2: Add All Required Roles
&lt;/h4&gt;

&lt;p&gt;For convenience, you can copy and paste the pre-defined role objects into your &lt;strong&gt;Manifest&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the prepared role definitions.&lt;/li&gt;
&lt;li&gt;Replace the role IDs with newly generated GUIDs.&lt;/li&gt;
&lt;li&gt;Press the "Save" button to apply the changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once saved, your roles should appear correctly in the Manifest.  &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here's prepared user roles, you can just copy-paste the whole object, BUT do not forget to use your own ids by using GUID, after pasting press "Save" button:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"appRoles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowedMemberTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full admin access."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ADMIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18846fd3-94e1-4f47-a8e5-27edb66a14b8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowedMemberTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Booking and campaign management access."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MANAGER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b2345678-90ab-cdef-0123-456789abcdeb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manager"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowedMemberTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Booking and campaign management access."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPORT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cd4062ff-2eb9-4960-93c1-1d20637f1d23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"report"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how it should look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbv3212ly6sfumafc90c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbv3212ly6sfumafc90c.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating groups
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Azure Active Directory (AAD)&lt;/strong&gt;, we can define groups for users to manage permissions within our application. Groups allow us to control which users have access to the app. A user must be assigned to the appropriate group in order to sign in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;!Note&lt;/strong&gt;: If a user is not a member of the group, they will not be able to access your application.  &lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to groups
&lt;/h4&gt;

&lt;p&gt;To create groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Groups Manifest&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You should see a screen similar to the one below:&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"New group"&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi06rw0dapgl2d23oopvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi06rw0dapgl2d23oopvi.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Create Group
&lt;/h4&gt;

&lt;p&gt;To create groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose &lt;strong&gt;Security&lt;/strong&gt; type in Group type input&lt;/li&gt;
&lt;li&gt;Fill other fields&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Create"&lt;/strong&gt; button. And &lt;strong&gt;Security group&lt;/strong&gt; will be created&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ucmd3fheed2zfjd2e3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ucmd3fheed2zfjd2e3d.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Assign users to group
&lt;/h4&gt;

&lt;p&gt;To assign users to group, follow these steps:&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to Group
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Groups -&amp;gt; All groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Group&lt;/strong&gt; to which you want to add users.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqzp4rdnstje863vuiog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqzp4rdnstje863vuiog.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Navigate to Members tab
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;"Members"&lt;/strong&gt; button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh94xg6p2me9qa1q34b5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh94xg6p2me9qa1q34b5d.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Add members to the Groups
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add members&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the users you want to add to the group as on the screenshot below.&lt;/li&gt;
&lt;li&gt;After you select all users you want to add to the group. Click Select&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgblb4ic1uhd5wjl0s4j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgblb4ic1uhd5wjl0s4j.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Assigning user roles
&lt;/h2&gt;

&lt;p&gt;To assign roles to users, follow these steps:&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to User Role Assignment
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Enterprise applications -&amp;gt; &amp;lt; YourApp &amp;gt; -&amp;gt; Users and groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Add user"&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1x659yhm2fk3t8u80d0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1x659yhm2fk3t8u80d0r.png" alt=" " width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Select Users and Assign Roles
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;First, select the users you want to assign roles to.
(See Screenshot below)&lt;/li&gt;
&lt;li&gt;On the next screen, choose the appropriate role for the user.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Assign&lt;/strong&gt; button&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgmrge7k8yvx69xifpe97.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgmrge7k8yvx69xifpe97.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flid0xas4un41cvwztqz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flid0xas4un41cvwztqz6.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For production environments, it's &lt;strong&gt;not recommended&lt;/strong&gt; to assign roles to individual users. Instead, assign roles to &lt;strong&gt;groups&lt;/strong&gt; to simplify user management and reduce maintenance efforts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Confirm Role Assignment
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;"Assign"&lt;/strong&gt; button to finalize the role assignment.&lt;/li&gt;
&lt;li&gt;You will be redirected to the &lt;strong&gt;Users and groups&lt;/strong&gt; view, where you can see the assigned roles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7b7j0c46loke05pzwswl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7b7j0c46loke05pzwswl.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;br&gt;
That's it! The Role assignment is now complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now you have completed all preparation steps and your application is ready to use Azure SSO.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Deleting users
&lt;/h2&gt;

&lt;p&gt;To trigger a &lt;strong&gt;soft-delete&lt;/strong&gt; of a user in your application, you need to either remove the user from an Azure group or fully delete the user from Azure IAM.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete user from group:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Azure AD group (the group with the objectId defined in your SSO Client Provider).&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to Group
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Groups -&amp;gt; All groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Group&lt;/strong&gt; to which you want to delete users.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff49epyv8o0ptzxqrm6j0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff49epyv8o0ptzxqrm6j0.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Navigate to Members tab
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;"Members"&lt;/strong&gt; button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gp2iqc6h9p2r47rxbk4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gp2iqc6h9p2r47rxbk4.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Remove members from the Groups
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Select the users you want to remove from the group.&lt;/li&gt;
&lt;li&gt;After you select all users you want to delete from the group. Click &lt;strong&gt;Remove&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A modal window will appear with the configuration form. Click &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbiachbl4pixopkwd56c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbiachbl4pixopkwd56c.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Folw9khmb7c73lrqrbxdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Folw9khmb7c73lrqrbxdw.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, if a member with this ObjectID exists in your application, the system will &lt;strong&gt;soft-delete&lt;/strong&gt; the user.&lt;/p&gt;

&lt;p&gt;Delete user from IAM: &lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Navigate to Users Page
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Users&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the users you want to delete.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Delete"&lt;/strong&gt; button, then confirm by clicking &lt;strong&gt;"Ok"&lt;/strong&gt; in the modal window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xoerf9frqgymf941hxj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xoerf9frqgymf941hxj.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja6y6sp7i29teevmxoy9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja6y6sp7i29teevmxoy9.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Navigate to Deleted users tab
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;"Deleted users"&lt;/strong&gt; button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnb3v833g7i69ot2kvzl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnb3v833g7i69ot2kvzl4.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Permanently delete the User
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Select the user you delete in &lt;strong&gt;Step 1&lt;/strong&gt; or you want to delete.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Delete permanently"&lt;/strong&gt;, then confirm by clicking &lt;strong&gt;"Ok"&lt;/strong&gt; in the modal window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3go3aq8f6g4isftof53p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3go3aq8f6g4isftof53p.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcnimdmkcsjbcni0g0efp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcnimdmkcsjbcni0g0efp.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, the selected users will be completely deleted from Azure IAM.&lt;br&gt;&lt;br&gt;
If a user with the same &lt;strong&gt;User ObjectID&lt;/strong&gt; exists in your application, that user will also be &lt;strong&gt;soft-deleted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The User ObjectID is linked to a user in your application only after the user logs in via the Azure SSO Provider.&lt;/p&gt;

&lt;h1&gt;
  
  
  (Optional) Multi Factor Authentication
&lt;/h1&gt;

&lt;p&gt;Multi-Factor Authentication (MFA) adds an additional layer of security to user login. When enabled, users must verify their identity using a second factor, such as the &lt;strong&gt;Microsoft Authenticator app&lt;/strong&gt;, in addition to their password.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Properties&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Make sure that Security Defaults are enabled. if not - enable it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap285f4xairxmudig00b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap285f4xairxmudig00b.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After enabling &lt;strong&gt;Security Defaults&lt;/strong&gt;, when a user from your organization attempts to log in, they will be prompted for MFA.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user will need to use the &lt;strong&gt;Microsoft Authenticator app&lt;/strong&gt; (or another supported MFA method) to complete the login.&lt;/li&gt;
&lt;li&gt;A screen similar to the one below will appear, guiding the user through the MFA verification process:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz9kpez09ehhmac22ud9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz9kpez09ehhmac22ud9.png" alt=" " width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On next login attempts, the user will be required to complete &lt;strong&gt;MFA&lt;/strong&gt; using the Microsoft Authenticator app.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to connect your application
&lt;/h2&gt;

&lt;p&gt;To enable &lt;strong&gt;Azure login&lt;/strong&gt; for your users, you need to retrieve the following three parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client ID&lt;/li&gt;
&lt;li&gt;Client Secret&lt;/li&gt;
&lt;li&gt;Tenant ID&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Step 1: Retrieve Client ID &amp;amp; Tenant ID
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to:
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; App registrations -&amp;gt; &amp;lt; YourApp &amp;gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Locate the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Tenant ID&lt;/strong&gt; in the &lt;strong&gt;App Overview section&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ve8f2u6wsuqg2agsrkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ve8f2u6wsuqg2agsrkg.png" alt=" " width="800" height="332"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Generate Client Secret
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;Client credentials&lt;/strong&gt; (as shown in Screenshot above).&lt;/li&gt;
&lt;li&gt;Create a new &lt;strong&gt;Client Secret&lt;/strong&gt;.
(See Screenshot below)&lt;/li&gt;
&lt;li&gt;After creating the secret, &lt;strong&gt;copy the "Value" immediately&lt;/strong&gt;, as it will no longer be visible once you refresh or leave the page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qskn0fvi203ik55x9js.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qskn0fvi203ik55x9js.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creation you should copy Value&lt;br&gt;&lt;br&gt;
Note! after updating page it won't be visible anymore&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegw5baak7mennz351fqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegw5baak7mennz351fqs.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Copy a issuer URL
&lt;/h3&gt;

&lt;p&gt;Reference:&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri" rel="noopener noreferrer"&gt; &lt;/a&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To obtain the Issuer URL for your application, copy: &lt;a href="https://login.microsoftonline.com/%7Btenant%7D/v2.0" rel="noopener noreferrer"&gt;https://login.microsoftonline.com/{tenant}/v2.0&lt;/a&gt; and replace &lt;strong&gt;{tenant}&lt;/strong&gt; with your application's &lt;strong&gt;tenant ID&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzu44noxifg63x1wt436t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzu44noxifg63x1wt436t.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ Now you have all the required credentials to configure Azure SSO for your application!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>sso</category>
      <category>authentication</category>
      <category>oidc</category>
    </item>
    <item>
      <title>Django Without the Mess: Repositories for Data, Services for Rules</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 07 Sep 2025 11:58:40 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/django-without-the-mess-repositories-for-data-services-for-rules-k8e</link>
      <guid>https://dev.to/soldatov-ss/django-without-the-mess-repositories-for-data-services-for-rules-k8e</guid>
      <description>&lt;h2&gt;
  
  
  The Elephant in the Room: Django's Fat Model Problem
&lt;/h2&gt;

&lt;p&gt;Let’s talk about the elephant in every Django project: that one &lt;code&gt;models.py&lt;/code&gt; file. You know the one. It started pure and simple, a beautiful reflection of your database schema. Now, it's a 2,000-line behemoth, bloated with custom managers, &lt;code&gt;@property&lt;/code&gt; decorators that hide monstrous queries, and &lt;code&gt;save()&lt;/code&gt; methods that seem to have more side effects than a late-night infomercial product.&lt;/p&gt;

&lt;p&gt;Your views are a mess, your business logic is scattered, and making any change feels like performing surgery with a butter knife. In this moment of desperation, a seemingly elegant solution appears: "selectors." Just pull all those messy queries out into a separate &lt;code&gt;selectors.py&lt;/code&gt; file! It feels clean. It feels right.&lt;/p&gt;

&lt;p&gt;But I'm here to tell you that selectors are a trap. They’re a quick fix that feels like progress but ultimately leads you down a path of architectural pain. There’s a better way, a more structured approach that will save you from future headaches: the Repository and Service patterns. Let’s break down why this combination wins, and why selectors just don’t hold up in the long run.&lt;/p&gt;




&lt;h2&gt;
  
  
  How We All Got Here
&lt;/h2&gt;

&lt;p&gt;No one sets out to write messy code. We typically arrive here by following well-intentioned advice. In the Django world, the classic mantra is "fat models, thin views." The idea is to keep your views lean and push all the logic related to your data into the corresponding model.&lt;/p&gt;

&lt;p&gt;And for a small project, this works beautifully! A &lt;code&gt;User&lt;/code&gt; model with a method like &lt;code&gt;is_profile_complete()&lt;/code&gt; is perfectly reasonable. But what happens when the logic gets more complex? Soon, you have &lt;code&gt;user.get_recent_orders()&lt;/code&gt;, &lt;code&gt;user.calculate_lifetime_value()&lt;/code&gt;, and &lt;code&gt;user.send_password_reset_email()&lt;/code&gt;. Your model is no longer just a data structure; it's a tangled web of business rules, database queries, and external interactions.&lt;/p&gt;

&lt;p&gt;This is when developers, rightly, look for a way to clean up. The &lt;code&gt;selectors.py&lt;/code&gt; file is born. You create functions like &lt;code&gt;user_get_active_with_recent_orders()&lt;/code&gt; and &lt;code&gt;product_get_top_sellers()&lt;/code&gt;. The model slims down, the view calls a clean function—problem solved, right?&lt;/p&gt;

&lt;p&gt;Not quite. Honestly, this is like sweeping a pile of dirt under the rug. The room looks cleaner at a glance, but you haven't actually dealt with the mess. You’ve just moved it. This separation is superficial, and as we’ll see, it creates a whole new set of problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository Pattern 101: Your Data's Bodyguard
&lt;/h2&gt;

&lt;p&gt;So, what’s the alternative? Let’s start with the first piece of the puzzle: the &lt;strong&gt;Repository Pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the simplest terms, a repository is an abstraction layer that sits between your application's business logic and your database. Its one and only job is to handle data access. Think of it as a specialized agent for a particular model. If you need to get users, save a user, or delete a user, you talk to the &lt;code&gt;UserRepository&lt;/code&gt;. You design it to serve your use cases, not to mirror every possible query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;your_app/&lt;/span&gt;
  &lt;span class="s"&gt;users/&lt;/span&gt;
    &lt;span class="s"&gt;models.py&lt;/span&gt;
    &lt;span class="s"&gt;repositories/&lt;/span&gt;
      &lt;span class="s"&gt;__init__.py&lt;/span&gt;
      &lt;span class="s"&gt;users.py&lt;/span&gt;
    &lt;span class="s"&gt;services/&lt;/span&gt;
      &lt;span class="s"&gt;__init__.py&lt;/span&gt;
      &lt;span class="s"&gt;users.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s what that &lt;code&gt;UserRepository&lt;/code&gt; might look like, inheriting from a &lt;code&gt;BaseRepository&lt;/code&gt; (like the one &lt;a href="https://gist.github.com/soldatov-ss/ad8a158b5e65308c952553bbe451fda9" rel="noopener noreferrer"&gt;here&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# your_app/users/repositories/users.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;your_app.apps.common.repositories&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseRepository&lt;/span&gt; 
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;your_app.users.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clear contract.&lt;/strong&gt; It provides a single, explicit interface for data operations. No more &lt;code&gt;User.objects.filter(...)&lt;/code&gt; scattered across 20 different files. You centralize your query logic in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized query intent.&lt;/strong&gt; If a flow needs special prefetch or annotations, you add a method and keep it consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test seams.&lt;/strong&gt; You can pass a fake repository in tests. You can add caching or logging in one place without hunting through views.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The service layer, also known as the brain
&lt;/h2&gt;

&lt;p&gt;If repositories are your data's bodyguards, services are the brain of the operation. Services sit above repositories to orchestrate business workflows and enforce rules. They are where your transactions should begin and end. If a business rule changes—like "premium users get priority processing"—you want one, and only one, place to make that change.&lt;/p&gt;

&lt;p&gt;The best analogy is this: &lt;strong&gt;Repositories fetch the ingredients. Services follow the recipe to cook the meal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A service doesn't know &lt;em&gt;how&lt;/em&gt; the ingredients are fetched (that's the repository's job), and the view doesn't know &lt;em&gt;how&lt;/em&gt; the meal is cooked. The view just says, "I'd like to order the 'New User Registration' please."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# users/services/users.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.repositories&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt;
&lt;span class="c1"&gt;# Assume a NotificationService exists elsewhere
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;notifications.services&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NotificationService&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notification_service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_repo&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notification_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notification_service&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Business rule: check if user already exists
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User with this email already exists.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 1: Create the user via the repository
&lt;/span&gt;        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 2: Orchestrate another action
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notification_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean, transactional, and easy to follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Risk of Selectors: A Slippery Slope
&lt;/h2&gt;

&lt;p&gt;So if services are the brain, why not just use selectors for the data? The risk isn't that selectors are inherently bad, but that they are a &lt;strong&gt;slippery slope&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They lack a strong architectural boundary, making it too easy to add "just one little piece" of business logic. Over time, these small compromises turn your clean query file into a tangled, secondary business logic layer.&lt;/p&gt;

&lt;p&gt;The Repository and Service pattern prevents this by creating &lt;strong&gt;explicit guardrails&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repositories&lt;/strong&gt; have one job: get data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services&lt;/strong&gt; have one job: enforce business rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This clear separation makes the right path the easy path and prevents the slow decay of your architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Admin actions, celery tasks, and the rest of the crew
&lt;/h2&gt;

&lt;p&gt;Keep the same pattern everywhere. Admin actions call services. Celery tasks call services. Management commands call services. One flow. One place for idempotency and retries. If a task runs twice, the service checks state and either skips or continues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common questions you will hear on the team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can I keep a small helper on the model.&lt;/strong&gt; Yes, if it only touches that model’s state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I always need a service.&lt;/strong&gt; If a view is a straight read, repo to serializer can be fine. When you branch or change state, reach for a service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need a repo per model.&lt;/strong&gt; Start where it helps most. Big aggregates, heavy queries, hot paths. Let the layer grow with need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can selectors live beside repos.&lt;/strong&gt; If they exist, make them thin wrappers over repos, not a separate source of truth.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why this matters on teams, not only in code
&lt;/h2&gt;

&lt;p&gt;Clean layers make it easier to talk. A PM says, payment failed on retry. You look at one place. A junior asks, where should I put this logic. You have a ready answer. A reviewer reads a PR and knows exactly which patterns to expect. The shape of the system stays steady as the people change.&lt;/p&gt;

&lt;p&gt;Also, repositories act like discreet places to teach performance. You can add little comments, like why you used &lt;code&gt;select_for_update&lt;/code&gt; here, or why an index matters. New teammates learn by seeing the same pattern in many methods. That is softer than sending a long wiki doc that nobody reads twice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Choose Clarity Over Convenience
&lt;/h2&gt;

&lt;p&gt;Look, selectors aren't born from malice. They come from a good place: the desire to clean up messy code. They offer a moment of relief, a feeling of organization. But it’s a temporary fix. They are a crutch, not a long-term architectural strategy.&lt;/p&gt;

&lt;p&gt;The Repository and Service patterns require a bit more discipline upfront. You have to think about your application's layers and enforce those boundaries&lt;br&gt;
Keep models slim. Put data access in repositories. Put rules in services. Own transactions at the service layer. Be consistent.&lt;/p&gt;

</description>
      <category>django</category>
      <category>cleancode</category>
      <category>architecture</category>
      <category>python</category>
    </item>
    <item>
      <title>Part 2: Django REST Framework: When (and When Not) to Override Serializers and Viewsets</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 10 Aug 2025 14:55:02 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/part-2-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-3j32</link>
      <guid>https://dev.to/soldatov-ss/part-2-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-3j32</guid>
      <description>&lt;h2&gt;
  
  
  DRF, Part 2: ViewSet Overrides
&lt;/h2&gt;

&lt;p&gt;Welcome to the second half of our guide on DRF architecture. In &lt;a href="https://dev.to/soldatov-ss/part-1-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-11a7"&gt;Part 1&lt;/a&gt;, we established a clear rule: serializers are the guardians of your data. They handle validation, transformation, and representation. They ensure that the data entering your system is clean and the data leaving is well-formed.&lt;/p&gt;

&lt;p&gt;But that’s only half the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ViewSet's Core Mission: Orchestration
&lt;/h2&gt;

&lt;p&gt;Viewsets handle the HTTP journey. They don't care if a &lt;code&gt;start_date&lt;/code&gt; is before an &lt;code&gt;end_date&lt;/code&gt; — that's the serializer's job. They care about things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing the incoming &lt;code&gt;request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Checking permissions and authentication.&lt;/li&gt;
&lt;li&gt;Calling the serializer to validate data.&lt;/li&gt;
&lt;li&gt;Triggering the save operation.&lt;/li&gt;
&lt;li&gt;Formatting the final &lt;code&gt;Response&lt;/code&gt; with the right data and status code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's explore the most common and powerful methods you can override.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Big Showdown: &lt;code&gt;create&lt;/code&gt; vs. &lt;code&gt;perform_create&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where so much confusion happens. Both seem to do the same thing! But their purpose is completely different, and choosing the right one is key to clean code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Override &lt;code&gt;create(self, request, *args, **kwargs)&lt;/code&gt; when you need to change the &lt;strong&gt;orchestration of the request or the shape of the final response&lt;/strong&gt;. Do you need to return a &lt;code&gt;202 Accepted&lt;/code&gt; instead of a &lt;code&gt;201 Created&lt;/code&gt;? Do you need to wrap the response in a &lt;code&gt;{ "data": ... }&lt;/code&gt; envelope? Do you need to do something before the serializer is even validated? That's a job for create.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Override &lt;code&gt;perform_create(self, serializer)&lt;/code&gt; when you just need to &lt;strong&gt;tweak how the object is saved&lt;/strong&gt;. This is a much cleaner, more focused hook. The default create method calls this after validation is successful. It's the perfect place to inject the current user or tenant ID right before saving.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A practical rule of thumb&lt;/strong&gt;: Always try to use &lt;code&gt;perform_create&lt;/code&gt; first. Only override the full &lt;code&gt;create&lt;/code&gt; method if you absolutely have to manipulate the HTTP response or the flow itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In your OrderViewSet
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderViewSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelViewSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... queryset and serializer_class definitions ...
&lt;/span&gt;
    &lt;span class="c1"&gt;# This is the CLEAN way to stamp the user
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# The serializer is already validated here.
&lt;/span&gt;        &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Only override this if you need to change the whole dance
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Maybe you need to check inventory before even trying to validate
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;check_inventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item out of stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_400_BAD_REQUEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Now call the original 'create' logic
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This principle of separating the HTTP-level orchestration (create) from the persistence hook (perform_create) is key. Now, let's apply that same logic to updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Update Trinity: &lt;code&gt;update&lt;/code&gt; (PUT), &lt;code&gt;partial_update&lt;/code&gt; (PATCH), and &lt;code&gt;perform_update&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Updates are more complex than creates because there are two different ways to do them: a full update (&lt;code&gt;PUT&lt;/code&gt;) and a partial one (&lt;code&gt;PATCH&lt;/code&gt;). DRF gives you three distinct methods to control this process.&lt;/p&gt;

&lt;p&gt;First, let's get the flow right. It's a common point of confusion. The methods &lt;code&gt;update&lt;/code&gt; and &lt;code&gt;partial_update&lt;/code&gt; are peers; one does not call the other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A &lt;code&gt;PUT&lt;/code&gt; request is routed to the &lt;code&gt;update()&lt;/code&gt; method. After validation, &lt;code&gt;update()&lt;/code&gt; calls &lt;code&gt;perform_update()&lt;/code&gt; to save the changes.&lt;br&gt;
&lt;strong&gt;Flow&lt;/strong&gt;: &lt;code&gt;PUT Request&lt;/code&gt; → &lt;code&gt;update()&lt;/code&gt; → &lt;code&gt;perform_update()&lt;/code&gt; → &lt;code&gt;serializer.save()&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;code&gt;PATCH&lt;/code&gt; request is routed to the &lt;code&gt;partial_update()&lt;/code&gt; method. After partial validation, &lt;code&gt;partial_update()&lt;/code&gt; also calls &lt;code&gt;perform_update()&lt;/code&gt; to save the changes.&lt;br&gt;
&lt;strong&gt;Flow&lt;/strong&gt;: &lt;code&gt;PATCH Request&lt;/code&gt; → &lt;code&gt;partial_update()&lt;/code&gt; → &lt;code&gt;perform_update()&lt;/code&gt; → &lt;code&gt;serializer.save()&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;perform_update()&lt;/code&gt; is the shared, final step for both. With that in mind, here’s when to override each one.&lt;/p&gt;




&lt;h3&gt;
  
  
  When to Override &lt;code&gt;perform_update(self, serializer)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is your go-to hook for 90% of update logic. Override &lt;code&gt;perform_update&lt;/code&gt; when you have code that &lt;strong&gt;must run for any update, whether it's a&lt;/strong&gt; &lt;code&gt;PUT&lt;/code&gt; &lt;strong&gt;or a&lt;/strong&gt; &lt;code&gt;PATCH&lt;/code&gt;. It keeps your code DRY (Don't Repeat Yourself) by providing a single place for shared save-time logic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why Override?&lt;/strong&gt; You need to stamp a field, clear a cache, or log an update without caring if it was a full or partial change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: You always want to record which user was the last person to modify an object.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# This logic runs for both PUT and PATCH, after validation.
&lt;/span&gt;    &lt;span class="c1"&gt;# It's the cleanest place for shared save-time hooks.
&lt;/span&gt;    &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_updated_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Override &lt;code&gt;partial_update(self, request, *args, **kwargs)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Override the &lt;code&gt;partial_update&lt;/code&gt; method for logic that should &lt;strong&gt;only run during a partial change&lt;/strong&gt; (&lt;code&gt;PATCH&lt;/code&gt;). This is incredibly useful for implementing fine-grained permissions or triggering specific side effects when certain fields are modified&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why Override?&lt;/strong&gt; You want to allow different update rules for different fields, or you want to react to specific, targeted changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: In a project management system, maybe anyone can &lt;code&gt;PATCH&lt;/code&gt; a task's &lt;code&gt;description&lt;/code&gt;, but only a manager can &lt;code&gt;PATCH&lt;/code&gt; its &lt;code&gt;due_date&lt;/code&gt;. Overriding &lt;code&gt;partial_update&lt;/code&gt; is the perfect place to check for this.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;partial_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Custom logic just for PATCH
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;due_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only managers can change the due date.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_403_FORBIDDEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Another example: trigger a notification ONLY if the status changes
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;old_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
        &lt;span class="n"&gt;new_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;old_status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;notify_team_of_status_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;partial_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Override &lt;code&gt;update(self, request, *args, **kwargs)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Override the &lt;code&gt;update&lt;/code&gt; method when you need to add logic that is &lt;strong&gt;specific to a full resource replacement&lt;/strong&gt; (&lt;code&gt;PUT&lt;/code&gt;). Because &lt;code&gt;PUT&lt;/code&gt; implies replacing the entire resource, you might have preconditions or post-conditions that don't apply to a simple &lt;code&gt;PATCH&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why Override?&lt;/strong&gt; You want to enforce a strict "all or nothing" replacement policy or perform an action that only makes sense when the entire object is changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Imagine you have a &lt;code&gt;Settings&lt;/code&gt; object. You could override &lt;code&gt;update&lt;/code&gt; to ensure that a &lt;code&gt;PUT&lt;/code&gt; request truly contains every single required setting field, preventing users from accidentally wiping out settings by sending an incomplete object. You could also log a specific "Settings Overwritten" event that is more severe than a simple field change.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Custom logic just for PUT
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;theme&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notifications&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timezone&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A full update requires all setting keys.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_400_BAD_REQUEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Log the major change
&lt;/span&gt;    &lt;span class="nf"&gt;log_settings_overwritten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Other Useful ViewSet Hooks
&lt;/h2&gt;

&lt;p&gt;Beyond &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt;, other methods give you powerful control over the request lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;list()&lt;/code&gt;: Override this when you need to reshape the entire list response beyond a simple array of objects, for example, by adding metadata or aggregation summaries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;retrieve()&lt;/code&gt;: Perfect for when a single object's response needs extra context that depends on the request, like adding a &lt;code&gt;can_edit&lt;/code&gt; flag for the current user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;destroy()&lt;/code&gt;: The default just deletes the object. Override this to implement soft deletes, check for dependencies, or record an audit log event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;get_queryset()&lt;/code&gt;: This is one of the most important overrides for performance and security. It’s where you scope the data (e.g., a user only sees their own invoices) and where you should add performance boosters like &lt;code&gt;select_related&lt;/code&gt; and &lt;code&gt;prefetch_related&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;get_serializer_class()&lt;/code&gt;: Your viewset's "chameleon." It lets you use different serializers for different actions (e.g., a summary for &lt;code&gt;list&lt;/code&gt;, details for &lt;code&gt;retrieve&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;get_permissions()&lt;/code&gt;: Great for when permissions change based on the action (e.g., anyone can &lt;code&gt;GET&lt;/code&gt;, but only staff can &lt;code&gt;POST&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Decision Guide: Where Does This Logic Go?
&lt;/h2&gt;

&lt;p&gt;Let’s boil everything from both articles down to a simple cheat sheet for a clean DRF architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is it about the shape, structure, or validity of data?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt;: Serializer (&lt;code&gt;validate&lt;/code&gt;, &lt;code&gt;validate_&amp;lt;field&amp;gt;&lt;/code&gt;, &lt;code&gt;to_representation&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Is it about managing the HTTP request/response flow, like returning a custom status code or handling action-specific permissions?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt;: ViewSet action (&lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Is it about changing how an object is saved after validation, for both &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;PATCH&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt;: ViewSet &lt;code&gt;perform_*&lt;/code&gt; method (&lt;code&gt;perform_create&lt;/code&gt;, &lt;code&gt;perform_update&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Is it about filtering the main list of objects based on the user or request?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt;: ViewSet &lt;code&gt;get_queryset&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Keeping these boundaries clear isn't just an abstract architectural exercise. It’s a practical strategy for building software that can grow and adapt without collapsing under its own weight. Your team will thank you. And more importantly, your future self will, too.&lt;/p&gt;

</description>
      <category>cleancoding</category>
      <category>django</category>
      <category>drf</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Part 1: Django REST Framework: When (and When Not) to Override Serializers and Viewsets</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 10 Aug 2025 14:54:57 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/part-1-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-11a7</link>
      <guid>https://dev.to/soldatov-ss/part-1-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-11a7</guid>
      <description>&lt;h2&gt;
  
  
  DRF, Part 1: Serializer Overrides
&lt;/h2&gt;

&lt;p&gt;Ask any Django developer where to put validation, formatting, and orchestration logic, and you’ll often get different answers — some say “just put it in the serializer,” others drop everything in the viewset.&lt;/p&gt;

&lt;p&gt;This works… until it doesn’t.&lt;br&gt;
When your API grows, mixing concerns makes code brittle, hard to test, and painful to maintain. Knowing exactly &lt;strong&gt;when&lt;/strong&gt; to override a serializer method and &lt;strong&gt;when&lt;/strong&gt; to override a viewset method is key to keeping a codebase clean.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Serializer's Core Mission
&lt;/h3&gt;

&lt;p&gt;Think of your serializer as the security guard at the door of your database. It's responsible for three primary tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt;: Checking the ID of incoming data to ensure it's legitimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation&lt;/strong&gt;: Making sure the data is in the right format before being saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Representation&lt;/strong&gt;: Deciding how the data should be presented when it's sent back out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Its world is small and focused: just the data. Let's look at the tools it uses.&lt;/p&gt;


&lt;h3&gt;
  
  
  1) Serializer method overrides
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;br&gt;
Serializers control what may enter and how it leaves: validation, input normalization, output formatting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;validate_&amp;lt;field_name&amp;gt;(self, value)&lt;/code&gt; - field-level checks&lt;br&gt;
This is your go-to for single-field validation. Django's model fields and DRF's serializer fields handle the basics, like checking if a field is an integer or a valid email. But what about your specific business rules?&lt;/p&gt;

&lt;p&gt;Use this when a single field has a rule that only it needs to worry about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Imagine you have a &lt;code&gt;Product&lt;/code&gt; model with a &lt;code&gt;discount_percentage&lt;/code&gt; field. For most products, any discount is fine. But for the "Electronics" category, you need to cap it at 50% to protect your margins.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;validate(self, data)&lt;/code&gt; - cross-field validation&lt;br&gt;
Sometimes fields can't be validated in isolation. They have relationships; they depend on each other. &lt;code&gt;validate&lt;/code&gt; is where your fields have a conversation. It runs after all the individual &lt;code&gt;validate_&amp;lt;field&amp;gt;&lt;/code&gt; methods have passed.&lt;/p&gt;

&lt;p&gt;Use this for cross-field validation.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;: A classic case is a &lt;code&gt;PromoCampaign&lt;/code&gt; model with a &lt;code&gt;start_date&lt;/code&gt; and an &lt;code&gt;end_date&lt;/code&gt;. It makes no sense for the promotion to end before it even begins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Check that the start date is before the end date.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;end_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End date must occur after start date.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough, right? This keeps your data integrity rules right next to the data definition.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;create(self, validated_data)&lt;/code&gt; &lt;br&gt;
DRF's default &lt;code&gt;create&lt;/code&gt; method is wonderfully simple: it just unpacks your validated data and calls &lt;code&gt;YourModel.objects.create(**validated_data)&lt;/code&gt;. But sometimes "simple" isn't enough.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Override &lt;code&gt;create&lt;/code&gt; when you need to control exactly how a new object instance comes into being. This could mean:&lt;/li&gt;
&lt;li&gt;Creating related objects in the same transaction.&lt;/li&gt;
&lt;li&gt;Injecting data that doesn't come from the user, like &lt;code&gt;created_by=self.context['request'].user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Handling nested serializers for write operations.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In a UserProfileSerializer that also creates a user
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Stamp the current user from the view's context
&lt;/span&gt;    &lt;span class="n"&gt;created_by_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;created_by_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;update(self, instance, validated_data)&lt;/code&gt;&lt;br&gt;
Just like &lt;code&gt;create&lt;/code&gt;, the default &lt;code&gt;update&lt;/code&gt; method loops through the validated data and does a &lt;code&gt;setattr(instance, key, value)&lt;/code&gt; for each item before calling &lt;code&gt;instance.save()&lt;/code&gt;. You should override it when you have more complex update logic.&lt;/p&gt;

&lt;p&gt;Maybe you need to prevent updates if an order is already "shipped." Or perhaps updating one field requires a calculated change to another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: When updating a blog post, you want to replace all its tags, not just add new ones. The default &lt;code&gt;update&lt;/code&gt; for a many-to-many relationship might not do exactly what you want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In a PostSerializer with a 'tags' field
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tags_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# This is the default behavior for all other fields
&lt;/span&gt;    &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Now, handle the tags with our custom logic
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tags_data&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Replaces all existing tags
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;to_representation(self, instance)&lt;/code&gt;&lt;br&gt;
This is the "glow-up" method. It's the last stop before your data is serialized and sent out into the world. Its job is to shape the outbound data. The model might store a user ID, but you want to show their full name. The database has a &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt;, but you want to add a &lt;code&gt;full_name&lt;/code&gt; field to the API response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Add a computed field to your User serializer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In your UserSerializer
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_representation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Get the default representation
&lt;/span&gt;    &lt;span class="n"&gt;representation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;to_representation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Add our custom field
&lt;/span&gt;    &lt;span class="n"&gt;representation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_full_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;representation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  So, When Should Serializers Just Say No?
&lt;/h3&gt;

&lt;p&gt;This is just as important. A serializer that does too much becomes a god object. Here’s what doesn't belong in a serializer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy Side Effects&lt;/strong&gt;: Sending emails, charging credit cards, updating inventory in another system, calling third-party APIs. This is a one-way ticket to a maintenance nightmare. If the database transaction fails after the email is sent, what do you do? This logic belongs elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex Queries&lt;/strong&gt;: A serializer's &lt;code&gt;validate&lt;/code&gt; method shouldn't be making five different database calls to check a condition. That logic should live in a dedicated place (like a "selector" function or repository's function) and be called from the view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business Workflows&lt;/strong&gt;: A multi-step process, like "register user, create trial subscription, and schedule a welcome email," is a business workflow. It's too high-level for a serializer. This is a job for a service layer.&lt;/p&gt;




&lt;p&gt;In &lt;a href="https://dev.to/soldatov-ss/part-2-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-3j32"&gt;Part 2&lt;/a&gt; of this series, we’ll dive into the ViewSet's role as the orchestra's conductor and explore how to use its methods—and a service layer—to keep your application logic clean and organized.&lt;/p&gt;

</description>
      <category>cleancoding</category>
      <category>django</category>
      <category>drf</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why Django REST Framework doesn't show your custom validation error messages (and what to do about it)</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Wed, 21 May 2025 12:52:56 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/why-django-rest-framework-doesnt-show-your-custom-validation-error-messages-and-what-to-do-about-2dcl</link>
      <guid>https://dev.to/soldatov-ss/why-django-rest-framework-doesnt-show-your-custom-validation-error-messages-and-what-to-do-about-2dcl</guid>
      <description>&lt;p&gt;The GitHub Discussion: &lt;a href="https://github.com/encode/django-rest-framework/discussions/7850" rel="noopener noreferrer"&gt;link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5qri6wzb07ou1putio7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5qri6wzb07ou1putio7.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue developers face:
&lt;/h3&gt;

&lt;p&gt;When developing with Django REST Framework (DRF), developers typically run into issues where error messages for custom validation specified on Django model constraints (e.g., UniqueConstraint.violation_error_message) are not passed through to API responses. DRF returns default validation error messages instead.&lt;/p&gt;

&lt;p&gt;For example, a custom error message like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;violation_error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;This email and username combination already exists.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...might never actually reach the API client.&lt;/p&gt;

&lt;p&gt;Instead, you'll see something generic like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;non_field_errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The fields email, username must make a unique set.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what the DRF maintainers had to say about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DRF validation is deliberately occurring at the serializer level, &lt;strong&gt;before&lt;/strong&gt; the creation of the model instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It intentionally avoids calling Django's &lt;code&gt;full_clean()&lt;/code&gt; in this processes, as serializers are meant to validate incoming data instead of an already-created model instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Due to the complexity and backward compatibility issues, there are no immediate plans from DRF for changing this behavior.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words - DRF prioritises serialiser checking for API input, which means that unless you explicitly call model checking (e.g. via &lt;code&gt;full_clean()&lt;/code&gt;), constraint errors and their custom messages will never be called or shown in API responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Possible solutions:
&lt;/h3&gt;

&lt;p&gt;Developers can tackle this issue through several practical approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicitly calling &lt;code&gt;full_clean()&lt;/code&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MyModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;validated_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_clean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;DjangoValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;DRFValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom serializer validators:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This method just overrides DRF's built-in UniqueTogetherValidator just to replace the error message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomUniqueValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UniqueTogetherValidator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This email and username combination already exists.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Sometimes just double your validation code:&lt;/strong&gt;
Sometimes, custom validators and extra abstractions aren't needed. Although we all know the DRY principle, sometimes it's acceptable to just dublicate simple validation logicdirectly into your serializer
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyModelSerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelSerializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyModel&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;validators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;UniqueTogetherValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;queryset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MyModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This email and username combination already exists.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Summary:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: DRF doesn't carry forward any custom validation messages defined in Django.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reason&lt;/strong&gt;: For reasons of architecture and backward compatibility, DRF validates its data independently from Django's model validation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Important Warnings:
&lt;/h4&gt;

&lt;p&gt;Be careful where you place validation logic. Django models can be modified via the admin interface, serializers, or directly with bulk operations (.update()).&lt;/p&gt;

&lt;p&gt;Placing the critical validation logic into &lt;code&gt;save()&lt;/code&gt; or &lt;code&gt;full_clean()&lt;/code&gt; is problematic, as those aren't called when performing bulk updates or certain direct model manipulations. Always bear these edge cases in mind when developing your validation plans.&lt;/p&gt;

&lt;p&gt;This approach keeps your validation logic consistent, reusable, and well-documented throughout your project.&lt;/p&gt;

</description>
      <category>django</category>
      <category>drf</category>
      <category>python</category>
    </item>
    <item>
      <title>Boost Your Django Docker Images by using UV package and project manager</title>
      <dc:creator>Soldatov Serhii</dc:creator>
      <pubDate>Sun, 30 Mar 2025 14:55:56 +0000</pubDate>
      <link>https://dev.to/soldatov-ss/boost-your-django-docker-images-by-using-uv-package-and-project-manager-16lj</link>
      <guid>https://dev.to/soldatov-ss/boost-your-django-docker-images-by-using-uv-package-and-project-manager-16lj</guid>
      <description>&lt;h2&gt;
  
  
  What's UV?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;UV&lt;/strong&gt; is an ultra-fast Python package manager written in Rust by the &lt;a href="https://github.com/astral-sh" rel="noopener noreferrer"&gt;Astral team&lt;/a&gt;.&lt;br&gt;
You might already be familiar with another great product from Astral - &lt;a href="https://github.com/astral-sh/ruff" rel="noopener noreferrer"&gt;ruff&lt;/a&gt; &lt;em&gt;(popular Python linter)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It drastically reduces dependency installation time and produces smaller, optimized Docker images—ideal for enhancing your Django project's Docker workflow.&lt;br&gt;
Let's dive in!&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick start:
&lt;/h2&gt;

&lt;p&gt;Follow these simple steps to get your local environment ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv venv    # 1. Create a UV Virtual Environment
source .venv/bin/activate    # 2. Activate the Virtual Environment
uv sync --all-groups    # 3. Install Project Dependencies
docker-compose up --build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This installs both &lt;strong&gt;production&lt;/strong&gt; and &lt;strong&gt;development&lt;/strong&gt; dependencies.&lt;br&gt;
Use &lt;code&gt;uv sync --no-dev&lt;/code&gt; if you want &lt;strong&gt;production-only&lt;/strong&gt; packages.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here's a breakdown of a Dockerfile designed specifically for Django applications using UV to manage Python dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS base
FROM base AS builder

# Set up environment
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

WORKDIR /app

# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --all-groups

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --all-groups

FROM base
COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000

CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Base Image
&lt;/h2&gt;

&lt;p&gt;We start by leveraging a slim Python image &lt;strong&gt;with UV pre-installed&lt;/strong&gt;, optimized for minimal size and maximum speed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS base
FROM base AS builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Environment Setup
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PYTHONDONTWRITEBYTECODE&lt;/code&gt; prevents Python from writing .pyc files to disk.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PYTHONUNBUFFERED&lt;/code&gt; ensures immediate logging output.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UV_COMPILE_BYTECODE&lt;/code&gt; compiles Python bytecode for faster startup.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UV_LINK_MODE=copy&lt;/code&gt; configures UV to copy dependencies instead of symlinking (improving compatibility).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UV_PROJECT_ENVIRONMENT&lt;/code&gt; sets the virtual environment path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;!Permission denied issue&lt;/strong&gt;:&lt;br&gt;
You might encounter permission issues when using the default virtual environment paths provided by UV as and I faced:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flk1pjr4xy4nbbc5qi2vp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flk1pjr4xy4nbbc5qi2vp.png" alt="PermissionDeniedImage" width="800" height="77"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can handle this issue in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;(Recommended)&lt;/em&gt;
By adding second volume entry(&lt;code&gt;/app/.venv&lt;/code&gt;) in your docker-compose file.
Since the &lt;code&gt;.venv&lt;/code&gt; directory in the container is now completely isolated from your host filesystem and Docker can't change ownership or permissions on your local &lt;code&gt;.venv&lt;/code&gt; folder, which solves your permission issues.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2.By explicitly setting &lt;code&gt;UV_PROJECT_ENVIRONMENT&lt;/code&gt; to a dedicated path (/app/venv), we create a clean separation from default or locally-created environments, effectively resolving permission conflicts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dependency Installation (Optimized Caching)
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This example is adapted from the official UV example: &lt;a href="https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --all-groups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UV installs dependencies from &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt;&lt;br&gt;
Here, I've used &lt;code&gt;--all-groups&lt;/code&gt; to install every dependency group defined in &lt;code&gt;pyproject.toml&lt;/code&gt; (including development dependencies like linting tools). For production builds, consider switching to &lt;code&gt;--no-dev&lt;/code&gt; to install only production dependencies. For detailed guidance on managing dependency groups, refer to the official Dependency &lt;a href="https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups" rel="noopener noreferrer"&gt;Groups documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding Project Source and Finalizing Installation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --all-groups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;!Note:&lt;/strong&gt; I've also used &lt;code&gt;--all-groups&lt;/code&gt; in there.&lt;br&gt;
By adding your project's source after installing dependencies, Docker efficiently caches layers, speeding up rebuilds when code changes occur.&lt;/p&gt;
&lt;h2&gt;
  
  
  Final Runtime Stage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM base
COPY --from=builder /app /app
ENV PATH="/app/venv/bin:$PATH"
EXPOSE 8000

CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this final image:&lt;br&gt;
✅ Only the necessary files and pre-installed dependencies are copied from the builder stage.&lt;br&gt;
✅ Django runs within UV's isolated virtual environment.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Important Considerations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production Use&lt;/strong&gt;:
This setup uses Django’s built-in development server (&lt;code&gt;runserver&lt;/code&gt;). For production environments, consider switching to a production-grade server like &lt;code&gt;gunicorn&lt;/code&gt; or &lt;code&gt;uvicorn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running Commands with UV&lt;/strong&gt;:
After configuring your virtual environment with UV, you &lt;strong&gt;MUST&lt;/strong&gt; prefix your commands with &lt;code&gt;uv run&lt;/code&gt;, e.g.:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv run python manage.py ...
uv run pytest -s -v
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Otherwise, commands executed outside UV's context will fail.&lt;/p&gt;

&lt;p&gt;Here's how can look like your &lt;code&gt;start.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset
set -o xtrace

uv run wait_for_postgres.py

uv run manage.py migrate
uv run manage.py runserver 0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your optimized Dockerfile is now ready, resulting in faster builds, smaller images, and better caching strategies for your Django application.&lt;/p&gt;

&lt;p&gt;Here's quick time comparison:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7xrofaop132570lu0wr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7xrofaop132570lu0wr.png" alt="New docker file" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fluqx36jqkdynyeuamafg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fluqx36jqkdynyeuamafg.png" alt="Old docker file" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the optimized Dockerfile, the build process completed in just 15.8s, with only 6.7s dedicated to installing dependencies.&lt;br&gt;
In contrast, the previous Dockerfile took 34.6s, spending 28.3s installing dependencies.&lt;/p&gt;

&lt;p&gt;You can easily measure this improvement yourself by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time docker build -t container-name . --no-cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, here's an example of &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[project]
name = "piedpiper-web"
version = "0.1.0"
description = "Piedpiper web"
readme = "README.md"
requires-python = "&amp;gt;=3.13"

# Core application dependencies
dependencies = [
    "boto3~=1.37",
    "dj-database-url==2.3.0",
    "dj-rest-auth==7.0.1",
    "django==5.1.7",
    "django-allauth==65.3.0",
    "django-autoslug==1.9.9",
    "django-configurations==2.5.1",
    "django-cors-headers==4.7.0",
    "django-filter==25.1",
    "django-model-utils==5.0.0",
    "django-role-permissions==3.2.0",
    "django-storages==1.14.5",
    "django-unique-upload==0.2.1",
    "djangorestframework==3.15.2",
    "djangorestframework-api-key==3.0.0",
    "djangorestframework-simplejwt==5.5.0",
    "gunicorn==23.0.0",
    "pillow~=11.1",
    "psycopg2-binary==2.9.10",
    "python-dotenv==1.0.1",
    "requests==2.32.3",
    "setuptools==77.0.3",
]

[dependency-groups]
# Linting and code quality dependencies
lint = [
    "black==25.1.0",
    "flake8==7.1.2",
    "isort==6.0.1",
    "pre-commit==4.2.0",
    "ruff==0.11.2",
]

# dev dependencies
dev = [
    "django-silk&amp;gt;=5.3.2",
    "nplusone&amp;gt;=1.0.0",
    "ipdb==0.13.13",
    "ipython==8.34.0",
    "mock==5.2.0",
    "coverage~=7.7",
    "pytest-django==4.10.0",
    "factory-boy==3.3.3"
    "drf-yasg~=1.21",
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  web:
    restart: always
    build:
      context: .
      dockerfile: Dockerfile
      target: builder
    command: bash scripts/start.sh
    working_dir: /app
    volumes:
      - ./:/app
      - /app/.venv
    ports:
      - "8000:8000"
    depends_on:
      - postgres
    env_file:
      - .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  More resourses:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;Official documentation&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.saaspegasus.com/guides/uv-deep-dive/" rel="noopener noreferrer"&gt;uv: An In-Depth Guide to Python's Fast and Ambitious New Package Manager&lt;/a&gt;&lt;br&gt;
&lt;a href="https://depot.dev/docs/container-builds/how-to-guides/optimal-dockerfiles/python-uv-dockerfile" rel="noopener noreferrer"&gt;Best practice Dockerfile for Python with uv&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hope it was useful,&lt;br&gt;
Thanks for reading 🙌&lt;/p&gt;

</description>
      <category>python</category>
      <category>uv</category>
      <category>django</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
