<?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: Descope</title>
    <description>The latest articles on DEV Community by Descope (@descope).</description>
    <link>https://dev.to/descope</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%2Forganization%2Fprofile_image%2F12798%2Fc2ebf8e9-6ee5-4b7b-93fd-4e62def6c983.png</url>
      <title>DEV Community: Descope</title>
      <link>https://dev.to/descope</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/descope"/>
    <language>en</language>
    <item>
      <title>OAuth vs. API Keys for Agentic AI</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 17 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/oauth-vs-api-keys-for-agentic-ai-4njg</link>
      <guid>https://dev.to/descope/oauth-vs-api-keys-for-agentic-ai-4njg</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/oauth-vs-api-keys" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're a senior developer or architect, you've likely weighed the OAuth vs. API keys tradeoff many times to balance simplicity against security and convenience against control. But those API security concerns always assumed traditional API clients: web apps, mobile apps, and predictable server-to-server integrations. Agentic AI systems challenge those assumptions.&lt;/p&gt;

&lt;p&gt;Unlike traditional API clients, agentic AI systems are autonomous, tool-using, and inherently non-deterministic. They make real-time decisions, chain multiple API calls together, and sometimes take actions their creators never explicitly programmed. When an AI agent can decide to delete your production database or book a flight on your behalf, the stakes of API authentication are high, and &lt;a href="https://www.reddit.com/r/vibecoding/comments/1mo0j3p/never_touching_cursor_again/" rel="noopener noreferrer"&gt;accidents happen&lt;/a&gt;. In this article, you'll reexamine the outstanding debate between OAuth and API keys through the lens of agentic AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are API keys?
&lt;/h2&gt;

&lt;p&gt;If you've used paid APIs from services like Claude, OpenAI, or Mailgun, you've already used API keys. API keys are simple, static credentials; typically long alphanumeric strings that act as both identifier and password rolled into one. They're given by API providers and included in requests, usually as a header or query parameter, to authenticate the caller.&lt;/p&gt;

&lt;h3&gt;
  
  
  How API keys work
&lt;/h3&gt;

&lt;p&gt;The workflow is straightforward. A developer generates an API key from a provider's dashboard, embeds it in their application, and sends it with every API request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/data&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer sk_live_a8f3j29dk3j2d9fj2k3d&lt;/span&gt;
&lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server validates the key against its database and either grants or denies access. It doesn't involve any redirects, token exchanges, or complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traditional use cases
&lt;/h3&gt;

&lt;p&gt;API keys have been used in machine-to-machine (M2M) authentication for years, in controlled environments such as internal microservices, CI/CD pipelines, monitoring tools, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths / benefits of API keys
&lt;/h3&gt;

&lt;p&gt;There's a reason API keys remain popular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple to implement and deploy:&lt;/strong&gt; No complicated flows, no authorization servers, no token management infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal overhead:&lt;/strong&gt; No redirects, no token juggling, no refresh logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Useful for low-risk, tightly controlled systems:&lt;/strong&gt; Where you trust the environment and the code using the key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For instance, if a script syncs data between two internal databases at 3 a.m., using API keys is perfectly fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations of API keys
&lt;/h3&gt;

&lt;p&gt;The following are some shortcomings of API keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long-lived and static by default:&lt;/strong&gt; API keys have no built-in expiration mechanism. Once issued, a key remains valid until explicitly revoked. How and when it's revoked depends on provider tooling and team discipline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No scopes:&lt;/strong&gt; API keys have no standardized concept of permissions. When it does exist, access control is defined ad hoc, per provider. The result is inconsistent granularity across services and a lack of interoperability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard to rotate at scale:&lt;/strong&gt; Rotating keys across dozens of services means coordinating deployments and risking downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No built-in user delegation:&lt;/strong&gt; API keys represent the application, not a specific user. There's no way to say "this action was taken by Alice".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited auditability and traceability:&lt;/strong&gt; Log entries show the API key was used, but not by whom, why, or in what context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's worth noting that individual providers have closed some of these gaps independently. For example: &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;GitHub supports fine-grained personal access tokens (PATs)&lt;/a&gt; with repository-level scoping and expiration. &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html" rel="noopener noreferrer"&gt;AWS allows per-key policy&lt;/a&gt; for its Key Management Service (KMS). &lt;a href="https://platform.claude.com/docs/en/build-with-claude/workspaces" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt; and &lt;a href="https://help.openai.com/en/articles/9186755-managing-projects-in-the-api-platform" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt; both offer workspace-scoped keys with configurable permissions. But each of these is a proprietary implementation: different APIs, different defaults, different enforcement. An agentic system that calls tools across five providers encounters five separate permission models with no single standard determining how keys are issued, scoped, rotated, or revoked. OAuth's value in this context isn't that it can do things API keys theoretically can't, but that it does them consistently, interoperably, and by a shared specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OAuth provides over API keys
&lt;/h2&gt;

&lt;p&gt;If you've ever clicked "Login with Google" or "Login with LinkedIn" on a website, you've seen &lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; in action.&lt;/p&gt;

&lt;p&gt;OAuth 2.0 fundamentally rethinks API authentication by separating two concerns that API keys conflate: who you are (authentication) and what you can do (authorization).&lt;/p&gt;

&lt;p&gt;Instead of a single static credential, OAuth uses short-lived access tokens granted through explicit authorization flows. These tokens carry specific permissions (called scopes) that define exactly what actions the bearer can perform.&lt;/p&gt;

&lt;p&gt;Here are the key ideas of OAuth that make it more secure inherently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separation between authentication and authorization:&lt;/strong&gt; With API keys, proving your identity and getting permissions are the same thing. OAuth splits these apart. An identity provider verifies who you are, an authorization server decides what you can access, and a resource server validates your token. Each component has a single, well-defined job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-grained scopes defining permissions:&lt;/strong&gt; Instead of all-or-nothing access, you request specific scopes like &lt;code&gt;read:calendar&lt;/code&gt; or &lt;code&gt;write:events&lt;/code&gt;. The authorization server issues a token that carries only those permissions. The API server enforces these boundaries automatically.&lt;/p&gt;

&lt;p&gt;A simplified OAuth request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /oauth/authorize?
  response_type=code
  &amp;amp;client_id=cal_agent_a8f3j29dk3
  &amp;amp;redirect_uri=https://myagent.com/callback
  &amp;amp;scope=read:calendar+write:events
  &amp;amp;state=x8f2k9d3j2a
  &amp;amp;code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &amp;amp;code_challenge_method=S256 HTTP/1.1
Host: auth.calendar.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Token-based, short-lived, revocable access:&lt;/strong&gt; Access tokens typically expire within an hour. When they do, the client uses a refresh token to get a new one without user intervention. But here's the key advantage: you can revoke a refresh token instantly, cutting off access without changing client credentials or redeploying code. The blast radius (extent of damage or impact in case of a leak) of a compromised token is limited to minutes, not months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standardized token exchange and per-action downscoping:&lt;/strong&gt; OAuth defines a standardized mechanism for exchanging one token for another with different scopes, audiences, and lifetimes through OAuth Token Exchange (RFC 8693).&lt;/p&gt;

&lt;p&gt;This enables a critical pattern for agentic AI systems: an agent can hold a base delegated token, then exchange it at runtime for narrowly scoped, ephemeral tokens tailored to each specific action or tool invocation.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An agent exchanges its base token for a &lt;code&gt;calendar:read&lt;/code&gt; token when analyzing schedules&lt;/li&gt;
&lt;li&gt;It exchanges again for a &lt;code&gt;refund:create&lt;/code&gt; token scoped to a specific transaction&lt;/li&gt;
&lt;li&gt;Each exchanged token is audience-restricted to the exact API being called and expires within minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures agents always operate with the minimum permissions required at the moment of action, dramatically reducing blast radius and preventing privilege accumulation over time.&lt;/p&gt;

&lt;p&gt;API keys lack any standardized, interoperable mechanism for token exchange or dynamic downscoping. Any similar behavior must be custom-built, inconsistent, and difficult to audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ability to represent human intent through consent flows:&lt;/strong&gt; Before any client gets access, the user sees a consent screen listing exactly what permissions are being requested. They click "Allow" or "Deny". This explicit grant means the client (say: an agent) operates with delegated authority, not stolen credentials. Since the user gives permission, it gets recorded.&lt;/p&gt;

&lt;p&gt;The diagram below shows how authentication, scopes, consent, and token lifecycle work together in the OAuth flow:&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%2Fwhwofp4zw20mw3anr8f6.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%2Fwhwofp4zw20mw3anr8f6.png" alt="Diagram illustrating the OAuth flow" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  OAuth benefits for modern architectures
&lt;/h3&gt;

&lt;p&gt;The capabilities of OAuth translate into concrete architectural advantages, especially as systems become more distributed and autonomous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delegation model aligns with trust boundaries:&lt;/strong&gt; OAuth is designed for third-party access. An AI agent accessing your Google Calendar is fundamentally a third party, even if you built it yourself. OAuth's delegation model reflects this reality. The agent doesn't hold your password. It holds a limited access that you approved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granular access provides least privilege by default:&lt;/strong&gt; Scopes enforce the principle of least privilege automatically. An agent building calendar summaries gets &lt;code&gt;read:calendar&lt;/code&gt; only. An agent scheduling meetings gets &lt;code&gt;read:calendar&lt;/code&gt; and &lt;code&gt;write:events&lt;/code&gt;. An agent never gets &lt;code&gt;delete:account&lt;/code&gt; unless you explicitly grant it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revocation and observability of token usage:&lt;/strong&gt; Every access token is traceable to a specific authorization grant, user, scope set, and timestamp. Your authorization server logs show exactly which agent did what, when, and under whose authority. If an agent misbehaves, you can revoke its token without affecting other systems or users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better fit for ecosystems with distributed autonomy and risk:&lt;/strong&gt; When multiple agents operate semi-independently, each with different trust levels and capabilities, OAuth provides the identity and permission infrastructure to manage that complexity safely. You're not sharing one master key across a dozen autonomous systems. You're issuing narrowly scoped, individually revocable tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic AI: a new category of API client
&lt;/h2&gt;

&lt;p&gt;Traditional API clients follow a script. A cron job runs at midnight, calls API endpoints sequentially, processes the data, and exits. A mobile app waits for user taps, makes predictable API calls, and displays results. The behavior is deterministic. Agentic AI systems are largely autonomous, operating on goals rather than hardcoded instructions. They learn from context, adapt to situations, and generate action sequences you never explicitly programmed.&lt;/p&gt;

&lt;p&gt;Consider a customer support agent built with an LLM. A user asks, "Why was I charged twice last month?" The agent doesn't follow a fixed script. Instead, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reasons that it needs to check billing history.&lt;/li&gt;
&lt;li&gt;Calls your payments API to retrieve customer transactions.&lt;/li&gt;
&lt;li&gt;Analyzes the results and notices a duplicate charge.&lt;/li&gt;
&lt;li&gt;Decides to check your refunds API to see if it was already addressed.&lt;/li&gt;
&lt;li&gt;Determines it wasn't refunded.&lt;/li&gt;
&lt;li&gt;Calls your refunds API to initiate a refund.&lt;/li&gt;
&lt;li&gt;Responds to the user with confirmation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a developer, you don't program steps 1 through 7 explicitly. Instead, you give the agent access to tools (the APIs), describe what each tool does, and let it figure out the sequence. The agent chains multiple API calls, makes contextual decisions, and takes financial action autonomously.&lt;/p&gt;

&lt;p&gt;This is fundamentally different from a &lt;a href="https://www.geeksforgeeks.org/blogs/what-is-a-webhook-and-how-to-use-it/" rel="noopener noreferrer"&gt;webhook&lt;/a&gt; that processes a payment and triggers a predefined workflow. The agent's behavior emerges from its training and the situation at hand. It operates beyond simple scripts or predictable machine-to-machine workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these characteristics matter for auth
&lt;/h3&gt;

&lt;p&gt;This autonomy creates new security requirements that API keys were never designed to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Need for granular, action-bound permissions:&lt;/strong&gt; When an AI agent can dynamically choose which APIs to call, you cannot give it blanket access to everything. You need to constrain it to specific actions: read billing data, yes; initiate refunds up to $100 USD, maybe; delete customer accounts, absolutely not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to audit and trace agent actions at fine resolution:&lt;/strong&gt; When something goes wrong, you need logs that show exactly what the agent did, why, and under whose authority. "API key used at 3:47 PM" is not enough. You need "Agent-CustomerSupport-v2 initiated $47 USD refund for user Alice under scope &lt;code&gt;refunds:create:limited&lt;/code&gt; at 3:47 PM."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need for safe delegation of user authority without exposing secrets:&lt;/strong&gt; The agent acts on behalf of users, but it should never hold their passwords or master credentials. It needs delegated, scoped authority that represents user intent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need for revocable, time-bounded permissions to reduce blast radius:&lt;/strong&gt; If an agent starts misbehaving for any reason, you should be able to cut off its access immediately with instant revocation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;API keys can't deliver this level of control. You need OAuth for its granular scope enforcement and built-in token revocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing API keys vs. OAuth in agentic AI scenarios
&lt;/h2&gt;

&lt;p&gt;Let's look at how a few real-life scenarios could use API Keys vs. OAuth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: an agent managing a user's calendar
&lt;/h3&gt;

&lt;p&gt;Consider an AI assistant that helps users manage their work calendar by reading events, finding conflicts, and suggesting optimal meeting times.&lt;/p&gt;

&lt;h4&gt;
  
  
  With API keys
&lt;/h4&gt;

&lt;p&gt;The agent uses a static key with full calendar access:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_calendar_full_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Agent can read, write, delete anything
&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.calendar.com/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.calendar.com/events/important-meeting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problems are serious. The API key grants complete access to all calendar operations. The agent can delete events, modify attendees, or clear entire calendars, even though it only needs to read events. If the key leaks through logs, prompt injection, or model output, attackers gain full calendar control. There's no user consent, no audit trail showing which specific agent performed which action, and no way to distinguish agent requests from legitimate server operations in logs.&lt;/p&gt;

&lt;h4&gt;
  
  
  With OAuth
&lt;/h4&gt;

&lt;p&gt;The user explicitly grants limited access through a consent flow:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests_oauthlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuth2Session&lt;/span&gt;

&lt;span class="c1"&gt;# Agent requests only read access
&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;calendar_agent_client&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&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;read:calendar&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="c1"&gt;# User sees: "Calendar Agent wants to: Read your calendar events"
# User clicks "Allow"
# Agent receives a token limited to reading
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://auth.calendar.com/token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;authorization_response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;callback_url&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# This works
&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.calendar.com/events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# This fails - token doesn't have write:calendar scope
&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.calendar.com/events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{...})&lt;/span&gt;  &lt;span class="c1"&gt;# 403 Forbidden
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token expires after an hour. If the agent misbehaves, the user revokes access instantly from their security settings. Audit logs clearly show: "calendar_agent_client accessed user Alice's calendar with scope &lt;code&gt;read:calendar&lt;/code&gt; at 2:34 PM". The blast radius is contained.&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%2F550p9kf7ch5y3x5jfpsk.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%2F550p9kf7ch5y3x5jfpsk.png" alt="An image illustrating the key differences between API key and OAuth approaches in agentic systems" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: an agent automating developer tooling
&lt;/h3&gt;

&lt;p&gt;Now let's consider an AI agent that helps developers by automatically creating GitHub issues from Slack conversations, assigning them to the right team members, and updating project boards.&lt;/p&gt;

&lt;h4&gt;
  
  
  With API keys
&lt;/h4&gt;

&lt;p&gt;A GitHub personal access token with full repo access sits in the agent's configuration:&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="n"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghp_full_repo_access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Agent can do anything
&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/repos/company/prod/issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&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;title&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;Bug found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Including destructive operations it shouldn't
&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/repos/company/prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The static token allows the agent to delete repositories, modify branch protection rules, or leak source code. There's no time limit, no scope restriction, and no clear audit trail linking actions back to the specific agent instance.&lt;/p&gt;

&lt;h4&gt;
  
  
  With OAuth
&lt;/h4&gt;

&lt;p&gt;The agent requests narrowly scoped access:&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;# Agent requests specific permissions
&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dev_agent_client&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&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;repo:issues:write&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;project:read&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="c1"&gt;# Token is bound to specific repos and actions
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_token&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="c1"&gt;# Agent can create issues
&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/repos/company/prod/issues&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&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;title&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;Bug found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# But cannot delete repos or access code
&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/repos/company/prod&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 403 Forbidden
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OAuth scopes restrict the agent to creating issues and reading project boards. The token expires after an hour and can be revoked if the agent starts creating spam issues. GitHub's audit log shows exactly which agent, acting under whose authority, performed each action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: multi-agent workflows
&lt;/h3&gt;

&lt;p&gt;Now consider an agentic system where multiple specialized agents collaborate: one agent researches competitors, another drafts marketing copy, and a third publishes content to your CMS.&lt;/p&gt;

&lt;h4&gt;
  
  
  With API keys
&lt;/h4&gt;

&lt;p&gt;All agents share the same CMS API key:&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="n"&gt;SHARED_CMS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cms_full_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Research agent (should only read)
&lt;/span&gt;&lt;span class="n"&gt;research_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHARED_CMS_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Writing agent (should read and draft)
&lt;/span&gt;&lt;span class="n"&gt;writing_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHARED_CMS_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Publishing agent (should publish drafts)
&lt;/span&gt;&lt;span class="n"&gt;publishing_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHARED_CMS_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# All have identical, full access
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates multiple problems. The research agent can accidentally publish content. The writing agent can delete published articles. If any agent is compromised, all three are compromised because they share credentials. Server logs show "CMS key used" but not which agent or why. There's no way to represent each agent's actual role and permissions.&lt;/p&gt;

&lt;h4&gt;
  
  
  With OAuth
&lt;/h4&gt;

&lt;p&gt;Each agent gets its own identity and scoped token:&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;# Research agent - read-only access
&lt;/span&gt;&lt;span class="n"&gt;research_oauth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;research_agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&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;content:read&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="c1"&gt;# Writing agent - read and draft
&lt;/span&gt;&lt;span class="n"&gt;writing_oauth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;writing_agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&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;content:read&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;content:draft&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="c1"&gt;# Publishing agent - publish only
&lt;/span&gt;&lt;span class="n"&gt;publishing_oauth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publishing_agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&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;content:publish&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;p&gt;Each agent operates with least privilege. The research agent cannot modify content. The writing agent cannot publish. The publishing agent cannot delete. If the writing agent is compromised through prompt injection, the attacker gains only drafting capabilities, not publishing or deletion. Audit logs clearly attribute each action to a specific agent identity with specific permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Model Context Protocol mandates OAuth
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.descope.com/learn/post/mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; is an open standard defining how AI systems should securely interact with external tools and data sources. MCP provides a structured way for agents to discover available tools, understand their capabilities, and invoke them safely.&lt;/p&gt;

&lt;p&gt;While MCP makes authorization optional for low-risk scenarios, it &lt;a href="https://www.descope.com/blog/post/oauth-2-0-vs-oauth-2-1" rel="noopener noreferrer"&gt;mandates OAuth 2.1&lt;/a&gt; when agents access user data or act on behalf of users, precisely the scenarios where autonomous agents make real-time decisions about tools and actions.&lt;/p&gt;

&lt;p&gt;MCP encourages OAuth 2.1 for the ability for every tool to explicitly define its required access scopes. When an agent wants to use a tool, it must authenticate using OAuth flows designed for agents running on servers without a traditional user interface (i.e., &lt;a href="https://en.wikipedia.org/wiki/Headless_software" rel="noopener noreferrer"&gt;headless&lt;/a&gt;). This typically means &lt;a href="https://curity.io/resources/learn/oauth-device-flow/" rel="noopener noreferrer"&gt;device flow&lt;/a&gt; (where users authorize on a separate device by entering a code) or authorization flows with &lt;a href="https://www.descope.com/learn/post/pkce" rel="noopener noreferrer"&gt;Proof Key for Code Exchange (PKCE)&lt;/a&gt;: a security extension that prevents authorization code theft by requiring a secret only the legitimate client knows. These flows ensure users grant explicit consent for the specific permissions each tool needs.&lt;/p&gt;

&lt;p&gt;MCP requires auditable, revocable, and safe delegation of authority. Each tool invocation is traceable to a specific token, scope set, and user consent. When an agent misbehaves or a tool becomes compromised, admins can revoke access immediately without affecting other tools or redeploying code.&lt;/p&gt;

&lt;p&gt;API keys lack the granularity, traceability, and revocation mechanisms MCP's security model requires. By mandating OAuth, MCP ensures agentic systems operate within clearly defined, user-controlled permission boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  When API keys still make sense
&lt;/h2&gt;

&lt;p&gt;OAuth isn't always the perfect choice. There are legitimate scenarios where API keys remain a more practical choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-agentic M2M automation:&lt;/strong&gt; Internal microservice calls on private networks don't need OAuth's complexity. When Service A calls Service B within your infrastructure, both under your complete control, a rotated API key works fine. The same applies to simple backend processes with predictable behavior (i.e., nightly ETL jobs, monitoring scripts, deployment pipelines). These systems follow deterministic logic with no autonomous decision-making.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-risk or high-control environments:&lt;/strong&gt; Internal admin tools accessible only from corporate networks, behind VPNs, with IP restrictions and vault-managed secrets, can safely use API keys. The infrastructure itself enforces security boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Startups or prototypes:&lt;/strong&gt; Early-stage products validating product-market fit can use API keys to move fast. However, OAuth migration might be required for scaling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Did you know?&lt;/strong&gt; Major AI providers like OpenAI and Anthropic still use API keys for millions of users because they're authenticating developer applications, not autonomous agents acting on behalf of end users. When those applications need to perform user-specific actions with granular permissions, OAuth becomes necessary at that layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick implementation guidance and best practices
&lt;/h2&gt;

&lt;p&gt;Use this decision tree to choose authentication based on your requirements:&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%2Fwwbd1aysqglcdh8z3cix.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%2Fwwbd1aysqglcdh8z3cix.png" alt="A decision tree that helps developers determine whether to use OAuth or API keys" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  For OAuth in agentic AI systems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implement narrow scopes:&lt;/strong&gt; Avoid wildcard permissions like &lt;code&gt;admin:*&lt;/code&gt; or &lt;code&gt;full_access&lt;/code&gt;. Define granular scopes that match actual operations: &lt;code&gt;calendar:read&lt;/code&gt;, &lt;code&gt;issues:create&lt;/code&gt;, &lt;code&gt;content:draft&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer short-lived access tokens with automatic refresh:&lt;/strong&gt; Set access token expiration to one hour or less. Use refresh tokens to obtain new access tokens without user intervention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require explicit user consent flows:&lt;/strong&gt; Users must see and approve exactly what permissions they're granting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bind agent identity to token issuance:&lt;/strong&gt; Each agent instance should have its own client ID. This allows per-agent revocation and helpful audit trails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable complete audit logging:&lt;/strong&gt; Log every token issuance, refresh, and API call with agent identity, scope, timestamp, and user context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Authorization Code Flow with PKCE and Device Code Flow:&lt;/strong&gt; For agents running on servers or in automated environments, these flows provide secure authentication without traditional browser redirects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For API keys
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotate regularly and automatically:&lt;/strong&gt; Implement 90-day rotation schedules with zero-downtime deployment strategies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store keys in secure vaults:&lt;/strong&gt; Use dedicated secret management systems like Google Secrets Manager or similar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply API rate limits and behavioral analytics:&lt;/strong&gt; Monitor for unusual patterns that might indicate compromise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict IP addresses or environments:&lt;/strong&gt; Limit key usage to known networks or infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition to OAuth once agents enter the ecosystem:&lt;/strong&gt; Plan this migration before adding autonomous capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agentic AI is transforming API authentication requirements. The simplicity of API keys comes at a cost: no granular permissions, no revocation, and limited auditability. OAuth provides the delegation model, scoped access, and security controls that autonomous systems demand. With protocols like MCP mandating OAuth for tool access, the industry is converging on a clear standard for intelligent, autonomous systems.&lt;/p&gt;

&lt;p&gt;OAuth implementation is becoming necessary for AI agents. &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; provides purpose-built authentication for agentic AI systems, with pre-configured OAuth flows, granular scope management, and agent identity infrastructure that scales from prototype to production.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Add Authentication and RBAC to a Webflow App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 15 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-rbac-to-a-webflow-app-2eia</link>
      <guid>https://dev.to/descope/add-authentication-and-rbac-to-a-webflow-app-2eia</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-rbac-webflow" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Authentication and authorization are essential for controlling user access and actions within applications, ensuring the right people can do the right things. However, implementing these features can be complex and error-prone, especially in &lt;a href="https://webflow.com/" rel="noopener noreferrer"&gt;Webflow&lt;/a&gt;, where server-side logic isn't easily accessible. Ineffective management of sessions and user roles can expose your application to security vulnerabilities.&lt;/p&gt;

&lt;p&gt;Consider a typical Webflow content management system (CMS) where team members collaborate on content but their access must vary based on their roles. Writers should manage only their own articles, while editors require full control to publish, delete, or oversee other authors' work. Role-based access control (&lt;a href="https://www.descope.com/learn/post/rbac" rel="noopener noreferrer"&gt;RBAC&lt;/a&gt;) simplifies this by assigning roles—such as Writer or Editor—to users and granting permissions accordingly.&lt;/p&gt;

&lt;p&gt;Traditionally, implementing authentication and RBAC required significant backend development and ongoing maintenance. With &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;, you can add these features to your Webflow app without writing complex backend code. Descope provides a visual workflow editor, &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt;, for authentication and integrated RBAC functionality, enabling you to add these features to your Webflow app without backend complexity. This tutorial will guide you through securing your Webflow CMS with Descope social login, email verification, and role-based permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up your Descope project and configuring an authentication flow
&lt;/h2&gt;

&lt;p&gt;Before implementing authentication and RBAC in your application, let's set up the development environment and configure Descope to meet your needs.&lt;/p&gt;

&lt;p&gt;Head over to the &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope website&lt;/a&gt; and create your free account. Once you've successfully created an account, you'll be guided through creating an authentication flow that matches your application's needs:&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%2Fplqg18yp1xe1nbo0h9ff.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%2Fplqg18yp1xe1nbo0h9ff.png" alt="Fig: Descope Welcome Dashboard" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a CMS application, you want to offer both email-based and social authentication to give users some flexibility in their preferred sign-in method.&lt;/p&gt;

&lt;p&gt;Navigate to the Flows section in the Descope Console and click Start from scratch:&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%2Fut6zm0w1s9wb0xwnaoiq.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%2Fut6zm0w1s9wb0xwnaoiq.png" alt="Fig: Flow creation dialog" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since this flow handles both new user registration and existing user authentication for the CMS, you can name it "publishpro auth flow". The flow editor presents a visual canvas where you design the authentication experience:&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%2Fy8yvt34gqoisqcsjblqg.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%2Fy8yvt34gqoisqcsjblqg.png" alt="Fig: Flow editor interface showing an empty canvas" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating and adding elements to the authentication interface
&lt;/h3&gt;

&lt;p&gt;You can now build the authentication interface by creating a Sign Up or Sign In screen. Click the + button to drag and drop a new screen. Inside the screen, click the screen body text element. On the right panel of the editor, go to the Content section and update the text to "Welcome to PublishPro". Then, scroll down to the Style section and set the style to the H2 element, as shown here:&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%2Fypf3cr7nglma62jqm45u.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%2Fypf3cr7nglma62jqm45u.png" alt="Fig: Add a welcome H2 text message to the screen" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drag and drop a Container element from the list of elements in the left panel of the screen. Then drag and drop an Email input element and a Button element inside this container. Select the Button and rename its Text value to "Submit" on the right panel. Then adjust its width to fit that of the container by toggling the Fill container button. Here's what 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%2F22jpnl2x4l5eokfmxw9z.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%2F22jpnl2x4l5eokfmxw9z.png" alt="Fig: Add container for Email input and Submit button" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding social login
&lt;/h3&gt;

&lt;p&gt;This setup handles email OTP authentication, providing a passwordless option for users. Next, you'll add social login as an alternative authentication method.&lt;/p&gt;

&lt;p&gt;To clearly distinguish between the two authentication methods, add a divider element between the methods. On the left panel, scroll down to the TEXT section and drag and drop a Divider element below the Button element:&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%2Fun0iqfoil107qh1fs01k.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%2Fun0iqfoil107qh1fs01k.png" alt="Fig: Add a Divider element to the screen" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While still on the left panel, scroll to the BUTTONS section and add a Google button from the list of &lt;a href="https://oauth.net/" rel="noopener noreferrer"&gt;OAuth&lt;/a&gt; providers right under the divider. You can choose other OAuth providers from the existing list, but this example uses only Google:&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%2F8r7xpqdt52u5n9th8qq3.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%2F8r7xpqdt52u5n9th8qq3.png" alt="Fig: Add a Google button under the divider" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that in place, you can click the Done button to return to the canvas. Here, you see the newly created screen, so connect it to the START of the flow, as follows:&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%2Fgxtzhgumlwga917y6khl.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%2Fgxtzhgumlwga917y6khl.png" alt="Fig: A sign-in or sign-up screen added to the canvas" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Linking the submit and socials buttons
&lt;/h3&gt;

&lt;p&gt;The next step is to link the Submit and Socials buttons on the newly created Sign Up or Sign In screen to their respective actions, ensuring they trigger the intended functionality when clicked. Click the + button and select Action. Then, search for and add both Sign Up or In / OTP / Email and Sign Up or In / OAuth actions to the canvas. Connect the Submit button to the Sign Up or In / OTP / Email action to handle email authentication and connect the Socials button to the Sign Up or In / OAuth action to handle social login:&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%2F27vfhe0vkdd9s9o2gve9.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%2F27vfhe0vkdd9s9o2gve9.png" alt="Fig: Added OTP/Email and OAuth authentication methods to the canvas" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These custom actions contain both actions and screens that handle the common flows inherent in OTP and OAuth authentication methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling new and existing users
&lt;/h3&gt;

&lt;p&gt;You also need to differentiate between new and existing users to customize their authentication experience. This can be done using the Condition option. Click the + button and select Condition to open a dialog for configuring an If-Else check. Set the step name to New/Existing User. In the If block, name it "New user" and use the &lt;code&gt;authInfo.firstSeen&lt;/code&gt; key with the Is True operator to identify first-time logins. For the Else block, simply name it "Existing" since it automatically handles returning users. Save the condition to complete the setup:&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%2F2uc5xiiulv1ghgldym1n.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%2F2uc5xiiulv1ghgldym1n.png" alt="Fig: Creating a New/Existing User condition on the flow canvas" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connect both Verify Code / OTP / Email and Sign Up or In / OAuth actions to the newly created condition so it performs the check every time a user logs 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%2Fn6jshzstkyvvu0uq27hh.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%2Fn6jshzstkyvvu0uq27hh.png" alt="Fig: Connect the existing authentication methods to a condition" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, a new screen, New user, needs to be added so that new users can provide their username and full name. To do this, create a new screen and add a Container element to the screen. Then inside the container, drag and drop the Display Name and Given Name inputs from the INPUTS section on the left panel. Also, add a button and change its text value to Continue. Remember to remove the screen body text element and save the new screen:&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%2Fnhuni8yv29tnw6tz7yzd.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%2Fnhuni8yv29tnw6tz7yzd.png" alt="Fig: New user details screen" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creating the screen, you need to add an action to update the user's profile. Click the + button, select Action, and in the Actions dialog that appears, search for and select Update User / Attributes. Edit the action to map the input fields from the New user screen (&lt;code&gt;form.displayName&lt;/code&gt; for Display Name and &lt;code&gt;form.givenName&lt;/code&gt; for Given Name) to their corresponding user attributes, ensuring new users' profile information is properly stored:&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%2Foii6g0tpplyb7um2qci2.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%2Foii6g0tpplyb7um2qci2.png" alt="Fig: Specify user attributes to be updated" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Completing the flow
&lt;/h3&gt;

&lt;p&gt;With the user detail collection screen and the update actions in place, you need to complete the flow by connecting these last two components. Start by connecting the New user screen's Submit button to the Update User / Attributes action, which ensures that when new users submit their details, the information gets properly stored in their user profile. The flow then needs two distinct paths to completion. The first path connects the Update User / Attributes action to the END node, handling new users who have just provided their additional information. The second path connects the Existing branch from the New/Existing user condition directly to the END of the flow, creating a streamlined experience for returning users who don't need to provide additional details. These connections work together to create two separate journeys through the authentication flow—one that collects necessary information from first-time users and another that provides a faster route for those returning to your application.&lt;/p&gt;

&lt;p&gt;Here is the final version of the flow you just created:&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%2Fmgpqk3tzv0xbp8lersbj.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%2Fmgpqk3tzv0xbp8lersbj.png" alt="Fig: Final version of the publishpro-auth-flow flow" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save the flow and click the Run button to test run the flow directly on the Descope Console:&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%2Fb1vbmitcx54zww2vrv7h.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%2Fb1vbmitcx54zww2vrv7h.png" alt="Fig: Preview of the publishpro-auth-flow on the Descope dashboard" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the PublishPro application
&lt;/h2&gt;

&lt;p&gt;The starter application is a CMS that currently lacks any authentication or access control.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://webflow.com/made-in-webflow/website/publish-pro" rel="noopener noreferrer"&gt;Here is the Webflow app.&lt;/a&gt; Currently, anyone, without signing in, can access the dashboard, see all articles in the system, and access creation, editing, deletion, and publishing features:&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%2Fzqy5c53in0ttg4pzlw6x.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%2Fzqy5c53in0ttg4pzlw6x.png" alt="Fig: PublishPro CMS starter version" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the application includes a navigation bar with a Sign In button, it lacks any functionality since authentication isn't implemented. This unrestricted access highlights two key security needs: protecting the dashboard behind authentication and implementing different access levels for various user roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing authentication
&lt;/h2&gt;

&lt;p&gt;Now that the authentication flow is set up in Descope, let's integrate it into the CMS application. You need to add the Descope web component and implement the authentication logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the required scripts
&lt;/h3&gt;

&lt;p&gt;To start, let's add the required Descope scripts, Descope web component, and user profile widget scripts to the Head code section of the Webflow app. These scripts provide the core functionality for the authentication system and user profile management. On the Webflow dashboard, under the Custom code tab of the site's settings, add the following scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@descope/web-component@latest/dist/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://static.descope.com/npm/@descope/user-profile-widget@0.0.113/dist/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to save the changes by clicking the Save button to successfully add the scripts to the Webflow app:&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%2Fp8nayv6y570zzmn5rxv0.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%2Fp8nayv6y570zzmn5rxv0.png" alt="Fig: Head code under the Custom code of the Webflow app" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Descope provides a &lt;a href="https://docs.descope.com/widgets" rel="noopener noreferrer"&gt;list of widgets&lt;/a&gt; that you can easily integrate alongside your authentication code to manage users' sessions:&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%2F4eimxsy2098eljm67afn.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%2F4eimxsy2098eljm67afn.png" alt="Fig: Descope User Profile Widget" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the Show Code button and select the Web Component option to see the sample code included in the preceding code snippet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding authentication and user profile modals
&lt;/h3&gt;

&lt;p&gt;Next, create a Code embed element for the authentication. You need two key components: an authentication modal and a user profile modal. These components handle user sign-in and profile management, respectively.&lt;/p&gt;

&lt;p&gt;For the authentication modal, create it under the Body element and name it &lt;code&gt;auth-modal&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Auth Modal --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"auth-modal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"auth-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;descope-wc&lt;/span&gt;
      &lt;span class="na"&gt;project-id=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_ID"&lt;/span&gt;
      &lt;span class="na"&gt;flow-id=&lt;/span&gt;&lt;span class="s"&gt;"publishpro-auth-flow"&lt;/span&gt;
      &lt;span class="na"&gt;theme=&lt;/span&gt;&lt;span class="s"&gt;"light"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to replace &lt;code&gt;YOUR_PROJECT_ID&lt;/code&gt; with the &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;project ID from your Descope Console&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%2Fy4c8mcp45b8i7m2v9slb.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%2Fy4c8mcp45b8i7m2v9slb.png" alt="Fig: Create auth-modal elements with Code embed" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, let's also create the user profile modal next to the Sign In button and name it &lt;code&gt;user-profile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Authenticated State --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-profile"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"user-profile"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"user-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"profile-widget-container"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"profile-widget-container"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;descope-user-profile-widget&lt;/span&gt;
      &lt;span class="na"&gt;project-id=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_ID"&lt;/span&gt;
      &lt;span class="na"&gt;widget-id=&lt;/span&gt;&lt;span class="s"&gt;"user-profile-widget"&lt;/span&gt;
      &lt;span class="na"&gt;theme=&lt;/span&gt;&lt;span class="s"&gt;"light"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to replace the &lt;code&gt;YOUR_PROJECT_ID&lt;/code&gt; with your project ID as well here:&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%2Fsqniyswvrfwiyek5r1ez.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%2Fsqniyswvrfwiyek5r1ez.png" alt="Fig: Create user-profile modal with Code embed" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since authentication is being implemented, the dashboard of the app should be visible only to authenticated users. Let's create a new Combo class named &lt;code&gt;w-hide&lt;/code&gt; with style &lt;code&gt;display: none !important&lt;/code&gt; to hide the dashboard from unauthorized users. Add this new class to the existing &lt;code&gt;main_wrapper&lt;/code&gt; class as well as to the &lt;code&gt;auth-modal&lt;/code&gt; and the &lt;code&gt;user-profile&lt;/code&gt; modal:&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%2Fzrzy7k19etexffhmob46.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%2Fzrzy7k19etexffhmob46.png" alt="Fig: Hide dashboard body from authorized users" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enhancing with custom styles
&lt;/h3&gt;

&lt;p&gt;You can use the &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag to add custom styles for the modals and other utility classes created in the remaining part of the project. This includes styles for the &lt;code&gt;auth-modal&lt;/code&gt;, &lt;code&gt;user-profile&lt;/code&gt;, and &lt;code&gt;widget-containers&lt;/code&gt;. Navigate back to the dashboard, check under the Custom code tab, and add the following CSS below the two &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags in the Head code section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.auth-modal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.auth-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.user-profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.user-name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;/* Utility Classes */&lt;/span&gt;
  &lt;span class="nc"&gt;._w-hide&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.user-profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.user-name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.user-name&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.profile-widget-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;/* Add some shadow and background to the widget */&lt;/span&gt;
  &lt;span class="nt"&gt;descope-user-profile-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to click the Save button for the styles to take effect on the project:&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%2F2gqny5wmhb0gsl8tbie6.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%2F2gqny5wmhb0gsl8tbie6.png" alt="Fig: Custom CSS for the authentication elements" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing custom authentication
&lt;/h3&gt;

&lt;p&gt;Next, let's add custom authentication to your Webflow app. To do this, go to the Custom code tab, scroll down to the Footer code section, and paste the code before the closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag. Let's break this down into easy steps.&lt;/p&gt;

&lt;p&gt;To begin, you need to wait for the DOM to be fully loaded. Then set up the necessary DOM elements and initialize event listeners:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get references to all the UI elements needed to be manipulated&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authModal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.auth-modal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mainWrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.main_wrapper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signInButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.sign-in-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.user-profile_container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profileWidgetContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-widget-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sidebar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.sidebar_wrapper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.dashboard_wrapper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Initialize Descope authentication components&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descope-wc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;authComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleAuthSuccess&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;authComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleAuthError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Set up the profile widget with logout handling&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profileWidget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descope-user-profile-widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;profileWidget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handleLogout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preceding initialization code ensures the app has access to all the necessary elements and sets up the Descope authentication component with its event listeners. Think of it as gathering all the tools before starting work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the authentication interface interactive
&lt;/h3&gt;

&lt;p&gt;Next, you need to handle user interactions with the authentication interface. This includes showing the login modal and managing the profile widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Show the auth modal when users click sign in&lt;/span&gt;
  &lt;span class="nx"&gt;signInButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;authModal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Toggle the profile drop-down when users click their name&lt;/span&gt;
  &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profileWidgetContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;profileWidgetContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nx"&gt;profileWidgetContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle returning from social login&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urlParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDescopeOAuthReturn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urlParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;descope-login-flow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isDescopeOAuthReturn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;authModal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&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;p&gt;These event listeners create the interactive elements of the authentication system. The code responds to user actions like clicking the sign-in button or toggling their profile menu. You also handle the special case of users returning from the social login.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing user sessions
&lt;/h3&gt;

&lt;p&gt;The heart of the authentication system lies in how successful logins, errors, and logouts are handled. The following functions manage the user's session and update the interface accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAuthSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;refreshJwt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Store the user's session data securely&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshJwt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Show authenticated UI&lt;/span&gt;
    &lt;span class="nx"&gt;authModal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;signInButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;mainWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Click to manage profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAuthError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auth Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleLogout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logout Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Clean up session data and refresh&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;p&gt;When authentication succeeds, the app securely stores the user's session data and updates the UI to show they're logged in. If something goes wrong, it logs the error. The logout function cleans up by removing stored data and refreshing the page.&lt;/p&gt;

&lt;p&gt;Finally, handle session persistence so users don't need to log in every time they visit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Check if user already has a valid session&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;authModal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;signInButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;signInButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;mainWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code checks for an existing session when the page loads. If found, it automatically restores the user's authenticated state without requiring them to log in again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the authentication
&lt;/h3&gt;

&lt;p&gt;To test this implementation, you can either publish your app or use the Webflow preview feature. The code creates a seamless authentication experience where users can sign in, maintain their session across page loads, and safely sign out when needed:&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%2F6gh1wjmnfc00wmehlyjy.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%2F6gh1wjmnfc00wmehlyjy.png" alt="Fig: PublishPro CMS dashboard with restricted access" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users can manage their profile through the profile widget that appears when clicking their name:&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%2Fwni2pihs3v7jhqpegzbf.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%2Fwni2pihs3v7jhqpegzbf.png" alt="Fig: PublishPro CMS dashboard showing user profile widget" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing RBAC
&lt;/h2&gt;

&lt;p&gt;With authentication in place, you can now implement RBAC to manage different user permissions within the CMS. The application defines two roles with distinct permissions: Writers can create and edit their own articles, and Editors have full access, including publishing and deleting articles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating roles and permissions
&lt;/h3&gt;

&lt;p&gt;To create the roles and permissions in the Descope Console, navigate initially to the &lt;a href="https://app.descope.com/authorization" rel="noopener noreferrer"&gt;Authorization section&lt;/a&gt;. Under the RBAC tab, locate the Permissions section in the right panel. You're creating four distinct permissions that represent the core actions in the CMS. Start by clicking + Permission to create &lt;code&gt;CreateArticleDrafts&lt;/code&gt;—this fundamental permission enables users to create new article drafts in the system. When setting up this permission, include a clear description explaining its purpose, such as "Allows creation of new article drafts in the CMS."&lt;/p&gt;

&lt;p&gt;Continue adding permissions by creating &lt;code&gt;UpdateArticleDrafts&lt;/code&gt;, which controls the ability to edit existing drafts. Follow this with the &lt;code&gt;DeleteArticleDrafts&lt;/code&gt; to manage article removal privileges and, finally, &lt;code&gt;PublishArticleDrafts&lt;/code&gt; to control who can publish articles publicly. For each permission, take time to write a descriptive explanation that will help you and future administrators understand its intended use.&lt;/p&gt;

&lt;p&gt;Once your permissions are in place, move to the Roles panel to establish the two key roles needed for the CMS. Begin with the Writer role by clicking + Role. Writers need the ability to create and modify content, so assign them the &lt;code&gt;CreateArticleDrafts&lt;/code&gt; and &lt;code&gt;UpdateArticleDrafts&lt;/code&gt; permissions. Include a description, like "Content creators who can write and edit drafts," to clearly define their responsibilities.&lt;/p&gt;

&lt;p&gt;Next, create the Editor role, which requires more comprehensive access. Editors need oversight of the entire content lifecycle, so assign them all four permissions: &lt;code&gt;CreateArticleDrafts&lt;/code&gt;, &lt;code&gt;UpdateArticleDrafts&lt;/code&gt;, &lt;code&gt;DeleteArticleDrafts&lt;/code&gt;, and &lt;code&gt;PublishArticleDrafts&lt;/code&gt;. Add a description, such as "Content managers with full article control," to document their broader responsibilities.&lt;/p&gt;

&lt;p&gt;Here's the Authorization page with RBAC set up:&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%2Fj0444vhxzqjjc3pjn6rr.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%2Fj0444vhxzqjjc3pjn6rr.png" alt="Fig: Descope Console Authorization page with Roles and Permissions tab" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This permission structure mirrors common editorial workflows where writers focus on content creation and editors manage the overall publication process. Writers can develop and refine content, while editors maintain quality control through their additional abilities to remove content and determine what gets published.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assigning roles
&lt;/h3&gt;

&lt;p&gt;Once you have created the roles, head over to the Users section and update the existing users by assigning them roles as you chose:&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%2Fsdm1uuu0skvl0suqph1r.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%2Fsdm1uuu0skvl0suqph1r.png" alt="Fig: Adding a new role in the Descope Console" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's modify the application to show or hide features based on user roles. Update the Footer code script to include the following permission-checking functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasEditorRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasWriterRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;writer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Editor has all permissions&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasEditorRole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Writer has create and update permissions&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasWriterRole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;p&gt;When the &lt;code&gt;checkPermission&lt;/code&gt; function is called, it checks the user's roles using case-insensitive comparison (&lt;code&gt;toLowerCase()&lt;/code&gt;) and then makes permission decisions based on a simple rule: if the user is an editor, they can access everything; but if the user is a writer, they only get access to create and update actions. Any other role or permission combination results in no access being granted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing permissions
&lt;/h3&gt;

&lt;p&gt;Now, you need to tell the application which UI elements should be restricted based on user roles. You do this by adding custom data attributes to the buttons. These attributes act like permission labels, telling the code which buttons should be visible to which users.&lt;/p&gt;

&lt;p&gt;Open the Webflow Editor and navigate to the Settings tab in the right panel. For each button in the interface, you add the appropriate permission attribute based on its function. Add &lt;code&gt;data-permission="update"&lt;/code&gt; to Edit buttons, &lt;code&gt;data-permission="delete"&lt;/code&gt; to Delete buttons, and &lt;code&gt;data-permission="publish"&lt;/code&gt; to Publish buttons. These attributes work with an &lt;code&gt;updateUIBasedOnPermissions&lt;/code&gt; function to dynamically show or hide buttons based on the user's role:&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%2Fuxokt5y7ui56wxbdhfkq.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%2Fuxokt5y7ui56wxbdhfkq.png" alt="Fig: Permission data attribute for RBAC" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the UI
&lt;/h3&gt;

&lt;p&gt;With that in place, you can update the UI based on the user's roles when they authenticate. To do so, let's create the &lt;code&gt;updateUIBasedOnPermissions&lt;/code&gt; function by updating the Footer code script to include the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUIBasedOnPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;publish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permission&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-permission="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldDisplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shouldDisplay&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isEditor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isWriter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;writer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-role="editor"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isEditor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-role="writer"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isWriter&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&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;p&gt;The &lt;code&gt;updateUIBasedOnPermissions&lt;/code&gt; function manages the visibility of UI elements based on a user's role and permissions by checking all elements with specific data attributes (&lt;code&gt;data-permission&lt;/code&gt; and &lt;code&gt;data-role&lt;/code&gt;) and showing or hiding them accordingly.&lt;/p&gt;

&lt;p&gt;To ensure this function is called when a user successfully authenticates, update the &lt;code&gt;handleAuthSuccess&lt;/code&gt; function as shown here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAuthSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;refreshJwt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshJwt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Update UI based on roles and permissions&lt;/span&gt;
  &lt;span class="nf"&gt;updateUIBasedOnPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Show authenticated UI&lt;/span&gt;
  &lt;span class="nx"&gt;authModal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;signInButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mainWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Click to manage profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;p&gt;In addition, update the initial authentication check to include permission-based UI updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Check auth state on load&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userDataString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Update permissions&lt;/span&gt;
  &lt;span class="nf"&gt;updateUIBasedOnPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;signInButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mainWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_w-hide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DSR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_data&lt;/span&gt;&lt;span class="dl"&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;h3&gt;
  
  
  Updating the sidebar
&lt;/h3&gt;

&lt;p&gt;Finally, you need to apply the permission system to the sidebar's Quick Actions buttons to ensure they follow the same role-based rules as the other interface elements. In the Webflow Editor, open the Settings panel for the New Articles button in the sidebar. Just as you did with the other buttons, add the &lt;code&gt;data-permission="create"&lt;/code&gt; attribute to ensure only users with article-creation permissions can see this button.&lt;/p&gt;

&lt;p&gt;The Analytics button requires a different approach since it's a feature specifically for editors. Open its Settings panel and add the &lt;code&gt;data-role="editor"&lt;/code&gt; attribute. Using a role-based attribute here makes sense because analytics access is tied to the editor role itself rather than a specific permission.&lt;/p&gt;

&lt;p&gt;These attribute additions connect the Quick Actions buttons to the permission-checking system built, ensuring consistent access control throughout the interface. When a user logs in, these buttons automatically show or hide based on their assigned role, just like the other UI elements:&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%2Fufalkdwre0rebzz2aec1.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%2Fufalkdwre0rebzz2aec1.png" alt="Fig: Dashboard with RBAC" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next step would be to implement these same permission checks on your backend API using one of the &lt;a href="https://docs.descope.com/backend-sdk" rel="noopener noreferrer"&gt;Descope SDKs&lt;/a&gt;, but that's beyond the scope of this tutorial since the focus is on the frontend implementation with Webflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you transformed a basic CMS into a secure, role-aware application using the Descope authentication and authorization capabilities. What makes this implementation particularly powerful is how robust security features were achieved without complex backend code, making it perfect for Webflow applications. Through the Descope visual flow editor, you created a customized authentication flow that offers both email OTP and social login options, providing users with flexible, secure authentication methods.&lt;/p&gt;

&lt;p&gt;The integration of the Descope web component and user profile widget, combined with RBAC, demonstrates how modern authentication can be implemented in Webflow applications without sacrificing security or user experience. This tutorial transforms a vulnerable Webflow app into a more secure CMS where content creators and editors can collaborate safely within their designated roles.&lt;/p&gt;

&lt;p&gt;To stay up to date with more developer tutorials, subscribe to the Descope blog or follow us on &lt;a href="https://www.linkedin.com/company/descope/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Modernize Auth Without Changing Your Firebase Sessions</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 13 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/modernize-auth-without-changing-your-firebase-sessions-3h4l</link>
      <guid>https://dev.to/descope/modernize-auth-without-changing-your-firebase-sessions-3h4l</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/external-tokens-firebase" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your app is built on Firebase, authentication tends to sit right at the center of everything. Firestore rules, Cloud Functions, Storage access, and even analytics all assume there's a valid Firebase user session.&lt;/p&gt;

&lt;p&gt;At the same time, Firebase Auth can feel limiting once you want to go beyond basic email/password or simple social login. Adding things like &lt;a href="https://descope.com/use-cases/passkeys" rel="noopener noreferrer"&gt;passkeys&lt;/a&gt;, flexible &lt;a href="https://www.descope.com/use-cases/mfa" rel="noopener noreferrer"&gt;MFA&lt;/a&gt;, or more nuanced sign-in and recovery flows usually means a lot of custom work — or compromises in UX.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/blog/post/external-tokens" rel="noopener noreferrer"&gt;Descope External Tokens&lt;/a&gt; is designed for exactly this situation. It lets you run &lt;a href="https://docs.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt; natively in your app, then hand off to Firebase at the very end so you still end up with a real Firebase session.&lt;/p&gt;

&lt;p&gt;In other words: Descope handles authentication while Firebase continues to power everything else in your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  External Tokens 101
&lt;/h2&gt;

&lt;p&gt;By using Descope External Tokens with Firebase, Descope is the place where authentication happens, and Firebase is the place where sessions are consumed.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&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%2Fht0ve5u81iqnnkeguzxx.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%2Fht0ve5u81iqnnkeguzxx.png" alt="External Tokens Flow Firebase Descope" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user signs in using a Descope Flow (passwordless, social, passkeys, MFA — whatever you've defined).&lt;/li&gt;
&lt;li&gt;When the Flow finishes successfully, Descope returns an &lt;code&gt;externalToken&lt;/code&gt;. That token is a Firebase custom auth token, signed by Descope using your Firebase admin credentials.&lt;/li&gt;
&lt;li&gt;Your app calls &lt;a href="https://firebase.google.com/docs/auth/admin/create-custom-tokens" rel="noopener noreferrer"&gt;&lt;code&gt;signInWithCustomToken&lt;/code&gt;&lt;/a&gt; with Firebase.&lt;/li&gt;
&lt;li&gt;Firebase creates a normal session, just as if the user had signed in directly with Firebase Auth.&lt;/li&gt;
&lt;li&gt;Once that's done, your app is "Firebase authenticated" in the usual way.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why use External Tokens
&lt;/h3&gt;

&lt;p&gt;This pattern is useful if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to use Descope's auth flows and UI&lt;/li&gt;
&lt;li&gt;You don't want redirects or awkward webviews&lt;/li&gt;
&lt;li&gt;You already rely heavily on Firebase services&lt;/li&gt;
&lt;li&gt;You don't want to rebuild your backend or security rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're not replacing Firebase, you're simply plugging Descope into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the Firebase External Token Connector
&lt;/h2&gt;

&lt;p&gt;To generate Firebase custom tokens, Descope needs access to your Firebase project's service account. This is done through a &lt;a href="https://docs.descope.com/connectors/connector-configuration-guides/token/firebase" rel="noopener noreferrer"&gt;Firebase External Token connector&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This integration allows you to continue using Firebase's services while replacing Firebase Auth with Descope Flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the connector in Descope
&lt;/h3&gt;

&lt;p&gt;In the Descope Console, navigate to the Connectors page and create a new connector using the Firebase template. You'll be asked to provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connector Name:&lt;/strong&gt; A unique name to help distinguish this connector (especially useful if you have multiple Firebase projects or environments).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connector Description:&lt;/strong&gt; A short description explaining what the connector is used for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Account private key:&lt;/strong&gt; The JSON credentials from your Firebase project. This allows Descope to securely sign Firebase custom tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting the Firebase service account JSON
&lt;/h3&gt;

&lt;p&gt;To generate a service account key in Firebase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Firebase Console and select your project&lt;/li&gt;
&lt;li&gt;Click the gear icon and go to Project settings&lt;/li&gt;
&lt;li&gt;Open the Service Accounts tab&lt;/li&gt;
&lt;li&gt;Click Generate new private key&lt;/li&gt;
&lt;li&gt;Download the JSON file&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%2F7xbiut48qlnaqni5pdl8.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%2F7xbiut48qlnaqni5pdl8.png" alt="Generating new private key in Firebase" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the file and copy the entire contents, including the surrounding braces.&lt;/p&gt;

&lt;p&gt;Back in the Descope Console, paste this JSON into the Service Account field of the Firebase connector configuration. After saving, use the Test button to confirm the credentials were added correctly.&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%2Fea1tus1yx69fuiy3covd.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%2Fea1tus1yx69fuiy3covd.png" alt="Configure Firebase Connector in Descope Console" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, the connector itself is fully configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling the connector for External Tokens
&lt;/h3&gt;

&lt;p&gt;Once the connector exists, it needs to be enabled for use in authentication flows.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Project Settings in the Descope Console&lt;/li&gt;
&lt;li&gt;Open the External Token section and enable External Tokens&lt;/li&gt;
&lt;li&gt;Select the Firebase connector you just 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%2F2mocemznwy65ke0x1la6.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%2F2mocemznwy65ke0x1la6.png" alt="Enabling the Descope Firebase Connector" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this point on, Descope will automatically generate a Firebase custom token at the end of a successful flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the authentication response looks like
&lt;/h3&gt;

&lt;p&gt;After a user successfully completes a Descope Flow, the authentication response includes an &lt;code&gt;externalToken&lt;/code&gt; field. This value is the Firebase custom token your app should exchange immediately.&lt;/p&gt;

&lt;p&gt;Here's what an example response looks like:&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;"cookieDomain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"cookieExpiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cookieMaxAge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cookiePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"externalToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FIREBASE_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"firstSeen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idpResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"refreshJwt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DESCOPE_REFRESH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionExpiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1750879215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionJwt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DESCOPE_SESSION_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A React web example
&lt;/h2&gt;

&lt;p&gt;On the web, the integration is straightforward: run a Descope Flow, grab the external token, and pass it to Firebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initializeApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firebaseConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_PROJECT.firebaseapp.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_PROJECT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_APP_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running a Flow and creating a Firebase session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Descope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@descope/react-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signInWithCustomToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./firebase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;externalToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;externalToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;externalToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing Firebase external token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signInWithCustomToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;externalToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Firebase user:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_DESCOPE_PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Descope&lt;/span&gt;
        &lt;span class="na"&gt;flowId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_FLOW_ID"&lt;/span&gt;
        &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;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;p&gt;At this point, Firestore rules, Storage access, and Functions all work exactly as they did before.&lt;/p&gt;

&lt;h2&gt;
  
  
  A React Native example
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Descope React Native SDK does not currently support Expo. If you're using Expo, follow the &lt;a href="https://www.descope.com/blog/post/expo-authentication" rel="noopener noreferrer"&gt;Expo OIDC integration&lt;/a&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Install the SDK
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @descope/react-native-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For iOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Wrap your app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuthProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@descope/react-native-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AppRoot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_DESCOPE_PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;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;h3&gt;
  
  
  Running a Flow and creating a Firebase session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlowView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@descope/react-native-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@react-native-firebase/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LoginScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;manageSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlowView&lt;/span&gt;
      &lt;span class="na"&gt;flowOptions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://auth.descope.io/login/YOUR_PROJECT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;androidOAuthNativeProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;iosOAuthNativeProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwtResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Save the Descope session&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;manageSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwtResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Exchange the external token for a Firebase session&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;externalToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwtResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;signInWithCustomToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;externalToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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;p&gt;After this runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Descope manages the auth session lifecycle&lt;/li&gt;
&lt;li&gt;Firebase manages the infrastructure session&lt;/li&gt;
&lt;li&gt;Your app can safely use both&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Descope Auth, Firebase sessions
&lt;/h2&gt;

&lt;p&gt;Descope External Tokens give you a practical way to modernize authentication without undoing the work you've already invested in Firebase. You can build richer, more flexible sign-in experiences using Descope Flows, while continuing to rely on Firebase for sessions, access control, and the rest of your backend.&lt;/p&gt;

&lt;p&gt;The integration point is intentionally simple: authenticate with Descope, exchange the returned token with Firebase, and move on. There's no custom token service to maintain, no changes to your Firebase rules, and no need to rethink how your app is structured.&lt;/p&gt;

&lt;p&gt;It's also worth noting that External Tokens aren't the only way to connect Descope and Firebase. If you prefer not to use &lt;code&gt;signInWithCustomToken&lt;/code&gt; at all, Descope can also be configured as a &lt;a href="https://docs.descope.com/identity-federation/applications/setup-guides/firebase-oidc" rel="noopener noreferrer"&gt;federated OIDC identity provider&lt;/a&gt; for Firebase. In that setup, Firebase handles the session directly after an OIDC login — but the tradeoff is that the authentication flow includes an OIDC redirect.&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%2Frkgfxjwlmba68tn1cntw.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%2Frkgfxjwlmba68tn1cntw.png" alt="Firebase OIDC flow diagram" width="720" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both approaches are valid. External Tokens are usually the better fit when you want a fully native, no-redirect experience, especially on mobile. OIDC federation can be a good option if redirects are acceptable and you want Firebase to remain the system that completes the sign-in.&lt;/p&gt;

&lt;p&gt;Either way, Descope lets you evolve authentication independently of the rest of your Firebase stack: and that flexibility is often the biggest win.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up for Descope&lt;/a&gt; to begin your drag &amp;amp; drop auth journey. Have questions about our platform? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book a demo&lt;/a&gt; with our auth experts.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>google</category>
      <category>security</category>
    </item>
    <item>
      <title>Adding Authentication and Remote Support to a Local MCP Server</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 10 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/adding-authentication-and-remote-support-to-a-local-mcp-server-32</link>
      <guid>https://dev.to/descope/adding-authentication-and-remote-support-to-a-local-mcp-server-32</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-remote-mcp" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/learn/post/mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; servers allow developers to connect large language models (LLMs) to external tools and resources through a standardized protocol. Some MCP servers are designed to run locally, often because they need to access resources that are in the same environment as the AI agent. Others are designed to run remotely, such as when exposing shared APIs, cloud-hosted tools, or team-wide services. Remote access is powerful, but without proper controls, it can easily become a security risk.&lt;/p&gt;

&lt;p&gt;An unsecured MCP server exposed to the internet can be discovered by automated scanners, abused by unauthorized users, or even used to extract sensitive data from connected resources. Nevertheless, with proper authentication and authorization in place, remote MCP servers can unlock new collaboration models. Teams can share cloud-hosted tools and services, manage who can access which resources, and even integrate audit logs for compliance and observability. This makes MCP servers safer and more practical in enterprise or team-based environments.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how to take a local &lt;a href="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;Playwright MCP server&lt;/a&gt; and make it remote-ready with authentication powered by &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;. You'll expose your server for secure remote access, add user authentication using &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt;, and implement &lt;a href="https://docs.descope.com/authorization/role-based-access-control" rel="noopener noreferrer"&gt;role-based access control (RBAC)&lt;/a&gt; to ensure only authorized users can access sensitive tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To complete this tutorial, you need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/download" rel="noopener noreferrer"&gt;Node.js v18+&lt;/a&gt; installed on your local machine.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/kimanikevin254/descope-playwright-mcp-auth/archive/refs/heads/assets.zip" rel="noopener noreferrer"&gt;assets&lt;/a&gt; ZIP folder downloaded and extracted. The folder includes a preprepared Descope flow that you will use later in this guide.&lt;/li&gt;
&lt;li&gt;A code editor and a web browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preparing the local MCP server
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/microsoft/playwright" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; MCP server exposes a set of tools that let LLMs interact with Playwright to perform actions such as running browser tests, inspecting pages, taking screenshots, and more. For example, you can use it to build an AI agent that automates quality assurance by navigating to a web application, filling out forms, and verifying that the UI behaves correctly. These tasks otherwise require manual testing or complex custom scripts.&lt;/p&gt;

&lt;p&gt;When running locally, the Playwright MCP server is accessible only from your own machine. This is ideal for initial development and testing as there's no risk of unauthorized access. However, if you want to expose this server remotely, for example, to integrate it into a cloud-based workflow or share it with your team, you need to make it accessible over the internet. This is where you need to start thinking about security; anyone who invokes the server's URL can invoke its tools and potentially run arbitrary browser automation on your infrastructure.&lt;/p&gt;

&lt;p&gt;Let's go ahead and set up the Playwright MCP server locally. Start by creating a new project folder and initializing a Node.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;descope-playwright-mcp-auth
&lt;span class="nb"&gt;cd &lt;/span&gt;descope-playwright-mcp-auth
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the Playwright MCP server, which is offered as an npm package by Microsoft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @playwright/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the package.json file and replace the scripts section with the following content that provides a command to run the MCP server as a standalone server and adds the port flag to enable HTTP transport:&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;"scripts"&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;"start:mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-server-playwright --port 3001"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now run the Playwright MCP server by executing the following command in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run start:mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;descope-playwright-mcp-auth@1.0.0 start:mcp
&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mcp-server-playwright &lt;span class="nt"&gt;--port&lt;/span&gt; 3001
&lt;span class="go"&gt;
Listening on http://localhost:3001
Put this in your client config:
{
  "mcpServers": {
    "playwright": {
      "url": "http://localhost:3001/mcp"
    }
  }
}
For legacy SSE transport support, you can use the /sse endpoint instead.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test that the Playwright MCP server is running as expected, you can use &lt;a href="https://modelcontextprotocol.io/docs/tools/inspector" rel="noopener noreferrer"&gt;MCP Inspector&lt;/a&gt;. The MCP Inspector is a browser-based debugging tool that lets you interact with MCP servers without writing any client code. It's perfect for quickly verifying that your server is exposing the right tools and responding correctly before integrating it with an actual AI agent. On a separate terminal, execute the following command to start the MCP Inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector@0.17.2 &lt;span class="nt"&gt;--transport&lt;/span&gt; http &lt;span class="nt"&gt;--server-url&lt;/span&gt; http://localhost:3001/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--transport&lt;/code&gt; flag specifies how the client communicates with the MCP server. MCP supports &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports" rel="noopener noreferrer"&gt;three transport types&lt;/a&gt;: &lt;code&gt;http&lt;/code&gt; (streamable HTTP), &lt;code&gt;sse&lt;/code&gt; (server-sent events (SSE) for streaming), and &lt;code&gt;stdio&lt;/code&gt; (standard input/output for local processes). Since the Playwright MCP server is running as an HTTP server on port 3001, you're using the &lt;code&gt;http&lt;/code&gt; transport to connect to it.&lt;/p&gt;

&lt;p&gt;This opens a browser tab where you can see the MCP Inspector interface. On the interface, select the Connect button to connect to your MCP server:&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%2Fjcsatpdz8voxjounavrr.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%2Fjcsatpdz8voxjounavrr.png" alt="Fig: MCP Inspector interface" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you're connected, you can perform actions such as listing the tools or invoking them:&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%2Fed5smy5idgdi1v65iqqx.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%2Fed5smy5idgdi1v65iqqx.png" alt="Fig: Connected to MCP server" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the MCP server remote-capable
&lt;/h2&gt;

&lt;p&gt;So far, the Playwright MCP server is listening on &lt;code&gt;localhost:3001&lt;/code&gt;, which means it's accessible only from your own machine. No external connections can reach it because it's bound to localhost (127.0.0.1). This isolation makes it safe for local testing, but it also means teammates can't connect or share the same tools. To enable collaboration, you need to expose it over the network, but doing so requires proper authentication and authorization to prevent unauthorized access.&lt;/p&gt;

&lt;p&gt;There are several approaches you can use to make the MCP server remotely accessible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binding to all interfaces (&lt;code&gt;--host 0.0.0.0&lt;/code&gt;) exposes your server directly to your network but offers no authentication.&lt;/li&gt;
&lt;li&gt;Using an authentication proxy offers programmatic control over authentication and authorization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this guide, you will use the authentication proxy approach with &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express&lt;/a&gt;. This gives you full control over authentication logic and RBAC. It also integrates well with the &lt;a href="https://docs.descope.com/mcp/mcp-express-sdk" rel="noopener noreferrer"&gt;Descope MCP Express SDK&lt;/a&gt;, which is designed to allow you to easily add &lt;a href="https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/" rel="noopener noreferrer"&gt;MCP specification-compliant authorization&lt;/a&gt; to your MCP server. The authentication proxy sits between clients and the MCP server, and validates every request before forwarding it.&lt;/p&gt;

&lt;p&gt;Start by installing the required dependencies for the authentication proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i express cors http-proxy @descope/mcp-express dotenv
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; typescript @types/node @types/express @types/cors @types/http-proxy ts-node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a new file named &lt;code&gt;.env&lt;/code&gt; to store your configuration and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;AUTH_PROXY_PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3000&lt;/span&gt;
&lt;span class="py"&gt;MCP_SERVER_PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines the ports assigned to the Playwright MCP server and the authentication proxy.&lt;/p&gt;

&lt;p&gt;Define your TypeScript configuration by creating a new file named &lt;code&gt;tsconfig.json&lt;/code&gt; and add the following:&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;"compilerOptions"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commonjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&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;"esModuleInterop"&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;"skipLibCheck"&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="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;To implement the proxy logic, create a new file named &lt;code&gt;src/auth-proxy.ts&lt;/code&gt; and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;httpProxy&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http-proxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AUTH_PROXY_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTH_PROXY_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MCP_SERVER_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_SERVER_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create proxy instance targeting the localhost-bound MCP server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpProxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createProxyServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MCP_SERVER_PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changeOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Handle proxy errors&lt;/span&gt;
&lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proxy error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proxy error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Ignore if already closed&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Proxy all /mcp requests&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AUTH_PROXY_PORT&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;AUTH_PROXY_PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGINT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Shutting down server...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;p&gt;This code sets up a simple Express proxy server that listens on port 3000 and forwards all &lt;code&gt;/mcp&lt;/code&gt; requests to the local Playwright MCP server running on port 3001. It uses &lt;code&gt;http-proxy&lt;/code&gt; for request forwarding, applies &lt;code&gt;cors&lt;/code&gt; to allow cross-origin access, and includes basic error handling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Please note that this setup supports HTTP transport only. Clients attempting to use SSE via the &lt;code&gt;/sse&lt;/code&gt; endpoint or WebSocket connections do not function with this configuration. Supporting SSE requires additional setup to handle streaming responses, and WebSocket support requires the &lt;code&gt;ws&lt;/code&gt; proxy option enabled. For most remote MCP deployments, HTTP transport is sufficient and easier to secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To make sure that the proxy is working as expected, run the MCP server using the command &lt;code&gt;npm run start:mcp&lt;/code&gt;. In a separate terminal, run the proxy server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodemon &lt;span class="nt"&gt;--exec&lt;/span&gt; &lt;span class="s1"&gt;'ts-node'&lt;/span&gt; src/auth-proxy.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;--exec ts-node&lt;/code&gt; flag explicitly tells nodemon to use &lt;code&gt;ts-node&lt;/code&gt; to execute the TypeScript file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run the MCP Inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector@0.17.2 &lt;span class="nt"&gt;--transport&lt;/span&gt; http &lt;span class="nt"&gt;--server-url&lt;/span&gt; http://localhost:3000/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Note that the &lt;code&gt;--server-url&lt;/code&gt; flag is now pointing to the proxy server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the MCP Inspector interface launches, you should be able to connect to the MCP server, list tools, and invoke them.&lt;/p&gt;

&lt;p&gt;At this stage, your MCP server is accessible to anyone who can reach the exposed host and port. There's no authentication, no encryption (TLS), and no request validation. This means that anyone can potentially connect to your MCP server and invoke any tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing authentication with Descope
&lt;/h2&gt;

&lt;p&gt;As you saw in the previous section, anyone who discovers your proxy server's URL can connect to your MCP server and invoke any available tool. In production or shared development environments, this opens doors to unauthorized access and abuse. For a Playwright MCP server, this can mean unauthorized users running browser automation on your infrastructure, executing scripts that capture sensitive data from your environment, or triggering resource-intensive operations that consume server resources. Without authentication, you have no control over who has access to your tools and no way to audit usage or attribute usage to specific users.&lt;/p&gt;

&lt;p&gt;Descope simplifies securing your MCP server with its visual, low-code flow editor. Instead of writing authentication logic from scratch, you can build complete authentication flows through a drag-and-drop interface. These flows define the entire authentication journey, from the user sign-in method, multifactor authentication (MFA), consent screens for third-party client access, and the process when authentication fails or succeeds. This approach lets you implement enterprise-grade security in minutes rather than days, while maintaining full control over the authentication experience.&lt;/p&gt;

&lt;p&gt;The primary step to implementing authentication with Descope is to create a project. On your &lt;a href="https://app.descope.com/home" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt;, click the project drop-down on the top navigation pane, and select + Project:&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%2Fy86ioaweqgxw40vunnkm.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%2Fy86ioaweqgxw40vunnkm.png" alt="Fig: Creating a new project" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Create project form, provide "descope-playwright-mcp-auth" as the name of the project and select Create:&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%2Fopowvqjfyumcow9q7zpk.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%2Fopowvqjfyumcow9q7zpk.png" alt="Fig: Providing project details" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, navigate to the Flows page from the sidebar and click the Import flow 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%2Ffisip1bgzbshe3m7onhd.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%2Ffisip1bgzbshe3m7onhd.png" alt="Fig: Importing a flow" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upload the &lt;code&gt;mcp-auth-consent.json&lt;/code&gt; file from the assets you downloaded earlier. This opens the flow in the Flow editor:&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%2Fkndoyppm0dqoxf2uxia4.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%2Fkndoyppm0dqoxf2uxia4.png" alt="Fig: Flow editor" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This flow has been created using the Descope flow editor by dragging and dropping authentication components, such as the &lt;a href="https://docs.descope.com/flows/conditions/isloggedin" rel="noopener noreferrer"&gt;user.loggedIn condition&lt;/a&gt;, &lt;a href="https://docs.descope.com/flows/screens" rel="noopener noreferrer"&gt;authentication screens&lt;/a&gt;, &lt;a href="https://docs.descope.com/flows/actions" rel="noopener noreferrer"&gt;verification actions&lt;/a&gt;, and consent conditions, onto the canvas and connecting them to define authentication logic. You can build similar flows either from scratch or from a template and use the available components. This tutorial uses a preprepared flow that includes the auth logic and consent flow to save time, but feel free to explore the editor and customize it later.&lt;/p&gt;

&lt;p&gt;The flow starts by checking if the user is logged in. If they're not, they are redirected to a screen where they can sign in using either an emailed one-time password (OTP) or Google single sign-on (SSO). Once logged in, the flow checks if the third-party application (the MCP client) has been authorized. If not, they're redirected to a screen where the scopes being requested by the app are displayed, and the user can choose whether to authorize the app or not.&lt;/p&gt;

&lt;p&gt;With the auth and consent flow in place, you need to set up &lt;a href="https://www.descope.com/learn/post/dynamic-client-registration" rel="noopener noreferrer"&gt;Dynamic Client Registration (DCR)&lt;/a&gt;. This protocol allows third-party client applications to register themselves automatically with your Descope project, instead of being manually preconfigured. When an MCP client, like the MCP connector, connects to your MCP server, it automatically initiates the DCR process by sending a registration request to the proxy's &lt;code&gt;/oauth/register&lt;/code&gt; endpoint (provided by the Descope MCP Express SDK). The proxy handles the client registration, creates OAuth credentials for the client, and returns them so the client can proceed with authentication. All this happens behind the scenes. The MCP Inspector takes care of the DCR flow automatically once you point it to your proxy's URL, which is why you don't see explicit DCR code in the client implementation.&lt;/p&gt;

&lt;p&gt;To set up DCR, navigate to the Inbound Apps page on your Descope console and select DCR Settings:&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%2Fmccy2l5ny2qk6wvrfalm.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%2Fmccy2l5ny2qk6wvrfalm.png" alt="Fig: Inbound Apps page" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the DCR Settings page, toggle the switch to enable DCR; and in the Approved scopes list section, provide the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: "openid"&lt;/li&gt;
&lt;li&gt;Description: "Allows this app to confirm your identity and access basic profile information, such as your assigned roles."&lt;/li&gt;
&lt;li&gt;Mandatory: Toggle to make sure this scope is mandatory&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%2F7q40k0fxpo8l6atukkja.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%2F7q40k0fxpo8l6atukkja.png" alt="Fig: Approved scopes" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, scroll down to the User consent flow section, and in the Consent flow drop-down, select the flow you imported in the previous steps and save the settings:&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%2Fpzq753wl8j0jn5rd0zj3.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%2Fpzq753wl8j0jn5rd0zj3.png" alt="Fig: User consent flow" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that the DCR is configured, you need to obtain credentials that your proxy server uses to communicate with the Descope APIs. The proxy needs these credentials to validate tokens, manage client registrations, and enforce authentication policies.&lt;/p&gt;

&lt;p&gt;Start by obtaining the project ID, which identifies your Descope project and tells the proxy which project's authentication rules to enforce. You can get this by navigating to the &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;Project page&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%2Fxyidyhx3f8fe4mr49kxr.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%2Fxyidyhx3f8fe4mr49kxr.png" alt="Fig: Project page" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You also need a management key to give your proxy server permission to perform administrative operations, like managing direct client registrations. Navigate to the &lt;a href="https://app.descope.com/settings/company/managementkeys" rel="noopener noreferrer"&gt;Management Keys page&lt;/a&gt;, select + Management Key, and provide the key details by specifying the name and the 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%2Fb7i8hph05hkzayedmxfn.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%2Fb7i8hph05hkzayedmxfn.png" alt="Fig: Generating a management key" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Descope into the Playwright MCP server
&lt;/h2&gt;

&lt;p&gt;At this point, you have completed setting up Descope. You enabled DCR so MCP clients can register automatically, configured the authentication and consent flow that users go through, and obtained credentials your proxy server needs to enforce authentication. The next step is to use these credentials to build the authentication proxy.&lt;/p&gt;

&lt;p&gt;With your Descope project set up, you can now integrate authentication into the proxy. The goal is to make sure that every request is verified before it's proxied to the MCP server.&lt;/p&gt;

&lt;p&gt;Before diving into the implementation, it's important to understand how the authentication flow works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The MCP client, in this case, the MCP Inspector, registers with your proxy via the DCR.&lt;/li&gt;
&lt;li&gt;You're redirected to the authentication/consent flow you set up earlier in Descope.&lt;/li&gt;
&lt;li&gt;You log in and consent to the request scopes.&lt;/li&gt;
&lt;li&gt;Descope issues an access token to the MCP client.&lt;/li&gt;
&lt;li&gt;The MCP client includes this token in its requests to the proxy server.&lt;/li&gt;
&lt;li&gt;The proxy server validates the token before forwarding the request to the MCP server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To handle auth in the proxy, use the Descope MCP Express SDK you installed earlier. Start by adding the following to your &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;YOUR-DESCOPE-PROJECT-ID&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;DESCOPE_MANAGEMENT_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;YOUR-DESCOPE-MANAGEMENT-KEY&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;SERVER_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SERVER_URL&lt;/code&gt; ENV variable refers to the URL that exposes your MCP server; in this case, it's the proxy URL. Make sure to replace the placeholders for &lt;code&gt;DESCOPE_PROJECT_ID&lt;/code&gt; and &lt;code&gt;DESCOPE_MANAGEMENT_KEY&lt;/code&gt; with the values you obtained from your Descope console.&lt;/p&gt;

&lt;p&gt;Next, open the &lt;code&gt;src/auth-proxy.ts&lt;/code&gt; file and add the following imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;descopeMcpAuthRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;descopeMcpBearerAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DescopeMcpProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@descope/mcp-express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just below the &lt;code&gt;MCP_SERVER_PORT&lt;/code&gt; definition, add the Descope MCP provider configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;descopeProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DescopeMcpProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;managementKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESCOPE_MANAGEMENT_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;serverUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SERVER_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dynamicClientRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;authPageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://api.descope.com/login/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?flow=mcp-auth-consent`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Add OAuth metadata and DCR endpoints&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;descopeMcpAuthRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;descopeProvider&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Protect your MCP endpoints with bearer authentication&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;descopeMcpBearerAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;descopeProvider&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code initializes &lt;code&gt;DescopeMcpProvider&lt;/code&gt; with your Descope project credentials and the proxy server URL. It also configures the DCR by specifying the URL to the auth and consent flow you set up earlier in the Descope console. The &lt;code&gt;descopeMcpAuthRouter()&lt;/code&gt; function adds the required OAuth metadata endpoints and the DCR &lt;code&gt;/register&lt;/code&gt; endpoint. The &lt;code&gt;descopeMcpBearerAuth()&lt;/code&gt; function protects the specified endpoints by checking the incoming request for a bearer auth token and attaches the user information to &lt;code&gt;req.auth&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding authorization to the Playwright MCP server
&lt;/h2&gt;

&lt;p&gt;At this point, your proxy requires authentication but doesn't enforce RBAC, which restricts access to tools based on user roles. Authorization determines what authenticated users are allowed to do. For example, you may want to prevent regular users from installing browser binaries (which can consume significant resources or introduce security risks) while allowing admins full access to all tools.&lt;/p&gt;

&lt;p&gt;For this guide, you'll use the already defined Tenant Admin role in Descope to represent a user with full access to all Playwright MCP tools. Only a user with this role can invoke the &lt;code&gt;browser_install&lt;/code&gt; tool. In a production environment, you define custom roles and permissions that map to specific MCP tools or categories of tools. You maintain a configuration file or database that maps tool names to required roles and then check user roles against this mapping before allowing tool invocation. This tutorial uses a simple hard-coded check with a built-in Tenant Admin role to demonstrate the authorization pattern.&lt;/p&gt;

&lt;p&gt;Start by adding the following import statement to the &lt;code&gt;src/auth-proxy.ts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Readable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add the following code before the line where you proxy &lt;code&gt;/mcp&lt;/code&gt; requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only check authorization for POST requests with a body&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcpReq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the MCP method is a tools call&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Accessing tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// If the tool is an admin tool, verify user has admin role&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browser_install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;descopeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;descopeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateRoles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;authInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tenant Admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`Unauthorized access attempt to admin tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Access denied: Tool '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' requires Tenant Admin role.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Convert object back to string for the proxy&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;p&gt;This middleware intercepts all requests to the &lt;code&gt;/mcp&lt;/code&gt; endpoint and uses &lt;code&gt;express.json()&lt;/code&gt; to parse incoming JSON bodies so they can be inspected. The middleware specifically looks for tool invocation requests (identified by &lt;code&gt;tools/call&lt;/code&gt; method) and checks the tool name against your authorization rules. For the restricted &lt;code&gt;browser_install&lt;/code&gt; tool, it validates the user's JWT using the Descope SDK and ensures that they have the Tenant Admin role. Unauthorized requests are blocked with a 403 error, while valid requests continue through to the MCP server. The middleware reconstructs the request body and updates its headers so it can be properly proxied to the MCP server.&lt;/p&gt;

&lt;p&gt;Finally, update the proxy handler to properly forward the reconstructed body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&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;p&gt;This code creates a readable stream from the reconstructed request body because the proxy library expects streaming data, not a plain string. After the middleware parses and potentially modifies the JSON body, you need to convert it back into a stream format that the proxy can forward to the upstream MCP server.&lt;/p&gt;

&lt;p&gt;Your proxy now enforces both authentication and RBAC before forwarding requests to the MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the integrations
&lt;/h2&gt;

&lt;p&gt;You've set up authentication and authorization, so let's test that authenticated users can connect to your MCP server, unauthorized users are blocked, and RBAC properly restricts admin-only tools.&lt;/p&gt;

&lt;p&gt;Start by running your local MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run start:mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a separate terminal, run the auth proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodemon &lt;span class="nt"&gt;--exec&lt;/span&gt; &lt;span class="s1"&gt;'ts-node'&lt;/span&gt; src/auth-proxy.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal, run the MCP Inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector@0.17.2 &lt;span class="nt"&gt;--transport&lt;/span&gt; http &lt;span class="nt"&gt;--server-url&lt;/span&gt; http://localhost:3000/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you have another machine running on the network, you can run the MCP Inspector on it with the command &lt;code&gt;npx @modelcontextprotocol/inspector@0.17.2 --transport http --server-url http://&amp;lt;AUTH-PROXY-MACHINE-IP&amp;gt;:3000/mcp&lt;/code&gt;, where &lt;code&gt;&amp;lt;AUTH-PROXY-MACHINE-IP&amp;gt;&lt;/code&gt; is the IP address of the machine that is running both the MCP server and the auth proxy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the MCP Inspector UI launches, select Connect to connect to your MCP server. You are then redirected to Descope 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%2Fkvnedswz5arkpx2um2ao.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%2Fkvnedswz5arkpx2um2ao.png" alt="Fig: Descope sign-in" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consent to the requested scopes:&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%2Fnyjoqi7iiwf1m70r8bpt.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%2Fnyjoqi7iiwf1m70r8bpt.png" alt="Fig: Consent to requested scopes" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you authorize the MCP client, you'll be connected to the MCP server:&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%2Fw455xml4o7a2nh9vtpaz.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%2Fw455xml4o7a2nh9vtpaz.png" alt="Fig: Connected to MCP server" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, if you try to invoke the &lt;code&gt;browser_install&lt;/code&gt; tool, you will get an error that confirms RBAC is working as expected:&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%2Fcgiv7mpa6thqeydqgd31.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%2Fcgiv7mpa6thqeydqgd31.png" alt="Fig: RBAC error" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You now need to assign the user the Tenant Admin role to confirm that they can invoke the &lt;code&gt;browser_install&lt;/code&gt; command if they have the appropriate roles. To do this, navigate to the &lt;a href="https://app.descope.com/users" rel="noopener noreferrer"&gt;Descope Users page&lt;/a&gt; and edit your user to assign them the Tenant Admin role:&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%2Ffxstvxxkvm7qk10jvfn6.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%2Ffxstvxxkvm7qk10jvfn6.png" alt="Fig: Assigning the Tenant Admin role to the user" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go back to the MCP Inspector UI, disconnect from the MCP server, and connect again. You are then prompted to sign in and consent to the requested scopes. Once you've done this, you can successfully invoke the &lt;code&gt;browser_install&lt;/code&gt; command:&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%2F9q0efq3u249cuijm80bq.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%2F9q0efq3u249cuijm80bq.png" alt="Fig: Invoking admin tool calls successfully" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can access the full code on &lt;a href="https://github.com/kimanikevin254/descope-playwright-mcp-auth" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit logging for compliance
&lt;/h2&gt;

&lt;p&gt;For production environments, logging user activity is important for security monitoring and regulatory compliance. The current setup logs basic activity (such as tool access and authorization results) to the console. This is useful for local testing but not sufficient for production.&lt;/p&gt;

&lt;p&gt;In a production environment, you should enhance this by doing the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application-level logging:&lt;/strong&gt; Capture detailed information about each request, including the timestamps, user IDs, roles, tools accessed, params used, and status of whether the request was granted. Send these logs to a centralized logging service, such as &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html" rel="noopener noreferrer"&gt;Amazon CloudWatch&lt;/a&gt; or &lt;a href="https://www.datadoghq.com/" rel="noopener noreferrer"&gt;Datadog&lt;/a&gt;, for long-term retention and analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descope audit logs:&lt;/strong&gt; Descope automatically tracks all authentication events, login attempts, token generation, and role assignments. You can view these on the &lt;a href="https://app.descope.com/audits" rel="noopener noreferrer"&gt;Audit page&lt;/a&gt; and export them for compliance reporting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance considerations:&lt;/strong&gt; Different regulations (&lt;a href="https://en.wikipedia.org/wiki/System_and_Organization_Controls" rel="noopener noreferrer"&gt;System and Organization Controls (SOC) 2 type 2&lt;/a&gt;, &lt;a href="https://www.hhs.gov/hipaa/index.html" rel="noopener noreferrer"&gt;Health Insurance Portability and Accountability Act (HIPAA)&lt;/a&gt;, &lt;a href="https://gdpr-info.eu/" rel="noopener noreferrer"&gt;General Data Protection Regulation (GDPR)&lt;/a&gt;) have specific requirements for log retention periods, tamper-proof storage, and access controls. Ensure your logging strategy aligns with your organization's compliance needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With proper audit logging, you gain full visibility into who is accessing your MCP server and what actions they're performing. As a result, you can quickly detect suspicious activity or unauthorized access attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this guide, you transformed a basic, local-only Playwright MCP server into a secure, remote-capable service. You learned how to upgrade from the default stdio transport to a remote-capable transport, expose it to remote connections through a proxy, protect it behind an authentication layer, and enforce fine-grained authorization using Descope.&lt;/p&gt;

&lt;p&gt;Leaving an MCP server exposed without protection is a serious security risk. By adding authentication and authorization layers, you prevent unauthorized access while enabling collaborative, enterprise-grade workflows. This approach makes it possible for teams to connect to shared MCP servers with confidence that only approved users and roles can perform sensitive operations.&lt;/p&gt;

&lt;p&gt;Descope provides &lt;a href="https://www.descope.com/use-cases/ai" rel="noopener noreferrer"&gt;purpose-built identity infrastructure for AI agents and MCP servers&lt;/a&gt;. Teams building external-facing MCP servers can add OAuth 2.1, PKCE, and secure DCR in three lines of code. Teams building AI agents can offload token management and storage for third-party tool connections. Teams building internal-facing MCP servers can implement &lt;a href="https://www.descope.com/blog/post/agentic-identity-control-plane" rel="noopener noreferrer"&gt;policy-based AI agent access to corporate tools&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whether you're building your first MCP server or scaling agentic workflows to production, Descope eliminates auth complexity so you can focus on building AI experiences. Explore &lt;a href="https://www.descope.com/ai" rel="noopener noreferrer"&gt;Descope's AI-focused demos&lt;/a&gt;, check out the &lt;a href="https://docs.descope.com/mcp" rel="noopener noreferrer"&gt;MCP documentation&lt;/a&gt;, or get started with a &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Free Forever account&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%2Fcxy8ekay7j9cs67e0jmi.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%2Fcxy8ekay7j9cs67e0jmi.png" alt="Agentic Identity Control Plane" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
      <category>security</category>
    </item>
    <item>
      <title>Add Authentication and MFA to Unreal Engine with Descope</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 08 Apr 2026 16:55:57 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-mfa-to-unreal-engine-with-descope-2ff1</link>
      <guid>https://dev.to/descope/add-authentication-and-mfa-to-unreal-engine-with-descope-2ff1</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-mfa-unreal" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Unreal Engine (UE) is widely known for its ability to create high-fidelity, immersive gaming experiences. Some of the most popular games today have been created with this more-than-capable engine, including games like &lt;a href="https://www.fortnite.com/" rel="noopener noreferrer"&gt;Fortnite&lt;/a&gt; and &lt;a href="https://www.rocketleague.com/en" rel="noopener noreferrer"&gt;Rocket League&lt;/a&gt;. Its visual scripting, robust C++ foundation, and cross-platform support make it a top choice for game development, from indie developers to AAA studios.&lt;/p&gt;

&lt;p&gt;But building a great game isn't just about graphics or gameplay. When you start adding features like multiplayer, player profiles, cross-device syncing, and monetization, secure, seamless authentication becomes nonnegotiable. Making sure that only verified users can access key features or data is essential for both user safety and developer control. &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; is a flexible identity platform that allows developers to embed authentication and &lt;a href="https://www.descope.com/learn/post/mfa" rel="noopener noreferrer"&gt;multifactor authentication (MFA)&lt;/a&gt; flows into their apps with minimal friction. With support for &lt;a href="https://www.descope.com/learn/post/magic-links" rel="noopener noreferrer"&gt;magic links&lt;/a&gt;, &lt;a href="https://www.descope.com/learn/post/otp" rel="noopener noreferrer"&gt;OTPs&lt;/a&gt;, and &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social logins&lt;/a&gt;, Descope makes it easier to build secure login flows that will feel like a natural part of your game.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll learn how to add authentication and MFA to a UE game using Descope. Along the way, we'll cover the different approaches game developers typically consider when implementing authentication, walk through a real-world integration using Descope's &lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OpenID Connect (OIDC)&lt;/a&gt; support, and demonstrate how to display user details and handle logouts in game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approaches to game authentication
&lt;/h2&gt;

&lt;p&gt;Game developers generally have three options when it comes to implementing authentication. The first is to rely on a storefront's built-in identity system, such as &lt;a href="https://store.epicgames.com/en-US" rel="noopener noreferrer"&gt;Epic Games&lt;/a&gt; or &lt;a href="https://store.steampowered.com/" rel="noopener noreferrer"&gt;Steam&lt;/a&gt;. These systems offer smooth onboarding within their ecosystems and can integrate well with features like &lt;a href="https://partner.steamgames.com/doc/features/multiplayer/matchmaking" rel="noopener noreferrer"&gt;matchmaking&lt;/a&gt;, &lt;a href="https://partner.steamgames.com/doc/features/achievements" rel="noopener noreferrer"&gt;achievements&lt;/a&gt;, and &lt;a href="https://dev.epicgames.com/docs/game-services/leaderboards/leaderboards-guide/set-up-leaderboards" rel="noopener noreferrer"&gt;leaderboards&lt;/a&gt;. However, they also come with some trade-offs: limited customization, dependency on a single platform, and constraints around marketing access or monetization outside the store.&lt;/p&gt;

&lt;p&gt;The second option is to purchase an authentication plugin from &lt;a href="https://www.fab.com/" rel="noopener noreferrer"&gt;Fab&lt;/a&gt;, Epic Games' new unified marketplace for digital assets. While this can help to kickstart development, the quality and reliability of these plugins can vary. Many still require manual setup and ongoing maintenance or lack support for features like MFA. For teams working on production-grade titles, these limitations often become apparent late in development, right when integration changes are most costly.&lt;/p&gt;

&lt;p&gt;Finally, you could opt to build your own authentication system from the ground up. This offers the most control but comes at a high cost in terms of engineering time, ongoing security maintenance, and compliance.&lt;/p&gt;

&lt;p&gt;However, a custom authentication system does bring some advantages that the other options might not be able to offer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain consistent player identity across storefronts and devices&lt;/li&gt;
&lt;li&gt;Implement subscriptions or microtransactions outside of the game platform&lt;/li&gt;
&lt;li&gt;Get better user analytics and tracking&lt;/li&gt;
&lt;li&gt;Reduce reliance on third-party platforms&lt;/li&gt;
&lt;li&gt;Strengthen overall security with flexible MFA options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Descope helps you to bridge this gap by offering a secure, developer-first approach to authentication and MFA. This approach works across platforms, can be integrated into your game's &lt;a href="https://www.ibm.com/think/topics/user-experience" rel="noopener noreferrer"&gt;UX&lt;/a&gt;, and doesn't require you to become an authentication expert.&lt;/p&gt;

&lt;p&gt;In the following sections, you'll learn how to implement a secure, user-friendly authentication flow in Unreal Engine using Descope: from setting up your Descope project to integrating it with your UE game, configuring authentication flows, and adding multifactor authentication. By the end, you'll have a working login system with MFA, profile fetching, and logout functionality ready to plug into your own game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you get started, you're going to need a few tools and services downloaded, installed, and ready to go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unreal Engine v5.6+ (installed via the &lt;a href="https://store.epicgames.com/en-US/download" rel="noopener noreferrer"&gt;Epic Games Store launcher&lt;/a&gt;). Instructions for installing the full UE can be &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/install-unreal-engine" rel="noopener noreferrer"&gt;found here&lt;/a&gt;. The tutorial might work on an older version, but your mileage may vary.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://visualstudio.microsoft.com/downloads/" rel="noopener noreferrer"&gt;Visual Studio&lt;/a&gt; or &lt;a href="https://visualstudio.microsoft.com/vs/community/" rel="noopener noreferrer"&gt;Visual Studio Community Edition&lt;/a&gt;, depending on your licensing needs.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope account&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Visual Studio: specific modifications for Unreal Engine
&lt;/h3&gt;

&lt;p&gt;Visual Studio's installer allows you to set up the &lt;a href="https://www.codecademy.com/article/what-is-ide" rel="noopener noreferrer"&gt;IDE&lt;/a&gt; for different workloads or project types. If you've previously only used VS for Python projects, you'll have it set up in a particular way that won't work with UE, which requires a few specific components for it to fully work.&lt;/p&gt;

&lt;p&gt;Launch the VS installer and click Modify to modify your installation of Visual Studio:&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%2Fetblvmybx77izj68nckq.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%2Fetblvmybx77izj68nckq.png" alt="Fig: Modify VS installation" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the Workloads tab, in the Desktop &amp;amp; Mobile section, you need to make sure that these three components are selected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.NET desktop development&lt;/li&gt;
&lt;li&gt;Desktop development with C++&lt;/li&gt;
&lt;li&gt;WinUI application development&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%2Fc3g2buudwww0ew7sat0n.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%2Fc3g2buudwww0ew7sat0n.png" alt="Fig: Selected Desktop &amp;amp; Mobile components" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still under Workloads, in the Gaming section, make sure that Game development with C++ is selected:&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%2Fy5jyhu4jyfjollm7otq7.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%2Fy5jyhu4jyfjollm7otq7.png" alt="Fig: Selected gaming components" width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've downloaded and installed all of the components, your Visual Studio environment is ready to go.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If, for whatever reason, things don't seem to be working, or the installed version of UE is starting up with errors, &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/setting-up-visual-studio-development-environment-for-cplusplus-projects-in-unreal-engine" rel="noopener noreferrer"&gt;consult the help pages&lt;/a&gt; on Epic Games' website to troubleshoot these errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Setting up Descope
&lt;/h2&gt;

&lt;p&gt;Once you've set up your Descope account, you can start designing your authentication workflow.&lt;/p&gt;

&lt;p&gt;First, log in to your Descope console &lt;a href="https://app.descope.com/" rel="noopener noreferrer"&gt;via this link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Navigate to Flows and then select Start from template.&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%2Fo6i07phsy9oayl4jamyz.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%2Fo6i07phsy9oayl4jamyz.png" alt="Fig: Start from template" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The flow template library allows you to filter by use cases and methods. Select Enchanted Link and Social (OAuth/OIDC) for your methods, and MFA for your use case. Select the Sign up or in version of the templates that matched your selected filters.&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%2Fsfmlc3r7steqbr4b18w6.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%2Fsfmlc3r7steqbr4b18w6.png" alt="Fig: Select template" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can rename your flow and give it a unique ID if you want. Once you're happy, click Create.&lt;/p&gt;

&lt;p&gt;Once your new flow has been created, you can customize the flow to behave the way you want it. In this tutorial, we'll remove the social login buttons for Google and Apple, and add a Discord button instead.&lt;/p&gt;

&lt;p&gt;Hover over the Welcome Screen element on the flow and select Edit.&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%2F1rtq4x57o54f596kaopw.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%2F1rtq4x57o54f596kaopw.png" alt="Fig: Edit Welcome Screen" width="592" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Delete the Google and Apple social buttons, and drag in a Discord button from the sidebar.&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%2Fr6exgtcan2x3fery39if.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%2Fr6exgtcan2x3fery39if.png" alt="Fig: Drag Discord button" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember to save your flow once you're happy with the changes you've made.&lt;/p&gt;

&lt;p&gt;Your custom authentication flow has now been set up and is ready to be used in your Unreal Engine project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating your Unreal Engine game
&lt;/h2&gt;

&lt;p&gt;With the prerequisites in place, the first step is to set up the new Unreal Engine project that you will add authentication to. Start off by opening Unreal Engine either from the Epic Games Launcher or from the installed location.&lt;/p&gt;

&lt;p&gt;Then, create a blank game project and give it a name. This tutorial will use UNREAL_AUTH_DEMO as the project name. Make sure to select C++ as your project 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%2Fvlo8vxlwb7k32dpqmf8w.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%2Fvlo8vxlwb7k32dpqmf8w.png" alt="Fig: Blank game project" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything went well, you'll see an empty game world showing up in the editor:&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%2Fztsyt4y04efmrrh3if4l.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%2Fztsyt4y04efmrrh3if4l.png" alt="Fig: Empty game world" width="752" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it didn't go well and your editor refused to start, now would be the time to consult the &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/setting-up-visual-studio-development-environment-for-cplusplus-projects-in-unreal-engine" rel="noopener noreferrer"&gt;Setting Up Visual Studio&lt;/a&gt; page to see if you're missing any components that need to be installed first.&lt;/p&gt;

&lt;p&gt;Otherwise, you're ready to create your menu component that will allow your users to sign up or in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create your main menu
&lt;/h3&gt;

&lt;p&gt;In the UE editor, at the bottom, expand your Content Drawer. Create a UI folder under Content to keep things organized.&lt;/p&gt;

&lt;p&gt;Add a User Interface widget blueprint:&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%2Fy9h0tktfcy1uwwb8fd47.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%2Fy9h0tktfcy1uwwb8fd47.png" alt="Fig: Add User Interface widget blueprint" width="718" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click User Widget as the parent class when prompted:&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%2Fobmddskma6ha0vsj2s1r.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%2Fobmddskma6ha0vsj2s1r.png" alt="Fig: User Widget Parent class" width="620" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Name your widget something practical like LoginMenu:&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%2Fav95vx7jb4m5o1vsl6zj.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%2Fav95vx7jb4m5o1vsl6zj.png" alt="Fig: Name your widget" width="616" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you're in the designer, you're going to need the following on your menu blueprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two buttons, one named SigninButton and another named SignoutButton.&lt;/li&gt;
&lt;li&gt;Both buttons need to have the Is Variable option enabled.&lt;/li&gt;
&lt;li&gt;A textbox named TextResult, also with Is Variable enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once your menu has been created, you should have a hierarchy that looks similar to this:&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%2Ffj8exyzjh6oojs4v54gc.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%2Ffj8exyzjh6oojs4v54gc.png" alt="Fig: Final hierarchy" width="361" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And while your menu might look slightly different, it should look similar to this:&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%2Fir5sfdzsqztu1hu5ai76.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%2Fir5sfdzsqztu1hu5ai76.png" alt="Fig: Final designer area" width="295" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This menu is now ready to be displayed in your UE level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Display your menu
&lt;/h3&gt;

&lt;p&gt;Now that you have a login menu, you'll want it to appear when your game starts.&lt;/p&gt;

&lt;p&gt;In the UE editor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new empty level (for example, you can name it MainMenuLevel).&lt;/li&gt;
&lt;li&gt;Open the level blueprint.&lt;/li&gt;
&lt;li&gt;On Begin Play, add a Create Widget node, select your LoginMenu widget, then connect it to an Add to Viewport node.&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%2F20z46pdrt3dajmasybm0.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%2F20z46pdrt3dajmasybm0.png" alt="Fig: Add to Viewport" width="776" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save your level blueprint, exit the blueprint editor, and click Play from the level editor to see your new menu inside the level:&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%2Ffh0ufkwgvgqaxeqx1qty.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%2Ffh0ufkwgvgqaxeqx1qty.png" alt="Fig: Menu appears in level" width="715" height="709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create your custom login widget class
&lt;/h3&gt;

&lt;p&gt;Next, you'll add some simple C++ logic to connect to the menu.&lt;/p&gt;

&lt;p&gt;Create a new C++ class (UserWidget parent) and name it LoginUIWidget.&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%2Fuud5vqx13xzf7ak9habh.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%2Fuud5vqx13xzf7ak9habh.png" alt="Fig: LoginUIWidget" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wire up basic button click events to confirm your UI is responding.&lt;/p&gt;

&lt;p&gt;Update LoginUIWidget.h to declare the buttons, text block, and click handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"CoreMinimal.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Blueprint/UserWidget.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"LoginUIWidget.generated.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;UCLASS&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;UNREAL_AUTH_DEMO_API&lt;/span&gt; &lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UUserWidget&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GENERATED_BODY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;NativeConstruct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;OnSigninClicked&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;OnSignoutClicked&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nl"&gt;protected:&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&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;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SigninButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&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;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SignoutButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&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;UTextBlock&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TextResult&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;p&gt;Note the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace UNREAL_AUTH_DEMO_API with your actual project name and API.&lt;/li&gt;
&lt;li&gt;Ensure the UButton and UTextBlock names match your blueprint widget variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then update LoginUIWidget.cpp to register the button click handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"LoginUIWidget.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Components/Button.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Components/TextBlock.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NativeConstruct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Super&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NativeConstruct&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SigninButton&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SigninButton&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OnClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDynamic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OnSigninClicked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SignoutButton&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SignoutButton&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OnClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDynamic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OnSignoutClicked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OnSigninClicked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sign in button pressed!"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// login logic here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OnSignoutClicked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sign out button pressed!"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// logout logic here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test code does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hooks up the Sign In and Sign Out buttons&lt;/li&gt;
&lt;li&gt;Logs a message to the UE output log when either button is pressed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save these two files. Now, you'll need to reparent your existing login menu blueprint to use this new class's logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reparent your login menu blueprint
&lt;/h3&gt;

&lt;p&gt;Open up the LoginMenu blueprint from the content drawer. Then, select File &amp;gt; Reparent Blueprint. You'll be able to search for LoginUIWidget (the new class you just created) and then select it. This will reparent your blueprint's logic to the custom from earlier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; At this point, you might run into an annoying bug that some people have with UE5 and VS 2022. If you do not see the class listed when you search for it, you'll need to do the following steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Close the Unreal Engine editor.&lt;/li&gt;
&lt;li&gt;In Visual Studio, select Build &amp;gt; Build unreal_auth_demo or the project name you chose.&lt;/li&gt;
&lt;li&gt;Wait for the build process to finish, and open the Unreal editor again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes, you can use the build process without having to close the editor, and there are ways to fix it, but that's beyond the scope of this tutorial. In any case, if you've followed the steps, you should be able to reparent the blueprint to your custom class.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Save your blueprint once you've finished the reparent process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change your default level and test the button
&lt;/h3&gt;

&lt;p&gt;Back in the editor, go to Edit &amp;gt; Project Settings, and then click Maps &amp;amp; Modes on the left sidebar menu. Change your Editor Startup Map and your Game Default Map to point to the MainMenuLevel you created.&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%2F9abvix35ujncklwkbc5o.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%2F9abvix35ujncklwkbc5o.png" alt="Fig: Change default map" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, you can test your custom button code logic. Play the game using the same green play button from before, and click the Sign in button.&lt;/p&gt;

&lt;p&gt;In your editor, you'll see a drawer called Output Log. Expand this drawer, scroll to the bottom, and see if your custom output message is there.&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%2Fscmmtmrctr56r51jspsw.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%2Fscmmtmrctr56r51jspsw.png" alt="Fig: Output Log" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you see the messages, that means your code is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get some information from Descope
&lt;/h2&gt;

&lt;p&gt;Before you can connect Unreal Engine to Descope, you need to gather some details about how the game will communicate with Descope during the login process. These details come from &lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; and &lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OpenID Connect (OIDC)&lt;/a&gt;, which are the standards we'll use to handle authentication with Descope.&lt;/p&gt;

&lt;h3&gt;
  
  
  OAuth and OIDC
&lt;/h3&gt;

&lt;p&gt;OAuth 2.0 is a protocol that lets an app (in this case, your game) request authorization for a user through a trusted provider like Descope. Instead of handling passwords directly, your game gets a temporary authorization code that can be exchanged for tokens.&lt;/p&gt;

&lt;p&gt;OpenID Connect (OIDC) builds on OAuth 2.0 and adds identity information. That's how your game can get details like the player's username, email, or profile data.&lt;/p&gt;

&lt;p&gt;In practice, this means your Unreal Engine project will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redirect the player to Descope's login page.&lt;/li&gt;
&lt;li&gt;Descope will authenticate the user (e.g., via Discord login + MFA).&lt;/li&gt;
&lt;li&gt;Descope will send your game back an authorization code.&lt;/li&gt;
&lt;li&gt;Your game exchanges that code for tokens that prove the user is authenticated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make this work, your game needs some values from Descope's OIDC configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting the OIDC Settings
&lt;/h3&gt;

&lt;p&gt;In your Descope console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to Federated Apps.&lt;/li&gt;
&lt;li&gt;Click OIDC Default Application. (On the free tier, this is the only one available for editing.)&lt;/li&gt;
&lt;li&gt;Scroll down to the SP Configuration section and note the following values:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; identifies your Unreal Engine app to Descope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization URL:&lt;/strong&gt; the endpoint your game uses to start the login flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token URL:&lt;/strong&gt; the endpoint your game calls to exchange the authorization code for tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logout URL:&lt;/strong&gt; the endpoint that clears the user's session when they sign out.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These values form the backbone of the login flow. In the next step, you'll wire them into the Unreal Engine code so the game can start and complete the OAuth process.&lt;/p&gt;

&lt;p&gt;While you're on this screen, update the Flow Hosting URL to point to the custom flow you created earlier:&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%2Fj7eb6udmuxn4ga35k2vf.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%2Fj7eb6udmuxn4ga35k2vf.png" alt="Fig: Custom Flow Hosting URL" width="780" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Modify your custom class
&lt;/h2&gt;

&lt;p&gt;Next, let's update LoginUIWidget.h to declare the functions, variables, and UI bindings that power the authentication flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"CoreMinimal.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Blueprint/UserWidget.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"LoginUIWidget.generated.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cm"&gt;/**
 *
 */&lt;/span&gt;
&lt;span class="n"&gt;UCLASS&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;UNREAL_AUTH_DEMO_API&lt;/span&gt; &lt;span class="n"&gt;ULoginUIWidget&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UUserWidget&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GENERATED_BODY&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;NativeConstruct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;OnSigninClicked&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;OnSignoutClicked&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;SetLoginState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bNewLoginState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UpdateUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nl"&gt;protected:&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&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;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SigninButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&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;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SignoutButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&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;UTextBlock&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TextResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Login state&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlueprintReadOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Login"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bIsLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Descope OIDC Parameters&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;ClientId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;RedirectUri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;RedirectLogoutUri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Descope endpoint URLs&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;AuthUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;TokenUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;LogoutUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;UserInfoUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;CapturedAuthCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// PKCE values&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;CodeVerifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;CodeChallenge&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Tokens&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;IdToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;RefreshToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;GeneratePKCEParameters&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;StartOAuthLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ListenForAuthCodeInBackground&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;PerformSignout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;RevokeTokens&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ClearSessionData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Fetching user data variables and functions&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;UserEmail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;FetchUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ExchangeAuthCodeForTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Code&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;p&gt;At a high level, this header sets up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI hooks: SigninButton, SignoutButton, TextResult&lt;/li&gt;
&lt;li&gt;Authentication state: bIsLoggedIn, tokens, and PKCE parameters&lt;/li&gt;
&lt;li&gt;Core functions: StartOAuthLogin, ExchangeAuthCodeForTokens, FetchUserProfile, and PerformSignout&lt;/li&gt;
&lt;li&gt;Networking: a socket listener (ListenForAuthCodeInBackground) to receive the redirect from Descope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the header file in place, you can now implement the functionality in LoginUIWidget.cpp. This is where the actual behavior for our authentication flow lives—handling button clicks, generating PKCE parameters, launching the OAuth login, listening for the redirect from Descope, exchanging the authorization code for tokens, and finally fetching the user's profile information to display in the game.&lt;/p&gt;

&lt;p&gt;The next code snippet is large, so it's recommended to copy the code directly from the &lt;a href="https://github.com/thinusswart/unreal_descope_auth_demo/blob/main/LoginUIWidget.cpp" rel="noopener noreferrer"&gt;Github repo, here&lt;/a&gt;. Just remember to update the following variables with the values you retrieved from Descope earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ClientId&lt;/li&gt;
&lt;li&gt;RedirectUri&lt;/li&gt;
&lt;li&gt;RedirectLogoutUri&lt;/li&gt;
&lt;li&gt;AuthUrl&lt;/li&gt;
&lt;li&gt;TokenUrl&lt;/li&gt;
&lt;li&gt;LogoutUrl&lt;/li&gt;
&lt;li&gt;UserInfoUrl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are the most important parts of the .cpp:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PKCE support (GeneratePKCEParameters) for secure OAuth logins&lt;/li&gt;
&lt;li&gt;Browser-based login (StartOAuthLogin) using the system browser&lt;/li&gt;
&lt;li&gt;Local redirect listener (ListenForAuthCodeInBackground) to capture the authorization code and exchange it for tokens&lt;/li&gt;
&lt;li&gt;Profile fetching (FetchUserProfile) to display the user's email and username&lt;/li&gt;
&lt;li&gt;Thread safety and timeouts—runs the socket in a background thread, logs any errors to the UE output log&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, because this project uses new features like network sockets and HTTP calls, you'll need to enable the corresponding Unreal Engine modules in your project's build configuration. Open your project's build file (ProjectName.Build.cs) and add the required dependencies, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnrealBuildTool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;unreal_auth_demo&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ModuleRules&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;unreal_auth_demo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadOnlyTargetRules&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PCHUsage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PCHUsageMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseExplicitOrSharedPCHs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });&lt;/span&gt;
        &lt;span class="n"&gt;PublicDependencyModuleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"CoreUObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"InputCore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Networking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"HTTP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"JsonUtilities"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Sockets"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;PrivateDependencyModuleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Uncomment if you are using Slate UI&lt;/span&gt;
        &lt;span class="c1"&gt;// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });&lt;/span&gt;

        &lt;span class="c1"&gt;// Uncomment if you are using online features&lt;/span&gt;
        &lt;span class="c1"&gt;// PrivateDependencyModuleNames.Add("OnlineSubsystem");&lt;/span&gt;

        &lt;span class="c1"&gt;// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true&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;p&gt;Build your code again using Build -&amp;gt; Build unreal_auth_demo. Once it's finished building, you can open up the Unreal editor again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it all
&lt;/h2&gt;

&lt;p&gt;Once your editor is open, run the game using the green play button again.&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%2F4ofxmi9lfyycifwq0601.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%2F4ofxmi9lfyycifwq0601.png" alt="Fig: Sign-in screen" width="726" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that your Sign Out button is not visible. This is because of the changes in the UpdateUI() method to show and hide the different buttons based on your logged-in state.&lt;/p&gt;

&lt;p&gt;Click Sign In. A browser window will open with the following login UI:&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%2Fimv3ivzcfm6p599x4q9x.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%2Fimv3ivzcfm6p599x4q9x.png" alt="Fig: Show Discord login" width="688" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the right authentication flow that you set up earlier on Descope's flow designer. Click Sign in with Discord.&lt;/p&gt;

&lt;p&gt;The first time you sign in, Descope will ask for some extra information about you and verify your identity using MFA.&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%2F8dzmsseclqnu0vqo44o0.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%2F8dzmsseclqnu0vqo44o0.png" alt="Fig: Username and phone number" width="700" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you entered your number correctly, you should receive a verification code via SMS:&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%2Fn9z3e7pqhrezg70kcd0j.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%2Fn9z3e7pqhrezg70kcd0j.png" alt="Fig: Enter verification code" width="625" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've entered the code, your screen should look like this:&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%2Fdyhto9xy9l0jgcawc0n2.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%2Fdyhto9xy9l0jgcawc0n2.png" alt="Fig: Verified" width="627" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This process should only happen the first time you sign in with a new user. Click Submit to continue.&lt;/p&gt;

&lt;p&gt;Discord will open up with the following dialog:&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%2Fnbtvuptn8rve00d9tnhy.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%2Fnbtvuptn8rve00d9tnhy.png" alt="Fig: Discord login" width="499" height="745"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Authorize and you should see the following:&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%2Femezwcglszp9r0jqi6fa.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%2Femezwcglszp9r0jqi6fa.png" alt="Fig: Descope authorized" width="516" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Descope will now call the configured callback URL (in your case, &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;) to tell your server that the login was successful and to give you an authorization code:&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%2Fhkvctjxrgu91ey4kbn81.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%2Fhkvctjxrgu91ey4kbn81.png" alt="Fig: Login successful" width="561" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the background, your code will use the ExchangeAuthCodeForTokens and FetchUserProfile methods to fetch proper authentication tokens and to retrieve user information. Once that's complete, it will update the UI in your game:&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%2Frelzx1u7aae7t36cerqi.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%2Frelzx1u7aae7t36cerqi.png" alt="Fig: Game UI updated" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice now that the Sign In button has been replaced with a Sign Out button.&lt;/p&gt;

&lt;p&gt;Try giving that button a click:&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%2Fu0jstifh54er6pi948b3.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%2Fu0jstifh54er6pi948b3.png" alt="Fig: Sign Out clicked" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You've now successfully integrated Descope as your auth provider for your Unreal Engine 5 game.&lt;/p&gt;

&lt;p&gt;As mentioned before, all of the code used in this tutorial &lt;a href="https://github.com/thinusswart/unreal_descope_auth_demo" rel="noopener noreferrer"&gt;is available here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Adding authentication and MFA to an Unreal Engine game doesn't necessarily mean writing your own identity system or locking yourself into a single storefront. With Descope, you can implement secure, flexible login flows, including social login and multifactor authentication, using modern standards like &lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OIDC&lt;/a&gt; and &lt;a href="https://www.descope.com/learn/post/pkce" rel="noopener noreferrer"&gt;PKCE&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial, you built a working login system in Unreal Engine from the ground up. You integrated Descope with Discord for social login, captured the authorization code using a local socket, exchanged it for tokens, and fetched the authenticated user's profile to update your game's UI. You now have a foundation for cross-platform identity, security, and personalization inside your UE game.&lt;/p&gt;

&lt;p&gt;Descope offers even more beyond what was covered here: drag-and-drop flow builders, passwordless authentication, risk-based access policies, and more. If you're building a connected game and want your auth system to scale with you, &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book a demo with Descope&lt;/a&gt;, or check out &lt;a href="https://docs.descope.com/" rel="noopener noreferrer"&gt;their documentation&lt;/a&gt; and explore how it can fit the identity strategy for your game or website.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>gamedev</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Developer’s Guide to JWT Storage</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 06 Apr 2026 17:07:00 +0000</pubDate>
      <link>https://dev.to/descope/the-developers-guide-to-jwt-storage-5ff7</link>
      <guid>https://dev.to/descope/the-developers-guide-to-jwt-storage-5ff7</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/developer-guide-jwt-storage" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/learn/post/jwt" rel="noopener noreferrer"&gt;JSON Web Tokens (JWTs)&lt;/a&gt; are compact, self-contained tokens used to securely transmit information between parties while maintaining data integrity. Many authorization providers use JWTs to provide a tamper-proof way of identifying an authenticated user in an application and checking what they're authorized to do.&lt;/p&gt;

&lt;p&gt;Unfortunately, securely storing JWTs is often overlooked, which can expose some serious vulnerabilities. For example, cross-site scripting (XSS) attacks can steal JWTs, letting malicious actors impersonate users and access sensitive data. Therefore, you need to store JWTs securely to protect users and &lt;a href="https://www.statista.com/statistics/273575/us-average-cost-incurred-by-a-data-breach/" rel="noopener noreferrer"&gt;potentially save your organization millions by preventing data breaches&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This guide will explore essential JWT security considerations, popular browser storage methods, and their benefits and drawbacks, best practices, and troubleshooting tips for working with JWTs. By the end, you'll be well-informed about the available JWT storage methods and choose the most appropriate solution for your use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding JWT storage
&lt;/h2&gt;

&lt;p&gt;JWTs are compact, self-contained tokens used to transmit user information in requests to a server. Given HTTP's stateless nature, once an application receives the token, it must be stored and used for subsequent API requests to the server.&lt;/p&gt;

&lt;p&gt;The JWT lifecycle consists of three stages: creation, storage, and usage. First, an authorization server generates the JWT after the user is successfully authenticated. Second, the application stores the token, typically in the browser. Third, the application includes the token in the authorization header when making requests to the resource server, which verifies the token before processing the request.&lt;/p&gt;

&lt;p&gt;Here's a diagram of the process:&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%2F16txzij8m9d8lj50mwka.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%2F16txzij8m9d8lj50mwka.png" alt="JWT Lifecycle" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Security is critical when storing JWT storage. Storing it improperly can lead to token theft, making it possible for attackers to impersonate users or elevate their privileges. Common attack vectors include the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Arbitrary signature attacks:&lt;/strong&gt; Arbitrary signature attacks occur when servers fail to verify token signatures, allowing attackers to modify claims and escalate privileges or impersonate users. While this is primarily a server-side issue, secure JWT storage makes it harder for attackers to obtain a valid token to study and manipulate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;none&lt;/code&gt; algorithm attacks:&lt;/strong&gt; The &lt;code&gt;none&lt;/code&gt; algorithm attack happens when servers mistakenly accept unsigned JWTs due to the &lt;code&gt;alg&lt;/code&gt; parameter being set to &lt;code&gt;none&lt;/code&gt;. JWT storage is not entirely related to this attack. However, it's harder for attackers to put together a malicious token if they can't gain access to the valid one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm confusion attacks:&lt;/strong&gt; In algorithm confusion attacks, attackers exploit mismatches between the signing and verification algorithms, generating valid tokens with their own keys. If tokens are stored insecurely, attackers can more easily obtain them to study the algorithm used and attempt to craft tokens with manipulated algorithms and secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kid manipulation:&lt;/strong&gt; Key ID (kid) manipulation exploits vulnerabilities in the kid parameter by injecting malicious commands into the token's key verification process. As with other attack vectors, kid manipulation is more straightforward if attackers obtain a valid JWT. However, the vulnerability needs to be fixed on the server side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brute force attacks:&lt;/strong&gt; Brute force attacks target weak or simple symmetric encryption secrets, allowing attackers to generate malicious tokens by guessing the secret. Secure JWT storage is crucial in preventing brute-force attacks. If tokens are stored insecurely, attackers can easily obtain valid tokens to use as a reference when attempting to guess the secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To mitigate these risks, it's crucial that you implement proper server-side token verification, disable insecure algorithms, use strong encryption, and validate all parameters in the token. When storing tokens in a client-side application, you must do so securely to prevent unauthorized access to valid tokens, which attackers could study and manipulate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common JWT storage methods
&lt;/h2&gt;

&lt;p&gt;Several options are available for storing tokens client-side in the browser. Each has its strengths and weaknesses, and you must assess your application requirements and decide which technique is best suited.&lt;/p&gt;

&lt;p&gt;For example, session or in-memory storage might be ideal if user sessions are generally short and get logged out when they're done. However, you might consider cookies or local storage for longer-lived user sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local storage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" rel="noopener noreferrer"&gt;Local storage&lt;/a&gt; provides a simple API for storing user data across sessions in the browser. Its simplicity and robustness across sessions make it a popular choice for storing JWTs in the browser. However, it's important to note that local storage is vulnerable to XSS attacks, where attackers can inject malicious scripts to access stored data, including JWTs. Make sure to implement appropriate &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;safety measures&lt;/a&gt; to prevent this.&lt;/p&gt;

&lt;p&gt;Run the following code to store a JWT in local storage after receiving it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Store JWT&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwtToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Use JWT in request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwtToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/api/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Clear JWT (log out)&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwtToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local storage has some pros and cons that you should carefully consider:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Easy to implement&lt;/li&gt;
&lt;li&gt;Accessible from JavaScript code&lt;/li&gt;
&lt;li&gt;Accessible across browser tabs&lt;/li&gt;
&lt;li&gt;Persistent between page refreshes&lt;/li&gt;
&lt;li&gt;Generous storage limit (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#web_storage" rel="noopener noreferrer"&gt;5MB&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Vulnerable to XSS attacks&lt;/li&gt;
&lt;li&gt;No automatic token expiration mechanism&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local storage is a popular choice for storing JWTs as it lets you persist tokens across pages and is easy to access from JavaScript. However, make sure you patch XSS vulnerabilities to prevent malicious actors from stealing tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookies
&lt;/h3&gt;

&lt;p&gt;Cookies offer additional security compared to other client-side storage options. They require server-side modifications to create secure cookies containing the token. When properly configured, cookies are less vulnerable to XSS attacks, but they can still be susceptible to cross-site request forgery (CSRF) attacks.&lt;/p&gt;

&lt;p&gt;Use the &lt;code&gt;HttpOnly&lt;/code&gt; flag to ensure client-side JavaScript cannot access the cookies. The &lt;code&gt;Secure&lt;/code&gt; flag ensures the cookie is sent only over a secure channel that's not susceptible to man-in-the-middle attacks. Finally, the &lt;code&gt;SameSite&lt;/code&gt; attribute can be used with &lt;a href="https://owasp.org/www-community/Anti_CRSF_Tokens_ASP-NET" rel="noopener noreferrer"&gt;other anti-CSRF strategies&lt;/a&gt; to help prevent CSRF attacks.&lt;/p&gt;

&lt;p&gt;Here's a basic example of storing a JWT in a cookie using &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateJWT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Insert the token into the `auth_jwt` cookie in the response&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&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;p&gt;Run the following code to make a request with the cookie in client-side JavaScript code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/protected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Set credentials to same-origin so cookies are included&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same-origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following is to verify the token on the server using Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/protected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth_jwt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token is invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Continue processing the request here since the JWT is valid&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logging the user out involves simply clearing the cookie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/logout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User logged out.&lt;/span&gt;&lt;span class="dl"&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;p&gt;Consider the following pros and cons regarding cookie storage before using it for your JWTs:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Enhanced security against XSS attacks (with &lt;code&gt;HttpOnly&lt;/code&gt; flag)&lt;/li&gt;
&lt;li&gt;Automatically included in requests&lt;/li&gt;
&lt;li&gt;Built-in security features (&lt;code&gt;Secure&lt;/code&gt; flag and &lt;code&gt;SameSite&lt;/code&gt; attribute)&lt;/li&gt;
&lt;li&gt;Server-side control over token storage lets the server revoke tokens if necessary before processing requests. This could happen if a user logs out of another system and a &lt;a href="https://openid.net/specs/openid-connect-backchannel-1_0.html#Backchannel" rel="noopener noreferrer"&gt;back-channel logout request&lt;/a&gt; is sent to the server.&lt;/li&gt;
&lt;li&gt;Accessible across browser tabs&lt;/li&gt;
&lt;li&gt;Persistent between page refreshes&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Vulnerable to CSRF attacks (mitigate with the &lt;code&gt;SameSite&lt;/code&gt; attribute and anti-CSRF mechanisms)&lt;/li&gt;
&lt;li&gt;Limited storage capacity (around 4KB)&lt;/li&gt;
&lt;li&gt;Often requires additional server-side logic&lt;/li&gt;
&lt;li&gt;Possibly tricky to work with when working across domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cookie storage is a great solution as it provides a good balance of security and functionality. However, remember to implement proper security measures, like using the &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, and &lt;code&gt;SameSite&lt;/code&gt; flags, along with anti-CSRF mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session storage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage" rel="noopener noreferrer"&gt;Session storage&lt;/a&gt; is a JavaScript API in the browser that lets you persist data for a single page until it's closed. This means that the data stored is lost as soon as the page is closed. Also, if a user opens another page in a different tab or window, it will have its own session storage.&lt;/p&gt;

&lt;p&gt;Session storage is slightly more secure than local storage, as session storage gets cleared when the page closes. However, session storage is still vulnerable to XSS attacks. Use the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP cheat sheet&lt;/a&gt; and a &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html" rel="noopener noreferrer"&gt;Content Security Policy&lt;/a&gt; to prevent these attacks. However, be aware that malicious browser extensions can still access session storage.&lt;/p&gt;

&lt;p&gt;Here's how to use session storage for JWTs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Store JWT&lt;/span&gt;
&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwtToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieve JWT&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwtToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Use JWT in request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/api/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Remove JWT (logout)&lt;/span&gt;
&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwtToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before using session storage, consider the following pros and cons:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Easy to implement&lt;/li&gt;
&lt;li&gt;Accessible to client-side scripts&lt;/li&gt;
&lt;li&gt;Automatic clearing of tokens when the page is closed&lt;/li&gt;
&lt;li&gt;Generous storage limit (5MB)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Vulnerable to XSS attacks&lt;/li&gt;
&lt;li&gt;Lost token between page refreshes and tabs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Session storage offers a simple way to store JWTs and automatically clears them when the page is closed. Tokens are also accessible from client-side scripts. However, be sure to protect your app against XSS attacks to prevent stolen tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  In-memory storage
&lt;/h3&gt;

&lt;p&gt;In-memory storage keeps the JWT in the web application's memory using a JavaScript variable or a &lt;a href="https://thenewstack.io/leveraging-web-workers-to-safely-store-access-tokens/" rel="noopener noreferrer"&gt;web worker&lt;/a&gt;, rather than persisting it in browser storage. This approach is helpful in single-page applications (SPAs) where the page doesn't refresh often.&lt;/p&gt;

&lt;p&gt;In-memory storage can be considered slightly more secure since the token is obscured from the attacker. Attackers can't use a standard browser API to retrieve it. However, with enough patience, an attacker could still figure out where the token is being stored and retrieve it through an XSS attack. Malicious actors can also try to access the in-memory JWT using a debugger. Similar to other storage techniques, having a well-defined Content Security Policy and implementing XSS preventive measures are vital.&lt;/p&gt;

&lt;p&gt;The following snippet demonstrates how to store a JWT in memory using a JavaScript class with a private variable storing the JWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;clearToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Store JWT&lt;/span&gt;
&lt;span class="nx"&gt;tokenService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Use JWT in request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/api/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// token is the global variable&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Clear JWT (logout)&lt;/span&gt;
&lt;span class="nx"&gt;tokenService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In-memory might seem slightly more secure than some other methods, but it still has some cons, which might influence your decision to go with this approach:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Harder to retrieve via XSS attacks&lt;/li&gt;
&lt;li&gt;Accessible to client-side scripts&lt;/li&gt;
&lt;li&gt;Automatic token clearing when navigating pages and tabs&lt;/li&gt;
&lt;li&gt;No storage limitations&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Still vulnerable to sophisticated XSS attacks&lt;/li&gt;
&lt;li&gt;Lost token between page refreshes and tabs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In-memory offers a balance between security and convenience. It's slightly harder to retrieve using XSS attacks, but you should still take preventive measures to prevent these attacks in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices for JWT storage
&lt;/h2&gt;

&lt;p&gt;Regardless of which storage method you use, there are some best practices you should follow to minimize authentication and authorization vulnerabilities in your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption of stored tokens
&lt;/h3&gt;

&lt;p&gt;Storing tokens unencrypted makes it possible for an attacker to decode the token and see what claims are associated with it. &lt;a href="https://www.rfc-editor.org/rfc/rfc7516" rel="noopener noreferrer"&gt;JSON Web Encryption (JWE)&lt;/a&gt; is a standardized method of encrypting JWTs. The encryption happens on the server before the token is sent to the client, and the encryption keys are kept on the server or in a dedicated key management service.&lt;/p&gt;

&lt;p&gt;Since the client cannot access the encryption key, they cannot view any of the claims in the token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token expiration and rotation
&lt;/h3&gt;

&lt;p&gt;JWT expiration times dictate when a token is no longer valid. Once a token expires, you must retrieve a new one using a &lt;a href="https://www.descope.com/learn/post/refresh-token" rel="noopener noreferrer"&gt;refresh token&lt;/a&gt;. A refresh token is a token issued along with the original JWT that lets you request a new JWT when the current one expires. When issuing a new JWT using a refresh token, the authorization server first ensures the user's session is still active and then sends the new token if it is.&lt;/p&gt;

&lt;p&gt;You can further enhance the security of your JWT and refresh token by rotating the refresh tokens. This means that every time you request a new JWT using a refresh token, a new refresh token is generated along with the new token and returned. The refresh token returned is the only valid token that can be used to get the next JWT. For a deeper look at this, check out &lt;a href="https://www.descope.com/blog/post/refresh-token-rotation" rel="noopener noreferrer"&gt;The Developer's Guide to Refresh Token Rotation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You should also set an expiration period for your refresh tokens, as this adds an extra layer of security. Even if the attackers can access the refresh token, they might not use it in time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling of token revocation
&lt;/h3&gt;

&lt;p&gt;If a server uses only the JWT expiry time claim to determine whether a token has expired, then a token can still be valid after a user is logged out or blocked from an application. While shortening the JWT expiry time can help minimize the risk, there's still a window of time where a token is valid after the user's session has ended.&lt;/p&gt;

&lt;p&gt;By implementing a blocklist, the server can use the token's expiry time and the blocklist to determine if the token is valid. When a user logs out or their account is banned, the authorization server sends a &lt;a href="https://openid.net/specs/openid-connect-backchannel-1_0.html#Backchannel" rel="noopener noreferrer"&gt;back-channel logout request&lt;/a&gt; to other servers, indicating that this user's session has ended. Each server can use this event to update its blocklist with the user's details. Then, when another request is sent using that user's JWT, it can get picked up as invalid even if it hasn't expired yet.&lt;/p&gt;

&lt;p&gt;While most applications are okay with a JWT lasting a little longer than a user session, some might require stricter security. Implementing token revocation lets you derive the benefits of JWT while offering an efficient way of immediately blocking tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protection against XSS and CSRF attacks
&lt;/h3&gt;

&lt;p&gt;Every storage method discussed in this article can be prone to XSS and CSRF attacks.&lt;/p&gt;

&lt;p&gt;To prevent XSS attacks, use cookie authentication so JavaScript can't access the JWT. However, if your frontend JavaScript needs access to the JWT, you can implement a &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html" rel="noopener noreferrer"&gt;Content Security Policy&lt;/a&gt; to restrict which scripts can run on the page.&lt;/p&gt;

&lt;p&gt;If you use cookie authentication, ensure your cookies are set up correctly. They should be &lt;code&gt;HttpOnly&lt;/code&gt; and marked as &lt;code&gt;Secure&lt;/code&gt;. Additionally, configure &lt;code&gt;SameSite&lt;/code&gt; to restrict when the cookie gets sent in requests from third-party URLs. Other anti-CSRF mechanisms, like anti-forgery tokens, can also help secure against CSRF attacks.&lt;/p&gt;

&lt;p&gt;There's no perfect solution when it comes to XSS and CSRF attacks. Assess your requirements, decide which storage mechanism you will use, and then cater to the vulnerabilities that come with that method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting common issues
&lt;/h2&gt;

&lt;p&gt;JWTs can be challenging to troubleshoot when things go wrong. The following are some useful tips for troubleshooting JWT issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging JWT storage issues
&lt;/h3&gt;

&lt;p&gt;If you're struggling to figure out if the JWT is being stored successfully or not, use the &lt;strong&gt;Application&lt;/strong&gt; tab in your browser's developer tools to analyze the local storage, session storage, and cookies associated with the current 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%2Frks66u0iif4725c85zxe.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%2Frks66u0iif4725c85zxe.png" alt="Screenshot showing the browser’s developer tools" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the token appears in your chosen storage mechanism, verify that the token is in the correct format. It should consist of three parts separated by dots.&lt;/p&gt;

&lt;p&gt;You can also monitor the lifecycle of the token logging messages. This can help identify where the token is not being processed or stored correctly in your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling token expiration elegantly
&lt;/h3&gt;

&lt;p&gt;Tokens expire, so make sure you implement &lt;a href="https://www.descope.com/learn/post/refresh-token" rel="noopener noreferrer"&gt;refresh tokens&lt;/a&gt; and use them to retrieve new tokens when the old ones expire.&lt;/p&gt;

&lt;p&gt;When using refresh tokens, it's better to proactively refresh a token before it expires by monitoring its expiry time and triggering a refresh a few seconds before the expiry time. Otherwise, you might send a token that's still valid for a split second when the request is sent, but it expires when the server receives it.&lt;/p&gt;

&lt;p&gt;If a refresh request fails, retry it as a network issue could be the cause. If it fails again, redirect the user to the authentication screen with a message notifying them that they must log in to continue using the application. This makes your application more robust when it encounters authentication errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dealing with cross-domain storage hurdles
&lt;/h3&gt;

&lt;p&gt;Cookies are typically associated with a specific domain, and the browser sends the cookie only when a request is made to that particular domain. However, what if your application needs to make requests to multiple domains using the same token? For example, an application's frontend might be on a different domain than the API.&lt;/p&gt;

&lt;p&gt;If so, you should consider a backend-for-frontend (BFF) for your application. A BFF consolidates all those API calls to different services into a single service. This way, your cookie needs to be configured only for your BFF's domain. The BFF can then proxy the request to the appropriate service with the JWT.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWT storage on mobile
&lt;/h2&gt;

&lt;p&gt;Until now, you've seen how JWTs can be stored in a browser context. But what about mobile apps? Many apps need to access restricted APIs that require authentication.&lt;/p&gt;

&lt;p&gt;JWT storage solutions like &lt;code&gt;HttpOnly&lt;/code&gt; cookies work well in a web application but are more complex in mobile apps, given the absence of browser-specific mechanisms like cookie management. Your app would need to manually store the cookie and attach it to every request to the API. When storing the cookie, you also need to make sure it's secure and not accessible by other apps or the end-user.&lt;/p&gt;

&lt;p&gt;Fortunately, mobile platforms offer secure storage to store sensitive secrets like JWTs. On Android, you can use &lt;a href="https://developer.android.com/privacy-and-security/keystore" rel="noopener noreferrer"&gt;Keystore&lt;/a&gt; to encrypt JWTs, which you can then store using &lt;a href="https://developer.android.com/reference/android/content/SharedPreferences" rel="noopener noreferrer"&gt;SharedPreferences&lt;/a&gt;. Similarly, iOS lets you use &lt;a href="https://developer.apple.com/documentation/security/keychain-services" rel="noopener noreferrer"&gt;Keychain&lt;/a&gt; to encrypt and store secrets on the user's device. Using these services is similar to how you'd store JWTs in local storage on the web: after the user logs in, retrieve the JWT and store it.&lt;/p&gt;

&lt;p&gt;When storing JWTs in a mobile app, remember that these devices are more prone to theft, so create short-lived JWTs that get rotated often using refresh tokens. You could even store a session identifier instead of the JWT in the mobile app. With a session identifier, the app makes a request to the server, which does a database lookup to retrieve the associated JWT and validate it.&lt;/p&gt;

&lt;p&gt;While slightly more complex to set up, this approach makes it even harder for a malicious user to access the underlying JWT since it's stored and accessed on the server. This solution also gives you more control over mobile app sessions since you can terminate a session with immediate effect, unlike JWTs, which only end a session when they expire (or are added to a blacklist, as discussed above).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this guide, you've discovered how local storage, session storage, cookies, and in-memory storage can be used for storing &lt;a href="https://www.descope.com/learn/post/jwt" rel="noopener noreferrer"&gt;JWTs&lt;/a&gt; securely in web applications. The guide discussed the security implications of each approach and some pros and cons. You were then introduced to some best practices that should be followed regardless of your storage method. These include encrypting the JWTs, expiring tokens and rotating their &lt;a href="https://www.descope.com/learn/post/refresh-token" rel="noopener noreferrer"&gt;refresh keys&lt;/a&gt;, handling token revocation, and protecting against XSS and CSRF attacks. Finally, the guide also provided insights into securely storing JWTs in mobile applications.&lt;/p&gt;

&lt;p&gt;JWTs are one of the most popular forms of authentication in modern systems. As new use cases emerge, JWTs are tweaked to work in those scenarios. For example, JWTs were initially designed to be stateless, meaning servers could verify a token without looking it up in an external system. However, some use cases require immediate token revocation, which brought about stateful JWT authentication, where the server also stores information about the token to verify it's still valid.&lt;/p&gt;

&lt;p&gt;The stateless nature of JWTs has also made them a popular choice when building scalable applications since each server can verify the token by decoding the token passed in requests. JWTs and their use cases are evolving constantly, and it's crucial that you continuously reassess your application's authentication configuration, including how you store JWTs in applications, to ensure no new security vulnerabilities or loopholes appear.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Adding Authentication and SSO to a Streamlit App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 03 Apr 2026 17:07:00 +0000</pubDate>
      <link>https://dev.to/descope/adding-authentication-and-sso-to-a-streamlit-app-4cm3</link>
      <guid>https://dev.to/descope/adding-authentication-and-sso-to-a-streamlit-app-4cm3</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/authentication-sso-streamlit" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Streamlit&lt;/a&gt; makes it simple to turn &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; scripts into shareable data apps. As these apps move from personal notebooks to team and company use, adding secure &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;authentication&lt;/a&gt; and &lt;a href="https://www.descope.com/learn/post/sso" rel="noopener noreferrer"&gt;single sign-on (SSO)&lt;/a&gt; becomes essential. Authentication protects sensitive data and gates features by user identity. SSO lets people sign in once and move across apps without repeating logins.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll use &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;, a drag &amp;amp; drop &lt;a href="https://www.descope.com/product" rel="noopener noreferrer"&gt;CIAM platform&lt;/a&gt; to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single sign-on&lt;/li&gt;
&lt;li&gt;Social login&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.descope.com/learn/post/rbac" rel="noopener noreferrer"&gt;Role-based access control (RBAC)&lt;/a&gt; to a Streamlit app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before diving into the integration process, ensure you understand the basics of &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; and &lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Streamlit&lt;/a&gt;. You also need a Descope account, but don't worry if you don't have one yet—you'll learn how to set one up shortly. The free tier will be sufficient for you to follow along.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/E3CG4hFbzBc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a project in Descope
&lt;/h2&gt;

&lt;p&gt;The first step is to connect your Streamlit app with Descope by creating a project in the Descope console.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope sign-up page&lt;/a&gt; and register for a free account (or log in if you already have one).&lt;/li&gt;
&lt;li&gt;From the top-left menu in the console, select &lt;strong&gt;+ Project&lt;/strong&gt; to create a new project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your project dashboard is the hub where you'll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create and manage projects&lt;/li&gt;
&lt;li&gt;Assign user roles&lt;/li&gt;
&lt;li&gt;Configure authentication flows&lt;/li&gt;
&lt;li&gt;Adjust project-level settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;&lt;strong&gt;Settings &amp;gt; Project&lt;/strong&gt;&lt;/a&gt;, you'll see your &lt;strong&gt;Project ID&lt;/strong&gt;. This is a unique identifier that links your Streamlit app to Descope.&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%2Fs57rmn3q7zb9ckkykj4n.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%2Fs57rmn3q7zb9ckkykj4n.png" alt="Descope console project settings" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; Never paste this ID directly into your code. Instead, store it securely as an environment variable or in &lt;code&gt;.streamlit/secrets.toml&lt;/code&gt;. Later in this tutorial, we'll show you how to use it safely inside your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Streamlit app
&lt;/h2&gt;

&lt;p&gt;With your Descope project set up, let's move to the Streamlit side. We'll start with a bare-bones app and then integrate authentication step by step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new directory for your app. Optionally, set up a Python virtual environment inside it.&lt;/li&gt;
&lt;li&gt;Install Streamlit with the following command:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;streamlit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In the new directory, create a file called &lt;code&gt;app.py&lt;/code&gt; and paste the following code:
&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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 is a demo app with Descope-powered authentication and SSO&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;You need a project ID to initialize Descope, so copy yours from the console settings, create a &lt;code&gt;.streamlit/secrets.toml&lt;/code&gt; file in the root directory of your app, and include the project ID, like this:&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%2F2axgsogkgsmkib3wzgpp.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%2F2axgsogkgsmkib3wzgpp.png" alt="Simple Streamlit app" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Image description](&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zm47gp82oqbzwm28qihp.png" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zm47gp82oqbzwm28qihp.png&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing OAuth social logins in Steamlit
&lt;/h2&gt;

&lt;p&gt;Before diving into enterprise SSO, let's start with something many users expect out of the box: &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social logins&lt;/a&gt;. OAuth providers like Google or GitHub let people sign in with accounts they already trust. This removes friction during signup and reduces the need to manage yet another password.&lt;/p&gt;

&lt;p&gt;By adding OAuth, your Streamlit app instantly feels more professional and user-friendly—and when paired with SSO later, you'll cover both casual and enterprise login scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Descope as a federated identity provider for Streamlit
&lt;/h3&gt;

&lt;p&gt;In this section, we walk you through setting up Descope as a federated &lt;a href="https://www.descope.com/learn/post/identity-provider" rel="noopener noreferrer"&gt;identity provider (IdP)&lt;/a&gt; for Streamlit and seamlessly integrating Google login with the application you created earlier.&lt;/p&gt;

&lt;p&gt;In the Descope console, go to: &lt;a href="https://app.descope.com/settings/authentication/social" rel="noopener noreferrer"&gt;&lt;strong&gt;Build &amp;gt; Authentication Methods &amp;gt; Social Login (OAuth/OIDC)&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you'll see a full list of supported providers. Select &lt;strong&gt;Google&lt;/strong&gt; for this example:&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%2Frs0miie8ppx6ss9eng6n.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%2Frs0miie8ppx6ss9eng6n.png" alt="Select Google social login" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Descope console provides an authentication account for all common social logins. There's also an option to specify a different account for authentication if you prefer.&lt;/p&gt;

&lt;p&gt;Next, configure the redirect URL. This is the location users will return to after completing authentication. Since we're working locally, set it to your app's address, &lt;code&gt;http://localhost:8501&lt;/code&gt;, in the configuration provided, like this:&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%2Fkf338tep822qh7k02fu9.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%2Fkf338tep822qh7k02fu9.png" alt="Add a redirect" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember, you can also &lt;a href="https://github.com/descope/python-sdk?tab=readme-ov-file#oauth" rel="noopener noreferrer"&gt;provide the redirect URL programmatically&lt;/a&gt; in your Streamlit code for added flexibility later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating Descope with your Streamlit app as its OAuth provider
&lt;/h3&gt;

&lt;p&gt;Now let's wire your app to Descope so it can trigger the OAuth login flow and handle the response.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install its &lt;a href="https://github.com/descope/python-sdk" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; to facilitate the interaction:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;descope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;You need a project ID to initialize Descope, so copy yours from the console settings, create a &lt;code&gt;.streamlit/secrets.toml&lt;/code&gt; file in the root directory of your app, and include the project ID, like this:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;DESCOPE_PROJECT_ID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"XXXXX"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Initialize Descope in your app:
&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;descope.descope_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DescopeClient&lt;/span&gt;

&lt;span class="n"&gt;DESCOPE_PROJECT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;descope_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DescopeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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 is a demo app with Descope-powered authentication and SSO&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;Create a "Sign in with Google" button that launches the OAuth flow using the &lt;code&gt;descope_client.oauth.start()&lt;/code&gt; method:
&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;# … collapsed repeated code
&lt;/span&gt;&lt;span class="n"&gt;descope_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DescopeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re not logged in, pls login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign In with Google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_container_width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;oauth_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8501&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth_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;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# Redirect to Google
&lt;/span&gt;        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;meta http-equiv=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; content=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0; url=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;unsafe_allow_html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;When clicked, this button triggers the OAuth flow. Descope generates a Google sign-in URL and redirects the user there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your Streamlit app should now look like this:&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;Sign In with Google&lt;/strong&gt; button, and you are redirected back to the app with a &lt;code&gt;code&lt;/code&gt; query parameter after successfully authenticating via Google. This flow is known as the &lt;a href="https://www.rfc-editor.org/rfc/rfc6749#section-1.3.1" rel="noopener noreferrer"&gt;authorization code flow&lt;/a&gt;. In the next step, you'll exchange the returned code for a token that authenticates your users.&lt;/p&gt;

&lt;p&gt;You can use the Streamlit &lt;code&gt;st.query_params&lt;/code&gt; method to capture the code from the URL, and the &lt;code&gt;descope_client.sso.exchange_token()&lt;/code&gt; method to trade it for a token. Along with the token, Descope also provides user data and a refresh token that renews the short-lived session token. By storing this information in Streamlit's &lt;code&gt;session_state&lt;/code&gt;, you can persist authentication details across multiple runs and conditionally display your app's content only when a valid token is present.&lt;/p&gt;

&lt;p&gt;To persist tokens and user data, update your code as follows:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;descope.descope_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DescopeClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;descope.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthException&lt;/span&gt;

&lt;span class="n"&gt;DESCOPE_PROJECT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;descope_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DescopeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;)&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;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# User is not logged in
&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;code&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Handle possible login
&lt;/span&gt;        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# Reset URL state
&lt;/span&gt;        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&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="c1"&gt;# Exchange code
&lt;/span&gt;            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;jwt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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;jwt_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;sessionToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh_token&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;jwt_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;refreshSessionToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt_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;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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rerun&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;AuthException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login failed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re not logged in, pls login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign In with Google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_container_width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;oauth_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8501&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth_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;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="c1"&gt;# Redirect to Google
&lt;/span&gt;            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;meta http-equiv=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; content=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0; url=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;unsafe_allow_html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# User is logged in
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;jwt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate_and_refresh_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refresh_token&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# Persist refreshed token
&lt;/span&gt;            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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;jwt_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;sessionToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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 is a demo app with Descope-powered authentication and SSO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subheader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome! you&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re logged in&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="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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name: &lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="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="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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Logout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# Log out user
&lt;/span&gt;            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rerun&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;AuthException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Log out user
&lt;/span&gt;        &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rerun&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, your app only displays content when a user is logged in. You've successfully implemented Streamlit authentication with OAuth, laying the foundation for full Streamlit SSO with enterprise providers like &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; in the next section.&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%2Fvh04ea3i8zx5zgnk0n28.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%2Fvh04ea3i8zx5zgnk0n28.png" alt="Authenticated Streamlit app" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing SAML SSO in Streamlit
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.descope.com/learn/post/saml" rel="noopener noreferrer"&gt;Security Assertion Markup Language (SAML)&lt;/a&gt; is an open standard for exchanging authentication and authorization data between an &lt;a href="https://docs.descope.com/manage/idpapplications/#identity-provider-idp-vs-service-provider-sp" rel="noopener noreferrer"&gt;IdP&lt;/a&gt; and a service provider (SP). In this setup, platforms like &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; act as the IdP, while your Streamlit app functions as the SP.&lt;/p&gt;

&lt;p&gt;SAML SSO allows users to access multiple applications with a single login. This is especially valuable for organizations managing many apps, since it reduces friction for users and administrators alike.&lt;/p&gt;

&lt;p&gt;In the SSO model, organizations that use your Streamlit app are represented as tenants. Each tenant groups users, permissions, and related configurations, giving you a way to handle enterprise customers with their own SSO setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Okta as the IdP and Descope as the SP
&lt;/h3&gt;

&lt;p&gt;For this example, you'll use &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; as the IdP while Descope acts as the SP that integrates with your Streamlit app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Descope console, create a new tenant: Go to the &lt;a href="https://app.descope.com/tenants" rel="noopener noreferrer"&gt;&lt;strong&gt;Tenants&lt;/strong&gt;&lt;/a&gt; page and click &lt;strong&gt;+ Tenant&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%2Fooureetum8ja9w3xhima.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%2Fooureetum8ja9w3xhima.png" alt="Create a Descope tenant" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the tenant &lt;strong&gt;Authentication Methods&lt;/strong&gt; section, choose the &lt;strong&gt;SAML&lt;/strong&gt; protocol. Enter your email domain in the &lt;strong&gt;SSO Domains&lt;/strong&gt; field under &lt;strong&gt;Tenant Details&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%2Fqfif34mla1rhmftgfi8k.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%2Fqfif34mla1rhmftgfi8k.png" alt="Select an SSO Protocol" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because Descope mediates communication between Okta and your app, the two need a secure, trusted connection. This connection is established by exchanging metadata XML documents between both platforms. The good news: Descope's built-in Okta integration makes this process much simpler.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your Okta account and click &lt;strong&gt;Admin&lt;/strong&gt; to access your admin dashboard:&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%2Fo5skie2kk74hlmvj8n2n.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%2Fo5skie2kk74hlmvj8n2n.png" alt="Okta Dashboard" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Applications &amp;gt; Applications&lt;/strong&gt; and click &lt;strong&gt;Browse App Catalog&lt;/strong&gt; to explore a comprehensive list of available integrations:&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%2Fe1d1rqeyw9s3qtdby36g.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%2Fe1d1rqeyw9s3qtdby36g.png" alt="Okta Admin Dashboard" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search for &lt;strong&gt;Descope&lt;/strong&gt; in the catalog and add the integration. This creates a new Okta app automatically.&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%2F0n1af7xgmcl1vl0dcs10.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%2F0n1af7xgmcl1vl0dcs10.png" alt="Add Okta Descope integration" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;General Settings&lt;/strong&gt; tab, provide a label for your new Okta app.&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%2Ftqdvuwwfrmbxk75pcvgq.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%2Ftqdvuwwfrmbxk75pcvgq.png" alt="Okta ap general settings" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch to the &lt;strong&gt;Assignments&lt;/strong&gt; tab and assign users who should have access to your Streamlit app via SSO:&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%2Fe4rkutyyb87ihcalrywb.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%2Fe4rkutyyb87ihcalrywb.png" alt="Assign users to the Okta app" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch to the &lt;strong&gt;Sign-On Options&lt;/strong&gt; tab and select &lt;strong&gt;SAML 2.0&lt;/strong&gt;. Copy the &lt;strong&gt;Metadata URL&lt;/strong&gt; then paste it into your tenant's SSO configuration in Descope. Next, copy the &lt;strong&gt;ACS URL&lt;/strong&gt; and &lt;strong&gt;Entity ID&lt;/strong&gt; from Descope into the &lt;strong&gt;Advanced Sign-on Settings&lt;/strong&gt; in Okta:&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%2F3ff700su39c5oo4v5e6m.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%2F3ff700su39c5oo4v5e6m.png" alt="Okta Advanced Sign-on Settings" width="786" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Expand the &lt;strong&gt;Attribute Statements&lt;/strong&gt; section in Okta. Map the following values:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;firstName&lt;/code&gt; &amp;gt; &lt;code&gt;user.firstName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lastName&lt;/code&gt; &amp;gt; &lt;code&gt;user.lastName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; &amp;gt; &lt;code&gt;user.email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uid&lt;/code&gt; &amp;gt; &lt;code&gt;user.id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These mappings define how user attributes are passed from Okta to Descope.&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%2Fbd4s1fuh5fmbgd1smyqs.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%2Fbd4s1fuh5fmbgd1smyqs.png" alt="Update Okta attribute mapping" width="775" height="909"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back in Descope, update your tenant's &lt;strong&gt;SSO Mapping&lt;/strong&gt; settings so attributes from Okta align correctly with Descope's schema.&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%2F4jibkgf99alabd64p7v4.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%2F4jibkgf99alabd64p7v4.png" alt="Descope SSO mapping" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once this is complete, you've established a working SAML SSO configuration between Okta, Descope, and your Streamlit app.&lt;/p&gt;

&lt;p&gt;It's worth noting that Descope also provides a &lt;a href="https://www.descope.com/blog/post/sso-setup-suite" rel="noopener noreferrer"&gt;self-service SSO configuration&lt;/a&gt; designed for multi-tenant environments. With this setup, each tenant can maintain its own SSO configuration, and tenant admins can manage their Descope SSO settings directly through your application.&lt;/p&gt;

&lt;p&gt;To enable this, you can either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embed the out-of-the-box &lt;code&gt;sso-config&lt;/code&gt; flow into your app's admin interface, or&lt;/li&gt;
&lt;li&gt;Generate a configuration link that tenant admins can use directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For detailed implementation guidance, refer to the &lt;a href="https://docs.descope.com/knowledgebase/sso/selfserviceregistration" rel="noopener noreferrer"&gt;Descope documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the SSO flow in Streamlit
&lt;/h3&gt;

&lt;p&gt;Now that you've configured SAML SSO between Okta and Descope, the next step is to implement the flow inside your Streamlit app. This will let users authenticate through Okta and seamlessly return to your app. So let's add another button under the &lt;strong&gt;Sign In with Google&lt;/strong&gt; button that starts the SSO flow when clicked.&lt;/p&gt;

&lt;p&gt;First, you'll need your tenant ID from the Descope console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to your tenant's settings.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Tenant ID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add it to your &lt;code&gt;.streamlit/secrets.toml&lt;/code&gt; file with the &lt;code&gt;DESCOPE_TENANT_ID&lt;/code&gt; key.&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;st.container&lt;/code&gt; element in your code like 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="c1"&gt;# … collapsed repeated code
&lt;/span&gt;&lt;span class="n"&gt;TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_TENANT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Get tenant ID from secret
# …
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# … google button here
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign in with SSO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_container_width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;sso_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8501&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sso_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;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="c1"&gt;# Redirect to Okta
&lt;/span&gt;            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;meta http-equiv=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; content=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0; url=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;unsafe_allow_html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Streamlit app should now look like this:&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%2F6a6jdnxlacno1jgno1o6.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%2F6a6jdnxlacno1jgno1o6.png" alt="Streamlit app final login look" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Sign In with SSO&lt;/strong&gt; to go to Okta for verification. Sign in with one of the users you assigned earlier. After successful authentication, you're redirected back to your Streamlit app with a &lt;code&gt;code&lt;/code&gt; query parameter—just like in the OAuth flow. Since you already implemented the code exchange, you'll be signed in automatically:&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%2Fgyc1oiumuz8i27bo49xz.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%2Fgyc1oiumuz8i27bo49xz.png" alt="Streamlit SSO authenticated view" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing authorization and user privileges
&lt;/h3&gt;

&lt;p&gt;While authentication verifies the user's identity, authorization controls their access to resources and features based on predefined roles and permissions. The Descope console includes tools to manage user roles and permissions so you can enforce access control in your Streamlit app. After login, Descope returns role information you can use to tailor the UI and gate functionality.&lt;/p&gt;

&lt;p&gt;All roles and permissions live under the &lt;a href="https://app.descope.com/authorization" rel="noopener noreferrer"&gt;&lt;strong&gt;Authorization&lt;/strong&gt;&lt;/a&gt; page. A &lt;strong&gt;Tenant Admin&lt;/strong&gt; role is created by default. Let's assign that role to a user and then conditionally display admin-only content in the app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to the &lt;a href="https://app.descope.com/users" rel="noopener noreferrer"&gt;&lt;strong&gt;Users&lt;/strong&gt;&lt;/a&gt; page, click the kebab menu of the user you want to update, and click &lt;strong&gt;Edit&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%2F9rp5lz4ep5n4akt7j9w5.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%2F9rp5lz4ep5n4akt7j9w5.png" alt="Edit Descope user" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the modal, select &lt;strong&gt;Tenant Admin&lt;/strong&gt; in the &lt;strong&gt;Roles&lt;/strong&gt; field and click &lt;strong&gt;Save&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%2Frjhh1pm9km5habv9n7l3.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%2Frjhh1pm9km5habv9n7l3.png" alt="Assign admin role to a user" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update your code to show an admin indicator:
&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;# … collapsed repeated code
&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;user&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# …
&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;Tenant Admin&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;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roleNames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="c1"&gt;# Show admin-specific content
&lt;/span&gt;                &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ADMIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🤖&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should now be the view of the admin user when they're signed 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%2Fq16mmzbg7oqt8v7kzmvs.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%2Fq16mmzbg7oqt8v7kzmvs.png" alt="Admin view" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach ensures that only authorized users see privileged features, improving both the security and integrity of your app.&lt;/p&gt;

&lt;p&gt;You can find the full code on &lt;a href="https://github.com/iamgideonidoko/streamlit-auth-sso" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streamlit SSO and authentication with Descope
&lt;/h2&gt;

&lt;p&gt;You just wired Streamlit SSO with Descope end to end: created a Descope project, added OAuth social login, implemented SAML SSO with Okta, and gated features with roles. You now have a secure pattern you can reuse across Streamlit apps, from personal dashboards to multi-tenant products.&lt;/p&gt;

&lt;p&gt;Ready to keep going? Use your &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Free Forever account&lt;/a&gt; to extend your app with &lt;a href="https://www.descope.com/learn/post/passwordless-authentication" rel="noopener noreferrer"&gt;passwordless&lt;/a&gt; options, &lt;a href="https://www.descope.com/learn/post/adaptive-authentication" rel="noopener noreferrer"&gt;adaptive MFA&lt;/a&gt;, and &lt;a href="https://www.descope.com/learn/post/scim" rel="noopener noreferrer"&gt;SCIM provisioning&lt;/a&gt;. Have questions or edge cases to discuss? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book time&lt;/a&gt; with the team for a focused walkthrough.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Next.js 14 Authentication and RBAC with App Router</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:44:55 +0000</pubDate>
      <link>https://dev.to/descope/nextjs-14-authentication-and-rbac-with-app-router-192l</link>
      <guid>https://dev.to/descope/nextjs-14-authentication-and-rbac-with-app-router-192l</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-nextjs14-app-router" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ensuring your application is secure through resilient authentication and authorization mechanisms is crucial in the &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js 14&lt;/a&gt; development process. This helps to ensure that only authenticated users can access the protected resources of your application and that each user can access only the resources they are allowed to access.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how to implement Next.js 14 authentication and role-based access control (RBAC) using the &lt;a href="https://nextjs.org/docs#app-router-vs-pages-router" rel="noopener noreferrer"&gt;App Router&lt;/a&gt; and Descope. Whether you're building a new application or enhancing an existing one, this guide will equip you with the skills to create secure login and access controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set up Descope authentication in Next.js 14&lt;/li&gt;
&lt;li&gt;Implement magic link login flows&lt;/li&gt;
&lt;li&gt;Add role-based authorization (RBAC)&lt;/li&gt;
&lt;li&gt;Protect routes with middleware&lt;/li&gt;
&lt;li&gt;Manage user sessions and tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding Next.js 14 authentication and RBAC
&lt;/h2&gt;

&lt;p&gt;Before implementing Next.js 14 authentication, let's clarify the core concepts you'll work with in this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication vs. authorization
&lt;/h2&gt;

&lt;p&gt;Both authentication and authorization are integral to developing a secure Next.js 14 application, but they serve distinct purposes. Authentication is the process of verifying a user’s identity and is typically achieved through a set of login credentials, such as email and passwords, magic links, passkeys or &lt;a href="https://www.descope.com/learn/post/authentication-types" rel="noopener noreferrer"&gt;other auth methods&lt;/a&gt;. &lt;a href="https://www.youtube.com/watch?v=qprypVZ6Pxo" rel="noopener noreferrer"&gt;Authorization&lt;/a&gt; determines whether the authenticated user is allowed to access specific resources or perform certain actions. This tutorial implements both using Next.js 14 with App Router.&lt;/p&gt;

&lt;p&gt;Want to learn more about the basics? See our complete guide: &lt;a href="https://www.descope.com/learn/post/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs. Authorization: What's the Difference?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  RBAC in this tutorial
&lt;/h2&gt;

&lt;p&gt;This tutorial uses role-based access control (RBAC) to manage who can do what in the blogging app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Editor role:&lt;/strong&gt; Can create and edit posts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin role:&lt;/strong&gt; Can view and publish posts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of assigning permissions to individual users, you'll assign them to roles. Then you’ll assign users to roles. This makes managing permissions scalable as your app grows. To learn more about RBAC concepts, benefits, and implementation strategies, see: &lt;a href="https://www.descope.com/learn/post/rbac" rel="noopener noreferrer"&gt;What Is RBAC: Your Simple Guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js 14 and App Router
&lt;/h2&gt;

&lt;p&gt;Next.js 14 supports both client-side rendering (CSR) and server-side rendering (SSR), each with different implications for authentication and authorization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side rendering (CSR):&lt;/strong&gt; The client renders content after the initial page load. This can cause a brief flash of unauthorized content before auth state is verified. You can improve UX with loading indicators or skeleton screens during auth checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-side rendering (SSR):&lt;/strong&gt; The server handles rendering before sending the content to the client. This approach prevents unauthorized content flashes since the server clocks the request until it verifies the auth state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comparing frameworks? See our comparison: &lt;a href="https://www.descope.com/blog/post/nextjs-vs-reactjs-vs-sveltekit" rel="noopener noreferrer"&gt;Svelte vs Next.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tutorial uses SSR with App Router, Next.js 14's file-system-based routing solution, to implement authentication that verifies on the server before rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Descope for Next.js 14 authentication
&lt;/h2&gt;

&lt;p&gt;Descope simplifies adding Next.js 14 authentication and authorization by offering an intuitive SDK and visual flows to build authentication screens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt; is a visual no-code interface to build screens and authentication flows for common user interactions with your application, such as login, sign-up, user invites, and &lt;a href="https://www.descope.com/learn/post/mfa" rel="noopener noreferrer"&gt;multi-factor authentication (MFA)&lt;/a&gt;. This feature abstracts away the implementation details of authentication methods, session management, and error handling, allowing you to focus on building the core features of your application rather than handling these complexities.&lt;/p&gt;

&lt;p&gt;Using Descope eliminates the need to write authentication and authorization logic from scratch, saving valuable development time and reducing the risk of security vulnerabilities. Descope's infrastructure ensures your application's authentication and authorization mechanisms are secure, scalable, and easy to maintain.&lt;/p&gt;

&lt;p&gt;The following sections explain how you can implement these features in a Next.js application using Descope. To follow along, you need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/download" rel="noopener noreferrer"&gt;Node.js v18&lt;/a&gt; installed &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/book/en/v2/Getting-Started-Installing-Git" rel="noopener noreferrer"&gt;Git CLI&lt;/a&gt; installed &lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;A Free Forever Descope account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/MNvspRb_iUY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Descope project
&lt;/h2&gt;

&lt;p&gt;To demonstrate Next.js 14 authentication with RBAC, you'll build a simple blogging application using the editor and admin roles described above.&lt;/p&gt;

&lt;p&gt;To begin, you’ll need to create a new project. On the &lt;a href="https://app.descope.com/login" rel="noopener noreferrer"&gt;Descope Console&lt;/a&gt;, create a new project with the name &lt;code&gt;descope-nextjs-auth-rbac&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%2Foo8ra9pzbw1ugn71gjbt.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%2Foo8ra9pzbw1ugn71gjbt.png" alt="Creating a new project" width="606" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Navigate to the project setup. Select &lt;code&gt;Consumers&lt;/code&gt; under &lt;code&gt;Who uses your application?&lt;/code&gt; and then click &lt;code&gt;Next&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%2Fecwviz8te4x0r12vh3cx.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%2Fecwviz8te4x0r12vh3cx.png" alt="Selecting the target audience.png" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;code&gt;Magic Link&lt;/code&gt; for &lt;code&gt;Which authentication methods do you want to use?&lt;/code&gt; and then click &lt;code&gt;Next&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%2Fyishg8erckahgp3ren81.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%2Fyishg8erckahgp3ren81.png" alt="Selecting the authentication method" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Skip the MFA method step and click &lt;code&gt;Go ahead without MFA&lt;/code&gt;. You can always set this up later:&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%2F01gncgkx6sjcd6uf02in.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%2F01gncgkx6sjcd6uf02in.png" alt="Skipping the MFA method step" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the next page, you can view the flows generated for your project. Click &lt;code&gt;Next&lt;/code&gt; to generate these flows:&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%2F5h2d1x0knjnmrrphm3en.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%2F5h2d1x0knjnmrrphm3en.png" alt="The generated flows" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the flows are generated, select &lt;code&gt;Project&lt;/code&gt; from the sidebar and take note of your project ID, which you’ll use in the next step:&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%2Fc15479s0by6npkrlst1k.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%2Fc15479s0by6npkrlst1k.png" alt="Obtaining the project ID" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, you need to obtain a management key. Select &lt;code&gt;Company&lt;/code&gt; from the sidebar, select the &lt;code&gt;Management Keys&lt;/code&gt; tab on the &lt;code&gt;Company&lt;/code&gt; page, and click the &lt;code&gt;+ Management Key&lt;/code&gt; button to create a new management key. Provide the key name and, under &lt;code&gt;Project Assignment&lt;/code&gt;, select &lt;code&gt;Use this management key for all the projects in the company&lt;/code&gt;. Click the &lt;code&gt;Generate Key&lt;/code&gt; button and copy the value of your key:&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%2Fona3r2g9dwoizqbwevpc.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%2Fona3r2g9dwoizqbwevpc.png" alt="Generating a management key" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have the key, you can implement authentication for the Next.js application. You will come back to the console to set up the RBAC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring the starter template
&lt;/h2&gt;

&lt;p&gt;This walkthrough uses a simple web app shell. To keep this guide focused on authentication and authorization, we've prepared a starter template for the blogging application. In this section, you'll clone and set up the template.&lt;/p&gt;

&lt;p&gt;To clone the template to your local machine, execute the following command in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git clone --single-branch -b starter-template https://github.com/kimanikevin254/descope-nextjs-auth-rbac.git&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Navigate into the project directory and install all the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd descope-nextjs-auth-rbac &amp;amp;&amp;amp; npm i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; project in the root folder and add the following content, which defines the location of the SQLite database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL="file:./dev.db"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;Run a &lt;a href="https://www.prisma.io/migrate" rel="noopener noreferrer"&gt;Prisma migration&lt;/a&gt; for the models defined in the &lt;code&gt;prisma/schema.prisma&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx prisma migrate dev --name init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;Run the app with the command &lt;code&gt;npm run dev&lt;/code&gt; and navigate to &lt;code&gt;http://localhost:3000/&lt;/code&gt; on your web browser. You should see a dashboard where the posts are displayed. At the moment, no posts are available:&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%2F8q8cfx5yru7zcpyrxr33.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%2F8q8cfx5yru7zcpyrxr33.png" alt="Dashboard" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users are able to write posts by clicking the &lt;code&gt;Start Writing&lt;/code&gt; button, which directs them to the &lt;code&gt;Write a Post&lt;/code&gt; page that has a rich text editor:&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%2Fmidhrgzsekiq5w6lkg0l.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%2Fmidhrgzsekiq5w6lkg0l.png" alt="Write a post" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this stage, the template is ready, but don’t create any posts until you have implemented authentication and authorization.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding authentication to Next.js 14
&lt;/h2&gt;

&lt;p&gt;This section walks you through implementing authentication in your Next.js 14 application using Descope. You'll use code examples and screenshots to guide each step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Install the Descope Next.js SDK&lt;/li&gt;
&lt;li&gt;Wrap your app with Descope authentication&lt;/li&gt;
&lt;li&gt;Configure the “sign up or in” function&lt;/li&gt;
&lt;li&gt;Protect routes with authentication middleware&lt;/li&gt;
&lt;li&gt;Test the authentication flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get started, execute the following command in the terminal to install the &lt;a href="https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk" rel="noopener noreferrer"&gt;Descope Next.js SDK&lt;/a&gt;, which you’ll use to implement authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i @descope/nextjs-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/layout.js&lt;/code&gt; file and replace the existing code with the following to wrap the whole application with the Descope Auth Provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Inter } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@descope/nextjs-sdk";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
   title: "Descope - Next.js Auth",
   description:
       "Demonstrating how to add auth &amp;amp; RBAC to Next.js 14 with Descope",
};

export default function RootLayout({ children }) {
   return (
       &amp;lt;AuthProvider projectId={process.env.DESCOPE_PROJECT_ID}&amp;gt;
           &amp;lt;html lang="en"&amp;gt;
               &amp;lt;body
                   className={`${inter.className} max-w-screen-lg mx-auto py-4`}
               &amp;gt;
                   {children}
               &amp;lt;/body&amp;gt;
           &amp;lt;/html&amp;gt;
       &amp;lt;/AuthProvider&amp;gt;
   );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;You will set the value of &lt;code&gt;DESCOPE_PROJECT_ID&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; file later on.&lt;/p&gt;

&lt;p&gt;You can use the &lt;code&gt;sign-up-or-in&lt;/code&gt; flow that you generated when configuring the project to allow the user to sign in to the application. The &lt;code&gt;sign-up-or-in&lt;/code&gt; flow presents the user with a &lt;code&gt;Welcome&lt;/code&gt; screen where they are prompted to provide their email address.&lt;/p&gt;

&lt;p&gt;Once the user provides the email address and clicks the &lt;code&gt;Continue&lt;/code&gt; button, a magic link is sent to the provided email address. Once the user clicks the magic link, Descope verifies the link, and the user is authenticated. The flow then checks if the user is new or returning. If the user is new, they are prompted to provide additional information(their name), and their details are updated.&lt;/p&gt;

&lt;p&gt;Here’s a visual representation of the flow in practice:&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%2Fy02zqrgjilhkojr61h5y.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%2Fy02zqrgjilhkojr61h5y.png" alt="Descope sign-up-or-in flow" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/sign-in/page.js&lt;/code&gt; file and replace the existing code with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import { Descope } from "@descope/nextjs-sdk";
import axios from "axios";
import { useRouter } from "next/navigation";

export default function Page() {
   const router = useRouter();

   // Register user or redirect to home
   const handleEvent = async (event) =&amp;gt; {
       try {
           if (event.detail.firstSeen !== true) {
               return router.replace("/");
           }

           // Register the user
           const { data } = await axios.post("/api/register", {
               descopeUserId: event.detail.user.userId,
               email: event.detail.user.email,
               name: event.detail.user.name,
           });

           if (data.error) {
               alert("Something went wrong");
           } else {
               return router.replace("/");
           }
       } catch (error) {
           console.log(error);
       }
   };
   return (
       &amp;lt;Descope
           flowId="sign-up-or-in"
           onSuccess={(e) =&amp;gt; handleEvent(e)}
           onError={(e) =&amp;gt; alert("Something went wrong. Please try again.")}
       /&amp;gt;
   );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;In the preceding code, once the user signs up or in successfully, the event data returned from this component is passed to the &lt;code&gt;handleEvent&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;This function checks if the user is new by examining &lt;code&gt;event.details.firstSeen&lt;/code&gt;. If the user is not new, they are redirected to the home screen. Otherwise, it sends a POST request to the &lt;code&gt;/api/register&lt;/code&gt; endpoint with the user’s details to register the user. If the registration is successful, the user is redirected to the home page; otherwise, an error message is displayed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/api/register&lt;/code&gt; endpoint is defined in the &lt;code&gt;app/api/register/route.js&lt;/code&gt; files, and it adds the user to the local database.&lt;/p&gt;

&lt;p&gt;You also need to set up a middleware to enforce authentication for all the pages in this application. Create a file named &lt;code&gt;middleware.js&lt;/code&gt; in the project root folder and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { authMiddleware } from "@descope/nextjs-sdk/server";

export default authMiddleware({
   projectId: process.env.DESCOPE_PROJECT_ID,
   redirectUrl: process.env.SIGN_IN_ROUTE,
});

export const config = {
   matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;This code uses the &lt;code&gt;authMiddleware&lt;/code&gt; function provided by the Descope Next.js SDK to protect all routes and redirect unauthenticated users to the sign-in page.&lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;.env&lt;/code&gt; file with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DESCOPE_PROJECT_ID=&amp;lt;YOUR-PROJECT-ID&amp;gt;
DESCOPE_MANAGEMENT_KEY=&amp;lt;YOUR-MANAGEMENT-KEY&amp;gt;

SIGN_IN_ROUTE="/sign-in"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Make sure to replace the placeholder values with the values you obtained earlier.&lt;/p&gt;

&lt;p&gt;The authentication for your Next.js application is now complete.&lt;/p&gt;

&lt;p&gt;Before you test it, open the &lt;code&gt;app/write/page.js&lt;/code&gt; file. In this file, notice that the &lt;code&gt;savePost&lt;/code&gt; function requires the Descope user ID. You can retrieve this using the hooks provided by the Next.js SDK.&lt;/p&gt;

&lt;p&gt;Add the following import statement to the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Add the following statement that retrieves the user just before the &lt;code&gt;savePost&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { user } = useUser();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;You can now test the authentication flow.&lt;/p&gt;

&lt;p&gt;On your browser, navigate to &lt;code&gt;http://localhost:3000&lt;/code&gt;. You are redirected to &lt;code&gt;http://localhost:3000/sign-in&lt;/code&gt; since you have not signed in. It should look like this:&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%2F0sqlhfk5xqj2u2li4q6i.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%2F0sqlhfk5xqj2u2li4q6i.png" alt="Sign-in page" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Provide your email address, click the link sent to your inbox, and provide your name since this is the first time you’re signing in. Then you are redirected to the home 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%2Fnvbgq26a3oultemhtwzt.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%2Fnvbgq26a3oultemhtwzt.png" alt="Home page" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirms that the authentication is working as expected.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding authorization to Next.js 14
&lt;/h2&gt;

&lt;p&gt;Currently, anyone can log in to the application, create posts, submit them for approval, and approve the posts. For this example, you want to specify that editors can write posts and then submit them for approval, and admins can publish the posts.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create roles and assign permissions in Descope&lt;/li&gt;
&lt;li&gt;Control UI visibility based on user roles&lt;/li&gt;
&lt;li&gt;Validate roles in API routes&lt;/li&gt;
&lt;li&gt;Configure session tokens for authorization&lt;/li&gt;
&lt;li&gt;Test role-based permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you implement this functionality, click the &lt;code&gt;Start Writing&lt;/code&gt; button and create a few posts to test the application with:&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%2Fyn9k4atb7lkzwryyzf73.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%2Fyn9k4atb7lkzwryyzf73.png" alt="Home page with some posts" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to the &lt;code&gt;Descope Console&lt;/code&gt;, select &lt;code&gt;Authorization&lt;/code&gt; from the sidebar, and click the + Role button. In the &lt;code&gt;Add Role&lt;/code&gt; modal, provide “editor” as the name and “Can write posts and submit them for approval” as the description. Then click the &lt;code&gt;Add&lt;/code&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%2F41x7nqqvhxufwznec8vi.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%2F41x7nqqvhxufwznec8vi.png" alt="Adding a role" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repeat the process to create a role for the admin. Provide “admin” as the role and “Can toggle a post’s published status” as the description.&lt;/p&gt;

&lt;p&gt;Assign the “editor” role to the user who is logged in to the application. In the &lt;code&gt;Descope Console&lt;/code&gt;, select &lt;code&gt;Users&lt;/code&gt; from the sidebar to edit the user details:&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%2Fwibu1n9wf5u8ycvmrlk6.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%2Fwibu1n9wf5u8ycvmrlk6.png" alt="Editing user details" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the user details modal, select &lt;code&gt;+ Add Tenant / Role&lt;/code&gt;, assign the &lt;code&gt;editor&lt;/code&gt; role, and click &lt;code&gt;Save&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%2F8g581f7ave7ftbxkaeur.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%2F8g581f7ave7ftbxkaeur.png" alt="Assigning editor role" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/page.js&lt;/code&gt; file and add the following import statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Add the following code before the &lt;code&gt;fetchPosts&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { user } = useUser();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;This hook allows you to retrieve the user’s details.&lt;/p&gt;

&lt;p&gt;Locate the following lines of code in the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Link
   href={"/write"}
   className="px-4 py-1 border rounded-lg bg-black text-white"
&amp;gt;
   Start Writing
&amp;lt;/Link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Replace it with the following to display only the &lt;code&gt;Start Writing&lt;/code&gt; button to editors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   user?.roleNames?.includes("editor") &amp;amp;&amp;amp; (
       &amp;lt;Link
           href={"/write"}
           className="px-4 py-1 border rounded-lg bg-black text-white"
       &amp;gt;
           Start Writing
       &amp;lt;/Link&amp;gt;
   )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/posts/[postId]/page.js&lt;/code&gt; file and add the following lines of code in their respective locations (indicated by the comments):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useUser } from "@descope/nextjs-sdk/client"; // After the import statement

const { user } = useUser(); // Before the return statement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Locate the following lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Button
   variant="default"
   className="px-6 mt-6"
   onClick={() =&amp;gt; togglePublishedStatus()}
&amp;gt;
   {post.published ? "Unpublish" : "Publish"}
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Replace it with the following to specify that only admins can publish/unpublish a post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   user?.roleNames?.includes("admin") &amp;amp;&amp;amp; (
       &amp;lt;Button
           variant="default"
           className="px-6 mt-6"
           onClick={() =&amp;gt; togglePublishedStatus()}
       &amp;gt;
           {post.published ? "Unpublish" : "Publish"}
       &amp;lt;/Button&amp;gt;
   )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;For additional security, you also validate these roles in the API router handlers. In the lib folder, create a new file named &lt;code&gt;descope.js&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { createSdk } from "@descope/nextjs-sdk/server";

export const descopeSdk = createSdk({
   projectId: process.env.DESCOPE_PROJECT_ID,
   managementKey: process.env.DESCOPE_MANAGEMENT_KEY,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;This code initializes a Descope client instance and exports it for use in other parts of the application.&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/api/posts/create/route.js&lt;/code&gt; file and add the following code just after &lt;code&gt;const data = await request.json()&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;// Make sure the user has the editor role
const userRoles = descopeSdk.getJwtRoles(data.sessionToken);

if (!userRoles?.includes("editor")) {
   throw new Error("User is not an editor");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;This code ensures that the user has the editor role. If not, it throws an error.&lt;/p&gt;

&lt;p&gt;Remember to add the following import statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { descopeSdk } from "@/lib/descope";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/api/posts/toggleStatus/route.js&lt;/code&gt; file and add the following code just after &lt;code&gt;const data = await request.json()&lt;/code&gt; to throw an error if the user making the request does not have the admin role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Make sure the user has the admin role
const userRoles = descopeSdk.getJwtRoles(data.sessionToken);

if (!userRoles?.includes("admin")) {
   throw new Error("User is not an admin");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Remember to add the following &lt;code&gt;import&lt;/code&gt; statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { descopeSdk } from "@/lib/descope";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;With the route handlers verifying the user roles, you need to make sure that the session token is passed in the request body. Start by opening the &lt;code&gt;app/write/page.js&lt;/code&gt; file and retrieving the session token using the &lt;code&gt;useSession&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { sessionToken } = useSession();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Make sure the &lt;code&gt;useSession&lt;/code&gt; hook is imported into the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useSession, useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Add the retrieved session token to the body of the POST request in the &lt;strong&gt;savePost&lt;/strong&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { data } = await axios.post("/api/posts/create", {
   title,
   content,
   descopeUserId: user?.userId,
   sessionToken,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/posts/[postId]/page.js&lt;/code&gt; file and retrieve the session token using the &lt;code&gt;useSession&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { sessionToken } = useSession();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Make sure the hook is imported into the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useSession, useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;togglePublishedStatus&lt;/code&gt; function with the following code that ensures that the session token is passed to the route handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const togglePublishedStatus = async () =&amp;gt; {
   try {
       const { data } = await axios.put("/api/posts/toggleStatus", {
           postId: params.postId,
           sessionToken,
       });

       setPost(data.data);
   } catch (error) {
       alert("Something went wrong");
       console.log(error);
   }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all the authorization checks are complete. You can test to see if everything is working as expected.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://localhost:3000/sign-in&lt;/code&gt; and log in to the application. Since you assigned the editor role to the user, you can see the &lt;code&gt;Start Writing&lt;/code&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%2Fzzqzhg39titeu5dvwm3n.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%2Fzzqzhg39titeu5dvwm3n.png" alt="Logged in as an editor" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on a post to open the details page. However, you cannot see the &lt;code&gt;Publish/Unpublish&lt;/code&gt; button since only admins are allowed to see it:&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%2F0ddtz90p0jtgkktbyoiu.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%2F0ddtz90p0jtgkktbyoiu.png" alt="Post details page as an editor" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, go back to the &lt;code&gt;Descope Console&lt;/code&gt; and assign the user the &lt;code&gt;admin&lt;/code&gt; role.&lt;/p&gt;

&lt;p&gt;Go back to the application and refresh the page. Since the user now has the admin role, they can see the button to &lt;code&gt;Publish/Unpublish&lt;/code&gt; a post:&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%2Fm96cem3nwt3zx6zr3q8e.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%2Fm96cem3nwt3zx6zr3q8e.png" alt="Post details page as an admin" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirms that the authorization flow is working as expected.&lt;/p&gt;

&lt;p&gt;You can access the complete &lt;a href="https://github.com/kimanikevin254/descope-nextjs-auth-rbac/tree/main" rel="noopener noreferrer"&gt;Next.js 14 authentication code&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps for Next.js 14 authentication
&lt;/h2&gt;

&lt;p&gt;Your authentication system is production-ready. You can extend it by adding &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social logins&lt;/a&gt;, MFA, or additional roles—all without writing custom auth logic.&lt;/p&gt;

&lt;p&gt;This flexibility is what makes Descope's &lt;a href="https://www.descope.com/product" rel="noopener noreferrer"&gt;CIAM platform&lt;/a&gt; powerful for production applications. Use no-code workflows to add authentication methods, configure authorization rules, and manage users at scale, so you can focus on your application's core features instead of authentication infrastructure.&lt;/p&gt;

&lt;p&gt;Start building with a &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Free Forever account&lt;/a&gt; today. Have questions about implementing RBAC or Next.js authentication? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book time with our experts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Adding Authentication and SSO to a Reflex App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 27 Mar 2026 19:56:05 +0000</pubDate>
      <link>https://dev.to/descope/adding-authentication-and-sso-to-a-reflex-app-2ahk</link>
      <guid>https://dev.to/descope/adding-authentication-and-sso-to-a-reflex-app-2ahk</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/reflex-auth-sso" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reflex.dev/" rel="noopener noreferrer"&gt;Reflex&lt;/a&gt; is an open-source full-stack web framework written in and for Python that lets you build both frontend UI and backend logic in pure Python, i.e., you don’t need to manually write JavaScript or separately manage a React/Vue frontend and a separate backend.&lt;/p&gt;

&lt;p&gt;By adding Descope to your Reflex application, you can immediately take advantage of a full-fledged identity platform—complete with passwordless authentication, MFA, passkeys, social login, enterprise SSO, and customizable user journeys. &lt;/p&gt;

&lt;p&gt;This lets you keep writing your entire Reflex project in Python while relying on Descope to seamlessly deliver the advanced authentication and security capabilities needed for real-world, scalable applications.&lt;/p&gt;

&lt;p&gt;Let’s begin!&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;It’s helpful to familiarize yourself with &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; and &lt;a href="https://reflex.dev/" rel="noopener noreferrer"&gt;Reflex&lt;/a&gt;. You’ll also need a &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; account, but don’t worry if you don’t have one yet—you’ll learn how to set one up shortly. The free tier will be sufficient for you to follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Descope Project
&lt;/h2&gt;

&lt;p&gt;The first step in connecting your Reflex app to Descope is to create a new Project in the Descope Console.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to the &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope sign-up page&lt;/a&gt; and register for a free account (or log in if you already have one).&lt;/li&gt;
&lt;li&gt;From the top-left menu in the console, click &lt;strong&gt;+ Project&lt;/strong&gt; to set up a new project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;Settings &amp;gt; Project&lt;/a&gt;, you’ll find your Project ID, which is a unique identifier used to link your Reflex app to Descope.&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%2F0gnyw4714qeof6n58ttb.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%2F0gnyw4714qeof6n58ttb.png" alt="Descope console project settings" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; Never paste this ID directly into your code. Instead, store it securely as an environment variable in a &lt;code&gt;.env&lt;/code&gt; file. We’ll configure this later in the blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Reflex app
&lt;/h2&gt;

&lt;p&gt;With your Descope Project set up, let’s move to the Reflex side. We’ll start with a &lt;a href="https://github.com/reflex-dev/templates/tree/main/company_dashboard" rel="noopener noreferrer"&gt;dashboard app template&lt;/a&gt; and then integrate authentication step-by-step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new directory for this app and clone the company dashboard template. Optionally, set up a Python virtual environment inside it.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;reflex-descope-auth&lt;/code&gt; to the &lt;code&gt;requirements.txt&lt;/code&gt; file. &lt;strong&gt;Note:&lt;/strong&gt; This is the &lt;a href="https://github.com/descope-sample-apps/reflex-descope-auth" rel="noopener noreferrer"&gt;Reflex Descope Auth plugin&lt;/a&gt;, which uses standard OIDC protocols to handle the login process in a reliable, familiar way.&lt;/li&gt;
&lt;li&gt;Now run this command to install all the dependencies using the command below:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Next, create a &lt;code&gt;.env&lt;/code&gt; file and set these environment variables in the project.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-descope-project-id&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;DESCOPE_FLOW_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-flow-id&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;DESCOPE_LOGOUT_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;
&lt;span class="py"&gt;SESSION_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;secure-random-secret&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can generate a secret using Python’s secrets module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import secrets; print(secrets.token_hex(32))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what your project structure should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── .gitignore
├── app
│   ├── __init__.py
│   ├── app.py
│   ├── components
│   │   ├── __init__.py
│   │   ├── documents_table.py
│   │   ├── header.py
│   │   ├── key_metrics.py
│   │   ├── sidebar.py
│   │   └── visitors_chart.py
│   └── states
│       ├── __init__.py
│       ├── auth_state.py
│       └── dashboard_state.py
├── apt-packages.txt
├── assets/
├── .env
├── requirements.txt
└── rxconfig.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Log in a user
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;app.py&lt;/code&gt; file, we’ll add a button that calls &lt;code&gt;start_login&lt;/code&gt;, a built-in method that initiates the login flow and redirects users to the Descope authorization endpoint.&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;index&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&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 main page, which serves as the login entry point.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login with Descope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;class_name&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;mt-8 px-8 py-3 text-white bg-red-600 rounded-lg font-semibold &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hover:bg-blue-700 transition-colors shadow-md&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-col items-start max-w-xl&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;container mx-auto flex flex-col lg:flex-row items-center justify-between gap-12 px-4 py-16&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w-full bg-gray-50&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;p&gt;The &lt;code&gt;dashboard_page&lt;/code&gt; function defines the main authenticated dashboard view of the application. It’s registered as a Reflex page using the &lt;code&gt;@rx.page()&lt;/code&gt; decorator with the root route (&lt;code&gt;/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When a user visits this page, the &lt;code&gt;on_load&lt;/code&gt; parameter triggers the &lt;code&gt;AuthState.check_login&lt;/code&gt; method to verify if the user is authenticated. If the user isn’t logged in, they’ll be redirected to the login flow automatically.&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="nd"&gt;@rx.page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_login&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;dashboard_page&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&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 main dashboard page.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;header_bar&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;key_metrics_section&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nf"&gt;visitors_chart_section&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nf"&gt;documents_table_section&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p-6 space-y-6&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w-full h-[100vh] overflow-y-auto&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-row bg-gray-50 h-[100vh] w-full overflow-hidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on_mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DashboardState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_initial_data&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;p&gt;The &lt;code&gt;callback&lt;/code&gt; page handles the authentication redirect after a user logs in. It’s defined using the &lt;code&gt;@rx.page()&lt;/code&gt; decorator and mapped to the &lt;code&gt;/callback&lt;/code&gt; route.&lt;/p&gt;

&lt;p&gt;When the user returns to this route after completing authentication, the &lt;code&gt;on_load&lt;/code&gt; parameter calls &lt;code&gt;AuthState.auth_redirect&lt;/code&gt;. This method processes the login response, typically validating tokens, storing session data, and determining whether the login succeeded or failed.&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="nd"&gt;@rx.page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_redirect&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;callback&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login Failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-red-500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Try Again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-col items-center space-y-4&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="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing login...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center space-x-2&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center justify-center h-screen&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;p&gt;The &lt;code&gt;auth_error&lt;/code&gt; page provides a dedicated route for handling login failures. It’s defined at the &lt;code&gt;/auth-error&lt;/code&gt; path and serves as a fallback whenever something goes wrong during the authentication process. For example, if token validation fails, the user denies consent, or the callback flow encounters an unexpected error.&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="nd"&gt;@rx.page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth-error&lt;/span&gt;&lt;span class="sh"&gt;"&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;auth_error&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login Failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-red-500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Try Again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-col items-center space-y-4 h-screen justify-center&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;p&gt;We’ll also add additional routes to this file to handle page protection and move the dashboard to a dedicated page.&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dashboard_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth-error&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;h2&gt;
  
  
  Define AuthState in Reflex
&lt;/h2&gt;

&lt;p&gt;Authentication in Reflex is handled through reactive state classes. The reflex-descope-auth plugin provides a base class called &lt;code&gt;DescopeAuthState&lt;/code&gt;, which already implements the full authentication lifecycle for you. By subclassing it, we can customize how our app handles redirects and session checks after a user logs in or out. The &lt;code&gt;reflex-descope-auth&lt;/code&gt; plugin validates Descope tokens only once during &lt;code&gt;finalize_auth&lt;/code&gt;. After that, Reflex simply stores the session in state and does not re-check the &lt;code&gt;exp&lt;/code&gt; or &lt;code&gt;rexp&lt;/code&gt; claims in the token automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;finalize_auth&lt;/code&gt; is a built-in method that completes the authentication process after the redirect from Descope, exchanges the authorization code for tokens, verifies ID token, and creates a session token for the user.&lt;/p&gt;

&lt;p&gt;By using &lt;code&gt;yield&lt;/code&gt;, Reflex processes each step reactively—first finalizing auth, then deciding the next redirect.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;reflex&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reflex_descope_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DescopeAuthState&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DescopeAuthState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@rx.event&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_redirect&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="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;DescopeAuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalize_auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_message&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="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth-error&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="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@rx.event&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_login&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;logged_in&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&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;&lt;code&gt;check_login()&lt;/code&gt; is a custom method that acts as a page guard for any route that should only be accessible to authenticated users. It works by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking the reactive state variable &lt;code&gt;self.logged_in&lt;/code&gt; (which is automatically managed by &lt;code&gt;DescopeAuthState&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If the user isn’t authenticated (&lt;code&gt;self.logged_in&lt;/code&gt; is False), it immediately returns a redirect to the homepage (&lt;code&gt;/&lt;/code&gt;), which serves as the login page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures that only valid sessions can access private pages like &lt;code&gt;/dashboard&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing OAuth social logins, magic links, and SSO
&lt;/h2&gt;

&lt;p&gt;Once your Reflex app is wired up with the &lt;a href="https://github.com/descope-sample-apps/reflex-descope-auth" rel="noopener noreferrer"&gt;Reflex Descope Auth plugin&lt;/a&gt;, adding new authentication methods becomes incredibly simple. Descope flows let you drag, drop, and configure options like OAuth social logins, magic links, and SAML/OIDC SSO without changing any of your Reflex code.&lt;/p&gt;

&lt;p&gt;If you want users to sign in with &lt;a href="https://docs.descope.com/auth-methods/oauth/providers/setting-up-your-own-apps/google" rel="noopener noreferrer"&gt;Google&lt;/a&gt;, &lt;a href="https://docs.descope.com/auth-methods/oauth/providers/setting-up-your-own-apps/github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or any other providers, you can configure these &lt;a href="https://docs.descope.com/auth-methods/oauth/providers" rel="noopener noreferrer"&gt;OAuth providers&lt;/a&gt; in the authentication methods in the console and add the &lt;a href="https://docs.descope.com/auth-methods/oauth" rel="noopener noreferrer"&gt;OAuth&lt;/a&gt; action in the flow. For passwordless experiences, you can switch to magic link login by adding the built-in &lt;a href="https://docs.descope.com/auth-methods/magic-link" rel="noopener noreferrer"&gt;magic link&lt;/a&gt; action in your flow. And when your app needs enterprise authentication, you can add a &lt;a href="https://docs.descope.com/auth-methods/sso" rel="noopener noreferrer"&gt;SSO&lt;/a&gt; action to the same flow.&lt;/p&gt;

&lt;p&gt;If you’d like to try SSO end-to-end, you can spin up a tenant and walk through the setup using the &lt;a href="https://docs.descope.com/management/tenant-management/sso/mock-saml-testing" rel="noopener noreferrer"&gt;Mock SAML Testing guide&lt;/a&gt;. And if your organization already uses a SAML IdP such as Okta, ADFS, or PingFederate, you can simply plug in its metadata URL in place of the mock provider.&lt;/p&gt;

&lt;p&gt;With everything controlled through Descope’s visual flow editor, your Reflex integration remains the same, with the plugin automatically handling redirects and token exchange. Here’s an example flow that combines the social login, magic link and SSO 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%2Fockt4ewb087e90wh7v1s.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%2Fockt4ewb087e90wh7v1s.png" alt="Flow with social login, magic links and SSO authentication methods" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest hurdles is enabling each customer to set up Single-Sign-On (SSO) with their own identity provider (IdP). Descope’s &lt;a href="https://docs.descope.com/auth-methods/sso/sso-setup-suite" rel="noopener noreferrer"&gt;SSO Setup Suite&lt;/a&gt; simplifies how organizations configure Single Sign-On (SSO) for their tenants. Instead of manually exchanging metadata and troubleshooting IdP configurations, the suite offers a guided, self-service flow that allows tenant admins to set up and test their SSO integration (SAML or OIDC) end-to-end.&lt;/p&gt;

&lt;p&gt;With built-in steps for attribute mapping, SCIM provisioning, and domain routing, it significantly reduces setup time and support overhead by empowering customers to get their SSO running smoothly with minimal developer involvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying the user name and logging out a user
&lt;/h2&gt;

&lt;p&gt;Once a user successfully signs in, we can use &lt;code&gt;AuthState.userinfo&lt;/code&gt; to display the user’s profile details such as their name, while &lt;code&gt;AuthState.logged_in&lt;/code&gt; tracks the current authentication status. &lt;code&gt;userinfo&lt;/code&gt; comes from the JWT claims inside the session token. The plugin extracts claims from the token and &lt;code&gt;userinfo&lt;/code&gt; simply reads from there. To be able to add custom attributes, you can define &lt;a href="https://docs.descope.com/flows/actions/custom-claims" rel="noopener noreferrer"&gt;custom claims&lt;/a&gt; in Descope.&lt;/p&gt;

&lt;p&gt;In the code snippet below, the header displays a personalized welcome message along with a Logout button when the user is signed in and &lt;code&gt;AuthState.logout&lt;/code&gt; will allow the user to log out.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;reflex&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.states.auth_state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthState&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;header_bar&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;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&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 header bar component.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logged_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&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;User&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-sm font-medium text-gray-700&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="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Logout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;px-4 py-1 text-sm text-white bg-red-600 rounded-md hover:bg-red-700&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="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center space-x-4&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="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center justify-between h-12 px-6 bg-white border-b border-gray-200&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;p&gt;The dashboard will then look similar to this:&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%2F387l6f6hvk2s579n9bg3.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%2F387l6f6hvk2s579n9bg3.png" alt="Dashboard is displayed after the user is logged in" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflex SSO and authentication with Descope
&lt;/h2&gt;

&lt;p&gt;With Descope, implementing authentication in a Reflex app becomes both simple and highly flexible. Descope’s flows let you design every step of the user journey without managing complex logic yourself. The Reflex Descope Auth plugin then brings these flows directly into your application, handling the redirects and tokens. Together, they give you a powerful, low-code way to add secure, customizable authentication to any Reflex project.&lt;/p&gt;

&lt;p&gt;If you have any questions about Descope or a customer identity project we can help with, &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt; with our auth experts.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Gradio App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 25 Mar 2026 20:46:42 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-gradio-app-3oia</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-gradio-app-3oia</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-sso-gradio" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gradio.app/" rel="noopener noreferrer"&gt;Gradio&lt;/a&gt; is an open source Python package that allows you to create web-based interfaces for AI models, APIs, or any Python function. Its simplicity and flexibility make it a popular choice among developers who want to quickly prototype and deploy web-based interfaces without worrying about frontend development.&lt;/p&gt;

&lt;p&gt;Secure authentication methods are needed for these applications to help prevent sensitive data and models from being exposed and ensure that only authorized users can access certain features. &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; is an authentication and user management platform that simplifies the process of adding &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;secure authentication&lt;/a&gt; to your applications. It offers a range of authentication methods and out-of-the-box support for &lt;a href="https://www.descope.com/learn/post/oauth#oauth-and-openid-connect-(oidc)" rel="noopener noreferrer"&gt;OAuth 2.0 and OpenID Connect (OIDC)&lt;/a&gt;, which makes it easy to implement authentication in your Gradio app.&lt;/p&gt;

&lt;p&gt;In this guide, you’ll learn how to integrate Descope authentication and &lt;a href="https://www.descope.com/learn/post/sso" rel="noopener noreferrer"&gt;single sign-on (SSO)&lt;/a&gt; into a Gradio application. You’ll add basic auth to your Gradio application using &lt;a href="https://www.descope.com/use-cases/magic-links" rel="noopener noreferrer"&gt;magic links&lt;/a&gt; and &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social login&lt;/a&gt;. You’ll also implement SSO into the application by configuring Descope to use &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; as an identity provider(IdP).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is adding SSO important for these types of apps?
&lt;/h2&gt;

&lt;p&gt;Many Gradio applications are used for business-to-business (B2B) purposes, where different organizations need secure access to their machine-learning models or APIs. In such cases, SSO is essential for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seamless access:&lt;/strong&gt; Employees can log in using their company credentials instead of managing separate usernames and passwords.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Improved security:&lt;/strong&gt; SSO reduces password fatigue and minimizes security risks associated with weak or reused passwords.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better user management:&lt;/strong&gt; IT administrators can enforce access policies, control permissions, and revoke access centrally through identity providers like Okta or Azure AD.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this tutorial, you need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://developer.okta.com/signup/" rel="noopener noreferrer"&gt;Okta developer account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;Python 3&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/book/en/v2/Getting-Started-The-Command-Line" rel="noopener noreferrer"&gt;Git CLI&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;A code editor and a web browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ecMZOQpOcAc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Gradio application
&lt;/h2&gt;

&lt;p&gt;To keep the focus on implementing authentication and SSO, the tutorial uses a prepared starter template that you’ll build on. To clone it to your local machine, execute the command below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--single-branch&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; starter-template https://github.com/kimanikevin254/descope-gradio-auth-sso.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gradio application you just cloned is mounted within a FastAPI application. This setup is necessary because Gradio only supports authentication with external OAuth providers, such as Descope, when it is mounted inside a FastAPI app.&lt;/p&gt;

&lt;p&gt;Here’s an overview of the most important files in this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/core/config.py&lt;/code&gt;: Loads environment variables and sets application configurations using Pydantic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/ui/gradio_apps/&lt;/code&gt;: Contains three simple Gradio applications:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;admin_dashboard.py&lt;/code&gt;: Displays user info and a logout button. In a real-world app, this would handle admin-specific features.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_dashboard.py&lt;/code&gt;: Similar to the admin dashboard but for regular users. This is where you would implement non-admin functionalities.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;login_page.py&lt;/code&gt;: A simple login page that prompts users to log in with a login button.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;app/ui/gradio_mount.py&lt;/code&gt;: Defines the GradioMounter class, which mounts all Gradio apps to the FastAPI application.&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt;: Defines the environment variables you will require in this project.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;You’ll explore the other files later.&lt;/p&gt;

&lt;p&gt;With the files cloned to your local machine, you can move on to setting up the application. Start by creating a virtual environment, activating it, and installing all the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="c"&gt;# Create the virtual environment&lt;/span&gt;

&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate &lt;span class="c"&gt;#Activate it&lt;/span&gt;

pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rename the &lt;code&gt;.env.example&lt;/code&gt; file to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the application using the command fastapi dev app/main.py and navigate to &lt;code&gt;http://localhost:8000/auth/&lt;/code&gt; to view the login page created using Gradio:&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%2F0iqotx907mfnv8comu3q.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%2F0iqotx907mfnv8comu3q.png" alt="Login Page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To view the admin dashboard, navigate to &lt;code&gt;http://localhost:8000/gradio/admin&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%2F5smasbjeao3o8jgawp0d.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%2F5smasbjeao3o8jgawp0d.png" alt="Admin Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To view the user dashboard, navigate to &lt;code&gt;http://localhost:8000/gradio/user&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%2F1sq9dxhwaf1k0gtctcx2.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%2F1sq9dxhwaf1k0gtctcx2.png" alt="User Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, all the dashboards are publicly available, which poses some security risks. In the next sections, you’ll add authentication to protect them and authorization to make sure that only users with the appropriate role can access the admin dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Descope
&lt;/h2&gt;

&lt;p&gt;To integrate Descope as an external OAuth provider for your Gradio application, you’ll need to apply some configurations in your Descope console.&lt;/p&gt;

&lt;p&gt;Open your &lt;a href="https://app.descope.com/" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt; and create a new project by clicking the project dropdown and selecting &lt;strong&gt;+ Project&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%2F1e9wee2clv9iygb93erk.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%2F1e9wee2clv9iygb93erk.png" alt="Creating a new project"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Provide a project name on the &lt;strong&gt;Create project&lt;/strong&gt; form and click &lt;strong&gt;Create&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%2Fspllfvgaolhsmzaqhl7f.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%2Fspllfvgaolhsmzaqhl7f.png" alt="Providing project details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need to define a &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;flow&lt;/a&gt; that will support the authentication methods you require for this project: magic links, social login, and SSO. To simplify the process, this tutorial uses a preconfigured flow included in the starter template. You’ll find it in the root folder as &lt;code&gt;sign-up-or-in.json&lt;/code&gt;. You only need to import it into Descope. To do this, navigate to &lt;strong&gt;Flows &amp;gt; sign-up-or-in&lt;/strong&gt; and select &lt;strong&gt;Import flow / Export flow &amp;gt; Import flow&lt;/strong&gt; inside the flow editor. This will allow you to upload the flow from your local machine:&lt;/p&gt;

&lt;p&gt;The flow you just imported offers three login options on the welcome screen: magic link, SSO, and social login. If a user chooses the magic link, they receive an email with a link. Clicking the link prompts new users to provide extra details while existing users get a JWT and complete the process. For social login or SSO, users are redirected to their provider, and after successful authentication, they receive a JWT, ending the flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticating with Descope
&lt;/h2&gt;

&lt;p&gt;With Descope fully set up, you can now integrate it as a custom OAuth provider for your Gradio application. You’ll need to configure an OIDC app in the Descope dashboard to obtain the necessary endpoints and credentials for the authorization code flow.&lt;/p&gt;

&lt;p&gt;Here are the required credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; A unique public identifier assigned to the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client secret:&lt;/strong&gt; A confidential key shared only between the application and the authorization server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization endpoint:&lt;/strong&gt; The URL that starts the authentication process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access token endpoint:&lt;/strong&gt; The URL used to exchange an authorization code for access tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User info endpoint:&lt;/strong&gt; The URL that retrieves details about the authenticated user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URL:&lt;/strong&gt; The destination where users are sent after completing authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWKs URI:&lt;/strong&gt; The URL where the authorization server’s public keys are stored for verifying tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes:&lt;/strong&gt; Permissions that define what data and actions the application can access on behalf of the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will define these in the &lt;code&gt;.env&lt;/code&gt; file. Some values for the variables are already included in the starter template, as are common for everyone. To get the ones that are not provided, navigate to &lt;strong&gt;Applications &amp;gt; OIDC default application&lt;/strong&gt; on your Descope console. Scroll down to the &lt;strong&gt;SP Configuration&lt;/strong&gt; section, copy the value of the Client ID, and assign it to the &lt;code&gt;OAUTH_CLIENT_ID&lt;/code&gt; variable in the &lt;code&gt;.env&lt;/code&gt; file:&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%2Ft0482t09778p3i77ofcm.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%2Ft0482t09778p3i77ofcm.png" alt="Obtaining the client ID"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure you also replace the placeholder inside the &lt;code&gt;OAUTH_JWKS_URI&lt;/code&gt;'s value with the same client ID.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OAUTH_SCOPES&lt;/code&gt; variable in the &lt;code&gt;.env&lt;/code&gt; file specifies the scopes your application will request from Descope. However, Descope does not include the &lt;code&gt;descope.claims&lt;/code&gt; (user’s roles, permissions, and tenants) and &lt;code&gt;descope.custom_claims&lt;/code&gt; (user’s custom claims) scopes in its response by default unless explicitly configured.&lt;/p&gt;

&lt;p&gt;To enable these scopes, go to the &lt;strong&gt;OIDC default application&lt;/strong&gt; details page. In the &lt;strong&gt;IDP Configuration&lt;/strong&gt; section, add &lt;code&gt;descope.claims&lt;/code&gt; and &lt;code&gt;descope.custom_claims&lt;/code&gt; under &lt;strong&gt;Supported Claims&lt;/strong&gt;. Make sure to save the changes:&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%2Fre2nli0hewlqkt7ty57u.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%2Fre2nli0hewlqkt7ty57u.png" alt="Defining additional claims"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To obtain the value for the &lt;code&gt;OAUTH_CLIENT_SECRET&lt;/code&gt; variable, navigate to &lt;strong&gt;M2M &amp;gt; + Access Key&lt;/strong&gt; on your Descope console. On the &lt;strong&gt;Generate Access Key&lt;/strong&gt; page, provide a name for your key and select &lt;strong&gt;Generate Key&lt;/strong&gt;. Copy the value of the generated key and assign it to the &lt;code&gt;OAUTH_CLIENT_SECRET&lt;/code&gt; variable in the &lt;code&gt;.env&lt;/code&gt; file:&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%2Flcyayoqs4w5nsgfekc3z.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%2Flcyayoqs4w5nsgfekc3z.png" alt="Creating the client secret"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You now have all the required values for implementing the authentication logic. Open the &lt;code&gt;app/core/auth.py&lt;/code&gt; file and add the following method to the &lt;code&gt;Auth&lt;/code&gt; class to initialize the OAuth client with the necessary settings:&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;# Initialize the OAuth client with settings
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;init_oauth&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;settings&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;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&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;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_id&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;authorize_url&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_AUTHORIZE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;access_token_url&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_ACCESS_TOKEN_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redirect_uri&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jwks_uri&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_JWKS_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;userinfo_endpoint&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_USERINFO_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_kwargs&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;scope&lt;/span&gt;&lt;span class="sh"&gt;'&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_SCOPES&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;p&gt;Add the following methods to the same class. These methods will redirect the user to the authorization endpoint set up for the OAuth client and exchange the returned authorization code for an access token:&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;# Redirect to authorization endpoint
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_redirect&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="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_uri&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getattr&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;oauth&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_NAME&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;authorize_redirect&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;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Exchange authorization code for access token   
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_access_token&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="n"&gt;Request&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getattr&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;oauth&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_NAME&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;authorize_access_token&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="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;OAuthError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&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;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status_code&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_401_UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAuth error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&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;Add the following method to the same class to retrieve the currently authenticated user from the session:&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;# Get the current authenticated user name
&lt;/span&gt;   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_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;request&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;-&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="nb"&gt;str&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="k"&gt;if&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;user&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method helps check if a request is authenticated when accessing API routes. If the user is not authenticated, you can take appropriate action, such as redirecting them to the login page.&lt;/p&gt;

&lt;p&gt;You’ll also need a method to protect Gradio apps by ensuring the user is authenticated and has the necessary roles. Add the following method to the &lt;code&gt;Auth&lt;/code&gt; class:&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;# Authenticate user and authorize based on path and roles
# To protect Gradio app routes
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate_and_authorize&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="n"&gt;Request&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
   &lt;span class="n"&gt;path&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="p"&gt;{})&lt;/span&gt;
   &lt;span class="n"&gt;tenants&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenants&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="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;roles&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;roles&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="c1"&gt;# Avoid blocking the gradio queue requests
&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;/gradio_api/queue&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;path&lt;/span&gt; &lt;span class="ow"&gt;and&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;user&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;and&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_ROLE&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;path&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pass. returning&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="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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;path&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_DASHBOARD_PATH&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;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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following method to the &lt;code&gt;Auth&lt;/code&gt; class to determine which Gradio app an authenticated user should see based on their roles:&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;# Determine the Gradio app to show the user based on their roles
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_redirect_path&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="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="p"&gt;{})&lt;/span&gt;
   &lt;span class="n"&gt;tenants&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenants&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="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;roles&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;roles&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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You have not set up tenants in your Descope account. You can only access the user dashboard since the roles needed to access the admin dashboard are set up via the tenant.&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="ow"&gt;not&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOGIN_PAGE_PATH&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_ROLE&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;roles&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_DASHBOARD_PATH&lt;/span&gt;
   &lt;span class="k"&gt;else&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="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_DASHBOARD_PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure the OAuth client is correctly initialized when the application starts, open the &lt;code&gt;app/main.py&lt;/code&gt; file and add the following code immediately after the middleware configuration:&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;# Initialize OAuth client
&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init_oauth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up all the auth routes by adding the code to the &lt;code&gt;app/api/routes/auth_router.py&lt;/code&gt; file:&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;# Redirect to OAuth login provider
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&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;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;redirect_uri&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="nf"&gt;url_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_callback&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize_redirect&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;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Handle OAuth callback after successful login
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth/callback&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth_callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_callback&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;Request&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize_access_token&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userinfo&lt;/span&gt;&lt;span class="sh"&gt;"&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;session&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&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="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&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_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&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;HTTPException&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="c1"&gt;# Log the error
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authentication error: &lt;/span&gt;&lt;span class="si"&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;detail&lt;/span&gt;&lt;span class="si"&gt;}&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;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth/error&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_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Log out the current user   
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/logout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;logout&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;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&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_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Authentication error page
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="k"&gt;return&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;Authentication failed. Please try again.&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 code defines routes that allow the user to log in, handle the OAuth callback, and log out of the application.&lt;/p&gt;

&lt;p&gt;Set up the dashboard routes that will display the appropriate dashboards to the user based on their roles by replacing the existing route in the &lt;code&gt;app/api/routes/dashboard_router.py&lt;/code&gt; file with the following:&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;# Main entry point, redirects based on user role
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&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;index&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;Request&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_user&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="k"&gt;if&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;redirect_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user_redirect_path&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redirect_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;else&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;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOGIN_PAGE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Routing endpoint for Gradio dashboards
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/gradio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_dashboard&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;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;redirect_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user_redirect_path&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redirect_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, protect the Gradio dashboards by replacing the &lt;code&gt;mount_admin_dashboard&lt;/code&gt; and &lt;code&gt;mount_user_dashboard&lt;/code&gt; methods in the &lt;code&gt;app/ui/gradio_mount.py&lt;/code&gt; file with the following:&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;# Mount the admin dashboard with authorization   
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mount_admin_dashboard&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;admin_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;admin_dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_gradio_app&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;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;admin_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth_dependency&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticate_and_authorize&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Mount the user dashboard with authorization
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mount_user_dashboard&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;user_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_gradio_app&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;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth_dependency&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticate_and_authorize&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code adds the &lt;code&gt;auth_dependency&lt;/code&gt; parameter before mounting these apps, which runs before any Gradio-related route in your FastAPI app. This ensures that proper authentication and authorization checks are performed before displaying the dashboards.&lt;/p&gt;

&lt;p&gt;You can now log in to the application using either magic links or social login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing SSO with OIDC using Descope
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OIDC&lt;/a&gt; is an identity layer built on top of the OAuth 2.0 framework. It allows applications to authenticate users securely by delegating authentication to a trusted IdP, such as Okta. OIDC simplifies SSO by enabling users to log in once and access multiple applications without reentering credentials.&lt;/p&gt;

&lt;p&gt;To implement SSO, you can configure Okta as the IdP and Descope as the authentication service. Start by launching your Okta admin dashboard and navigating to &lt;strong&gt;Applications &amp;gt; Applications &amp;gt; Browse App Catalog&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%2Fbrg4eusnkgphzlwl4j0d.webp" 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%2Fbrg4eusnkgphzlwl4j0d.webp" alt="Okta Developer Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;Browse App Catalog&lt;/strong&gt; page, search for &lt;code&gt;descope&lt;/code&gt;, and select &lt;strong&gt;Descope&lt;/strong&gt; from the search results:&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%2F6c5ls8yu9izjo22fi4g4.webp" 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%2F6c5ls8yu9izjo22fi4g4.webp" alt="Searching for Descope App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;+ Add Integration&lt;/strong&gt; from the Descope app details 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%2Fiyooxd11dtyonrsvntqd.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%2Fiyooxd11dtyonrsvntqd.png" alt="Adding the Descope App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;General Settings&lt;/strong&gt; tab, leave everything as is and click &lt;strong&gt;Next&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%2Fhlrz9kv65qt40pk8glv8.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%2Fhlrz9kv65qt40pk8glv8.png" alt="General Settings Tab"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;Sign-On&lt;/strong&gt; options tab, select &lt;strong&gt;OpenID Connect&lt;/strong&gt; as the sign-on method:&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%2Fktewkxqaxf8let9dopvw.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%2Fktewkxqaxf8let9dopvw.png" alt="Selecting the sign-on method"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to the &lt;strong&gt;Advanced Sign-on Settings&lt;/strong&gt; section, and in the &lt;strong&gt;Callback URL&lt;/strong&gt; field, provide the value &lt;code&gt;https://api.descope.com/v1/oauth/callback&lt;/code&gt;. This defines the URL where the user will be redirected after getting successfully authenticated by Okta. Save the changes by clicking &lt;strong&gt;Done&lt;/strong&gt; at 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%2Fnqlkwwf6g67xc7bd4fbw.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%2Fnqlkwwf6g67xc7bd4fbw.png" alt="Providing the OIDC SSO callback URL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You also need to assign users to the Descope app you just configured. This will ensure that only authorized users can authenticate via the app. To do this, go to the app’s details page and the &lt;strong&gt;Assignments&lt;/strong&gt; tab and select &lt;strong&gt;Assign &amp;gt; Assign to Groups&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%2Frmpihtfvxzvgshugxlpx.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%2Frmpihtfvxzvgshugxlpx.png" alt="Assigning the app to groups"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;Assign Descope to Groups&lt;/strong&gt; page, select the &lt;strong&gt;Assign&lt;/strong&gt; button beside the &lt;strong&gt;Everyone&lt;/strong&gt; group to allow all users in your organization to authenticate via the app, and select &lt;strong&gt;Done&lt;/strong&gt; to save the changes.&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%2Faz7xakflm70cq6ap4vbz.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%2Faz7xakflm70cq6ap4vbz.png" alt="Assigning the Descope app to everyone"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select the &lt;strong&gt;Sign On&lt;/strong&gt; tab and take note of your OIDC client ID and secret:&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%2Fplgenkix6l1q4dbwb4cx.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%2Fplgenkix6l1q4dbwb4cx.png" alt="Obtaining client ID and secret"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to the Descope console to create a tenant that you will use with the Descope app you just configured in Okta. On your Descope console, navigate to &lt;strong&gt;Tenants &amp;gt; + Tenant&lt;/strong&gt;, provide the tenant details on the &lt;strong&gt;Create Tenant&lt;/strong&gt; form, and select &lt;strong&gt;Create&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%2Fnobghoexarvrhpedx52v.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%2Fnobghoexarvrhpedx52v.png" alt="Creating a tenant"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the details page of the Tenant you just created, add your email domain under the &lt;strong&gt;Tenant Settings &amp;gt; Details&lt;/strong&gt; section in the &lt;strong&gt;Email domain&lt;/strong&gt; field, and save the changes:&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%2F3f573nrgvhaqy7tfdca2.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%2F3f573nrgvhaqy7tfdca2.png" alt="Adding the tenant email domain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;Authentication Methods &amp;gt; SSO&lt;/strong&gt; from the tenant details page sidebar, and under &lt;strong&gt;Authentication Protocol&lt;/strong&gt;, select &lt;strong&gt;OIDC&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%2Fy9wxf9x1oqf5e9x75fd1.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%2Fy9wxf9x1oqf5e9x75fd1.png" alt="Selecting authentication protocol"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Tenant Details &amp;gt; SSO Domains&lt;/strong&gt;, provide your email domain. This will help to determine which SSO configuration to load once a user chooses to authenticate using SSO:&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%2Fl0454e0ja8qpdkjro15n.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%2Fl0454e0ja8qpdkjro15n.png" alt="Setting the SSO domain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to the &lt;strong&gt;SSO configuration &amp;gt; Account Settings&lt;/strong&gt; section and provide the required values as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider Name:&lt;/strong&gt; &lt;code&gt;Okta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; The Client ID you obtained from the Okta dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret:&lt;/strong&gt; The client secret you obtained from the Okta dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; &lt;code&gt;openid profile email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant Type:&lt;/strong&gt; &lt;code&gt;Authorization code&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To obtain the values needed for the &lt;strong&gt;SSO configuration &amp;gt; Connection Settings&lt;/strong&gt; section, you’ll need the OAuth endpoints from the &lt;a href="https://developer.okta.com/docs/concepts/auth-servers/#discovery-endpoints-org-authorization-servers" rel="noopener noreferrer"&gt;Okta “well-known” configuration&lt;/a&gt;. Navigate to &lt;code&gt;https://&amp;lt;YOUR-OKTA-INSTANCE&amp;gt;.okta.com/.well-known/openid-configuration&lt;/code&gt; on your browser, and take note of the issuer and authorization, token, user info, and JWKs endpoints.&lt;/p&gt;

&lt;p&gt;Make sure to replace &lt;code&gt;&amp;lt;YOUR-OKTA-INSTANCE&amp;gt;&lt;/code&gt; with the correct value, which is your organization’s Okta instance 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%2Fppnjonwi1zekuwmwvvre.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%2Fppnjonwi1zekuwmwvvre.png" alt="Obtaining Okta OAuth endpoints"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Head back to the Descope console, provide these values in the respective inputs under &lt;strong&gt;SSO configuration &amp;gt; Connection Settings&lt;/strong&gt;, and save the changes.&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%2F1en7ewecmmzb2kdlq2lq.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%2F1en7ewecmmzb2kdlq2lq.png" alt="Connection Settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SSO with Okta as the IdP is now fully configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demonstrating the application
&lt;/h2&gt;

&lt;p&gt;To run the application and confirm that everything is working as expected, use the command &lt;code&gt;fastapi dev app/main.py&lt;/code&gt; and navigate to &lt;code&gt;http://localhost:8000&lt;/code&gt; on your browser. You will be redirected to the login 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%2Fkajgtzsm092xunkrzjyb.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%2Fkajgtzsm092xunkrzjyb.png" alt="Login Page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;Login&lt;/strong&gt; button, and you’ll be redirected to Descope’s sign-in page, which is powered by the flow you configured earlier. You can sign in using any of the available methods:&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%2Fe135d8d3ip6kp653clfy.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%2Fe135d8d3ip6kp653clfy.png" alt="Flow-powered sign-in page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After successful authentication, you’ll be redirected to the user dashboard. This is because you don’t have the appropriate roles to access the admin dashboard:&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%2Fg5pf1as0kes5680ocxkp.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%2Fg5pf1as0kes5680ocxkp.png" alt="User dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you manually tried to navigate to &lt;code&gt;http://localhost:8000/gradio/admin&lt;/code&gt;, you will get an error informing you that you’re not authenticated:&lt;/p&gt;

&lt;p&gt;To check if a user with the appropriate role can access the admin dashboard, open the &lt;a href="https://app.descope.com/users" rel="noopener noreferrer"&gt;users’ page&lt;/a&gt; on the Descope console, edit your user to assign them the &lt;code&gt;Tenant Admin&lt;/code&gt; role, and save the changes:&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%2Fjmt680ny9nrvym8ijmhw.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%2Fjmt680ny9nrvym8ijmhw.png" alt="Assigning a role to the user"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, go back to the app, log out, and log in again. You should be redirected to the admin dashboard:&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%2Fs1unwtwncwaicnktqceb.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%2Fs1unwtwncwaicnktqceb.png" alt="Admin Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this guide, you learned how to integrate Descope as an OAuth provider for your Gradio application using OIDC. You also explored how to implement SSO with Okta as the identity provider and Descope as the authentication service. Additionally, you implemented role-based access control to protect Gradio app routes.&lt;/p&gt;

&lt;p&gt;Descope is a drag-and-drop customer authentication and identity management platform. Our no- or low-code CIAM solution helps hundreds of organizations easily create and customize their entire user journey using visual workflows—from authentication and authorization to MFA and federated SSO. Customers such as GoFundMe, &lt;a href="https://www.descope.com/customers/navan" rel="noopener noreferrer"&gt;Navan&lt;/a&gt;, &lt;a href="https://www.descope.com/customers/you-com" rel="noopener noreferrer"&gt;You.com&lt;/a&gt;, and &lt;a href="https://www.descope.com/customers/branch" rel="noopener noreferrer"&gt;Branch&lt;/a&gt; use Descope to reduce user friction, prevent account takeover, and get a unified view of their customer journey.&lt;/p&gt;

&lt;p&gt;To learn more, join our dev community, &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;, and explore the &lt;a href="https://docs.descope.com/" rel="noopener noreferrer"&gt;Descope documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
