<?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: Mrunank Pawar</title>
    <description>The latest articles on DEV Community by Mrunank Pawar (@mrunankpawar).</description>
    <link>https://dev.to/mrunankpawar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F491439%2F2bd76281-a5ab-4da4-a87d-1afe579f5e91.jpeg</url>
      <title>DEV Community: Mrunank Pawar</title>
      <link>https://dev.to/mrunankpawar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrunankpawar"/>
    <language>en</language>
    <item>
      <title>Adding Authentication Middleware With Descope</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 29 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/adding-authentication-middleware-with-descope-1jo2</link>
      <guid>https://dev.to/descope/adding-authentication-middleware-with-descope-1jo2</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/authentication-middleware" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Looking to implement authentication middleware in your app? Most modern frameworks support middleware functions that let you intercept user requests, verify tokens, and control access—all in a centralized way.&lt;/p&gt;

&lt;p&gt;In this blog, we'll walk through how authentication middleware works and how to implement it effectively using code examples in Node.js and Python. Once you understand the basics, we'll show how Descope's tools can streamline the process with out-of-the-box support for token validation, role checks, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is authentication middleware in web apps
&lt;/h2&gt;

&lt;p&gt;In the context of web development, authentication middleware is a function that intercepts incoming requests and runs logic before or after those requests reach your application's core handlers. Middleware gives you a centralized way to manage access control, freeing up developer time to focus on business logic instead of duplicating auth checks across routes.&lt;/p&gt;

&lt;p&gt;Most modern frontend and backend frameworks support this concept. You typically register a function—similar to a callback—that's triggered with every request. This can happen before the request reaches a controller or after the response is generated.&lt;/p&gt;

&lt;p&gt;Within this middleware function, you can inspect request headers (like &lt;code&gt;Authorization&lt;/code&gt;) and validate session tokens or permissions before allowing access to protected routes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Authentication middleware in Node.js
&lt;/h3&gt;

&lt;p&gt;Let's look at how to build simple authentication middleware using Express in a Node.js app. This middleware intercepts requests, checks for an authorization header, and validates the session token before passing the request to the next handler.&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;var&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;validateJwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&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="c1"&gt;// validation logic&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &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="nx"&gt;next&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;isValidSession&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&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;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="nx"&gt;authMiddleware&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;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;/&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="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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;responseText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&amp;lt;br&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;responseText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;small&amp;gt;Session Valid:&lt;/span&gt;&lt;span class="dl"&gt;'&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;isValidSession&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;&lt;span class="dl"&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseText&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="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the &lt;code&gt;authMiddleware&lt;/code&gt; function runs on every request. It extracts the token from the &lt;code&gt;Authorization&lt;/code&gt; header, runs a validation check, and attaches the result (&lt;code&gt;isValidSession&lt;/code&gt;) to the request object. This setup gives you a lightweight way to apply authentication logic across all routes in your Express app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Descope authentication to Node.js middleware
&lt;/h2&gt;

&lt;p&gt;Now that we understand how middleware works, we can use it to implement robust &lt;a href="https://www.descope.com/learn/post/authorization" rel="noopener noreferrer"&gt;authorization&lt;/a&gt; and authentication middleware by validating user sessions on every request.&lt;/p&gt;

&lt;p&gt;Descope makes this easier by providing &lt;a href="https://docs.descope.com/sdk/" rel="noopener noreferrer"&gt;SDKs&lt;/a&gt; that handle session and token validation. Instead of repeating auth logic across your app, you can centralize it in a single middleware function. That way, each incoming request is automatically checked for a valid &lt;a href="https://www.descope.com/learn/post/access-token" rel="noopener noreferrer"&gt;access token&lt;/a&gt; or &lt;a href="https://www.descope.com/learn/post/refresh-token" rel="noopener noreferrer"&gt;refresh token&lt;/a&gt; before hitting protected routes.&lt;/p&gt;

&lt;p&gt;Here's how to build authentication middleware using the Descope Node.js SDK:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;authentication.js&lt;/strong&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DescopeClient&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@descope/node-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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DescopeMiddleware&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="nx"&gt;descopeClient&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="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;validate&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="k"&gt;try&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;descopeSdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&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;Successfully validated user session:&lt;/span&gt;&lt;span class="dl"&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="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="k"&gt;catch &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;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;Could not validate user session &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&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="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;&lt;strong&gt;app.js&lt;/strong&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="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="s1"&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;DescopeMiddleware&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./authentication.js&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&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;DescopeMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__PROJECT_ID__&lt;/span&gt;&lt;span class="dl"&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;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&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;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;/&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;send&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&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;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="mi"&gt;3000&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;Express Running port 3000&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;This pattern helps you manage authentication in one place. It also improves maintainability by offloading session validation to Descope's SDK, allowing you to focus on application logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Descope decorators in Python
&lt;/h2&gt;

&lt;p&gt;Python decorators are a powerful way to wrap additional behavior around existing functions, making them especially useful for building &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;authentication&lt;/a&gt; middleware in Python web frameworks.&lt;/p&gt;

&lt;p&gt;A decorator takes a function as input, extends or modifies its behavior, and returns a new function. This allows you to centralize logic, such as logging, validation, or—in our case—authentication, without cluttering every route with repeated code.&lt;/p&gt;

&lt;p&gt;Here's a basic example of how decorators work:&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;my_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&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;wrapper&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;Something is happening before the function is called.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;func&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;Something is happening after the function is called.&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;wrapper&lt;/span&gt;

&lt;span class="nd"&gt;@my_decorator&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;say_hello&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;Hello!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;say_hello&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use a decorator, you simply annotate a function with &lt;code&gt;@decorator_name&lt;/code&gt;. When you call that function, Python actually runs the wrapper logic defined inside the decorator. In this case, calling &lt;code&gt;say_hello()&lt;/code&gt; produces the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Something is happening before the function is called.
&amp;gt; Hello!
&amp;gt; Something is happening after the function is called.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure is ideal for building authentication middleware in frameworks like &lt;a href="https://www.descope.com/blog/post/auth-flask-app" rel="noopener noreferrer"&gt;Flask&lt;/a&gt;, FastAPI, or &lt;a href="https://www.descope.com/blog/post/auth-django-app" rel="noopener noreferrer"&gt;Django&lt;/a&gt;, where decorators can be applied directly to route functions to enforce login or permission checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Descope Python decorator
&lt;/h3&gt;

&lt;p&gt;You could write your own authentication middleware using decorators in Flask, FastAPI, or Django. But to save time, Descope provides prebuilt decorators that handle session validation, role enforcement, and other identity checks out of the box.&lt;/p&gt;

&lt;p&gt;Here's an example of using Descope's &lt;code&gt;@descope_validate_auth&lt;/code&gt; decorator to protect a route that requires authentication:&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;# This needs authentication
&lt;/span&gt;&lt;span class="nd"&gt;@APP.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@descope_validate_auth&lt;/span&gt;&lt;span class="p"&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="c1"&gt;# Can add permissions=["Perm 1"], roles=["Role 1"], tenant="t1" conditions
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;private&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Restricted page, authentication needed.&amp;lt;/h1&amp;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;This decorator automatically checks the user's &lt;a href="https://www.descope.com/learn/post/jwt" rel="noopener noreferrer"&gt;JWT&lt;/a&gt;. If the token doesn't meet the conditions—such as having the correct roles or permissions—Descope will return a 401 Unauthorized response without running the route handler.&lt;/p&gt;

&lt;p&gt;Descope also provides other decorators that extend the behavior of your routes. For example, the &lt;code&gt;@descope_full_login&lt;/code&gt; decorator can trigger a &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flow&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@APP.route&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="n"&gt;methods&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;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@descope_full_login&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;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flow_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;sign-up-or-in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;success_redirect_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://dev.localhost:9010/private&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;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Nothing to do! this is the MAGIC!
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need to handle logout? The &lt;code&gt;@descope_logout&lt;/code&gt; decorator clears the user's 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="nd"&gt;@APP.route&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="nd"&gt;@descope_logout&lt;/span&gt;&lt;span class="p"&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="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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Goodbye, logged out.&amp;lt;/h1&amp;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;With just a few lines of code, you can use decorators to handle user authentication and session flow without repeating logic across your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication middleware made simpler with Descope
&lt;/h2&gt;

&lt;p&gt;Authentication middleware lets you centralize and automate how your app handles access control. By intercepting requests and validating tokens before they hit protected routes, middleware simplifies security and improves the user experience.&lt;/p&gt;

&lt;p&gt;Descope gives you tools to build and manage this layer more efficiently. Whether you prefer writing your own middleware or using prebuilt decorators, you can plug in Descope's SDKs and flows to streamline session validation, role enforcement, and login logic.&lt;/p&gt;

&lt;p&gt;Want to add authentication to your app without reinventing the wheel? &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; for a Free Forever Descope account to get started. If you have questions or want to learn more about authentication best practices, &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book time&lt;/a&gt; with our auth experts.&lt;/p&gt;

</description>
      <category>node</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Shiny App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-shiny-app-15mb</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-shiny-app-15mb</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-shiny" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://shiny.posit.co/" rel="noopener noreferrer"&gt;Shiny&lt;/a&gt; lets you easily build web apps for data science using R and Python without the need for any web development experience. You can deploy these applications on your own web server or use &lt;a href="https://posit.co/products/enterprise/connect/" rel="noopener noreferrer"&gt;Posit Connect&lt;/a&gt; to deploy them directly from your IDE.&lt;/p&gt;

&lt;p&gt;Although deploying the applications is straightforward, it's also vital to set up proper access control for these applications to ensure that only authenticated users can view and interact with the data. Integrating single sign-on (SSO) with Shiny apps can provide a seamless authentication experience for the users. As these apps are often used by different business or enterprise users who might need to log in with their own identity provider (IdP), SSO allows them to use their existing credentials to log in.&lt;/p&gt;

&lt;p&gt;Descope offers a no-code &lt;a href="https://www.descope.com/learn/post/ciam" rel="noopener noreferrer"&gt;Customer Identity Platform&lt;/a&gt; (CIAM) that lets you easily define a customized workflow for sign-up, login, multi-factor authentication (MFA), SSO, and Security Assertion Markup Language (SAML).&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll learn how to implement authentication and SSO in a Shiny app.&lt;/p&gt;

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

&lt;p&gt;Before starting, you need to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://posit.co/download/rstudio-desktop/" rel="noopener noreferrer"&gt;Install RStudio&lt;/a&gt; on your development machine.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.posit.co/connect/admin/getting-started/local-install/" rel="noopener noreferrer"&gt;Install Posit Connect locally&lt;/a&gt; and configure its license. This tutorial uses an on-prem installation of Posit Connect on AWS, but alternatively, you can sign up for &lt;a href="https://connect.posit.cloud/" rel="noopener noreferrer"&gt;Posit Connect Cloud&lt;/a&gt; and use it to follow along.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up for a free Descope account.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Before you can integrate Descope authentication into your Shiny application, you need to define an authentication flow using the Descope portal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up for a free Descope account&lt;/a&gt; if you don't already have one, and follow the &lt;a href="https://app.descope.com/gettingStarted" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; wizard to create a new flow. In the wizard, first, choose Consumers as the target audience for your 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%2Fff7zn7tz7m7wzlo0ywjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fff7zn7tz7m7wzlo0ywjl.png" alt="Fig: Choose target users" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, choose the authentication methods you want to use. In this tutorial, you'll use Magic Link and SSO as the authentication method. You can change these settings in the future if you 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%2Fvfubuzcsm15mqxce7hle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfubuzcsm15mqxce7hle.png" alt="Fig: Choose authentication methods" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Skip the next step that prompts you to choose an MFA method by clicking Go ahead without MFA. In the next step, review the selected settings and click Next to generate the authentication 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%2F1fcz9kmjlech0hxuwt8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fcz9kmjlech0hxuwt8i.png" alt="Fig: Review authentication methods" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Shiny app
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you'll create a personal finance application using Shiny and add Descope authentication. The Shiny app will let you upload a CSV file containing month-wise expense data, view a bar chart with monthly expense trends, and also analyze category-wise expenses.&lt;/p&gt;

&lt;p&gt;Since the tutorial focuses on adding authentication, you can clone the &lt;a href="https://github.com/maskaravivek/personal-finance-shiny" rel="noopener noreferrer"&gt;starter code&lt;/a&gt; with the personal finance Shiny app. In the terminal, navigate to the project directory and check out the starter branch.&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;personal-finance-shiny
git checkout starter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the application using RStudio and attempt to run the starter app. When you run it for the first time, it will prompt you to install the required packages. After you install the packages and rerun the app, you'll notice that you're directly taken to the dashboard page without 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%2Fkzgpuypa4gdbeeim9bqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzgpuypa4gdbeeim9bqa.png" alt="Fig: Personal finance Shiny app" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, click on the Publish button in RStudio to deploy the app on &lt;a href="https://docs.posit.co/pct/" rel="noopener noreferrer"&gt;Posit Connect&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%2Fe5mgxfezizt0050ow86s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5mgxfezizt0050ow86s.png" alt="Fig: Publish Shiny app" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before you can publish the Shiny app, you need to &lt;a href="https://docs.posit.co/connect/user/publishing-rstudio/" rel="noopener noreferrer"&gt;connect your Posit Connect account&lt;/a&gt; by clicking Add new account and choosing Posit Connect. Enter the Posit Connect server URL and click Next to connect the account.&lt;/p&gt;

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

&lt;p&gt;You'll be redirected to the web browser to confirm the linking between RStudio and Posit Connect. Click Connect to link the account.&lt;/p&gt;

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

&lt;p&gt;After confirming the connection in the browser, navigate back to RStudio and click Connect Account to verify the account.&lt;/p&gt;

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

&lt;p&gt;Finally, select the connected account, provide a title for the app, and click Publish to deploy the application.&lt;/p&gt;

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

&lt;p&gt;The dashboard of the app has the following components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The left section lets you upload a CSV file containing personal expense data. The GitHub repo contains a &lt;a href="https://github.com/maskaravivek/personal-finance-shiny/blob/starter/data/sample_expense_data.csv" rel="noopener noreferrer"&gt;sample CSV&lt;/a&gt; that you could use for testing.&lt;/li&gt;
&lt;li&gt;When you upload the CSV file, the right section lets you view and analyze the category-wise expenses based on the uploaded data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implement vanilla authentication with Descope
&lt;/h2&gt;

&lt;p&gt;You've deployed the app to Posit Connect and can now add authentication methods to access it using Descope. In this section, you'll add auth using &lt;a href="https://www.descope.com/learn/post/magic-links" rel="noopener noreferrer"&gt;magic links&lt;/a&gt; and &lt;a href="https://docs.descope.com/auth-methods/otp" rel="noopener noreferrer"&gt;email OTP&lt;/a&gt; for simplicity. You can configure and use other &lt;a href="https://docs.descope.com/" rel="noopener noreferrer"&gt;authentication methods&lt;/a&gt; too but we will focus on magic links and OTP for this tutorial.&lt;/p&gt;

&lt;p&gt;Descope offers a &lt;a href="https://docs.descope.com/getting-started/html" rel="noopener noreferrer"&gt;WebJS component&lt;/a&gt; to integrate its auth flows with HTML-based web apps. You can easily integrate the WebJS component with your Shiny app by adding a custom &lt;a href="https://shiny.posit.co/r/articles/build/js-build-widget/" rel="noopener noreferrer"&gt;JavaScript widget&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In your R project, create a &lt;code&gt;www/descope-auth.js&lt;/code&gt; file, add the following content to create a container for the Descope element, and set success and failure 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="c1"&gt;// www/descope-auth.js&lt;/span&gt;
&lt;span class="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCustomMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initDescopeAuth&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="nx"&gt;config&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;wcElement&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;createElement&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;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;project-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flow-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flowId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;wcElement&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="nf"&gt;function &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="nx"&gt;user&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;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;successInput&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="nx"&gt;wcElement&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="nf"&gt;function &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;Authentication 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="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorInput&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;const&lt;/span&gt; &lt;span class="nx"&gt;container&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="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerId&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;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&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="c1"&gt;// Clear any previous instances&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wcElement&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;Next, you need to update the UI definition to include the &lt;code&gt;@descope/web-component&lt;/code&gt; and &lt;code&gt;descope-auth.js&lt;/code&gt; JS scripts. Update the &lt;code&gt;app.R&lt;/code&gt; file by replacing it with the following code snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fluidPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;useShinyjs&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# For dynamic UI control&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://unpkg.com/@descope/web-component@3.32.0/dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"descope-auth.js"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Include custom JavaScript&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="n"&gt;titlePanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Personal Finance Dashboard with Descope Auth"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;# Descope Authentication UI&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"authContainer"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appContent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;sidebarLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;sidebarPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload Expense Data (CSV)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".csv"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;selectInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Select Category:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze"&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="n"&gt;mainPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;tabsetPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;tabPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Overview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;plotOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"expenseTrend"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tableOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"summaryTable"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;tabPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Category Analysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;plotOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"categoryTrend"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tableOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"categoryTable"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;Finally, you need to update the server definition to initialize the Descope web component and implement success and failure callbacks. Add the following code snippet at the start of the server section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;session&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="c1"&gt;# Initialize Descope Auth&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;sendCustomMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"initDescopeAuth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR_DESCOPE_PROJECT_ID&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;flowId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sign-up-or-in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;containerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"authContainer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;successInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_authenticated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;errorInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth_error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&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="k"&gt;if&lt;/span&gt;&lt;span class="w"&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;is.null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&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="n"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Login successful!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"appContent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&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;is.null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Authentication failed. Please try again."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;# existing server definition&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;Replace &lt;code&gt;YOUR_DESCOPE_PROJECT_ID&lt;/code&gt; with your &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;Descope project ID&lt;/a&gt;, save the changes, and republish the app to Posit Connect using RStudio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test authentication using magic link
&lt;/h3&gt;

&lt;p&gt;When a user clicks the magic link received in their email for authentication, Descope will validate the auth and redirect the user back to the Shiny application. For redirection to work, you need to configure the auth redirect URL in Descope. Head over to &lt;a href="https://docs.descope.com/auth-methods/magic-link#magic-link" rel="noopener noreferrer"&gt;Magic link&lt;/a&gt; under Authentication Methods and set the &lt;a href="https://docs.descope.com/auth-methods/magic-link#redirect-url" rel="noopener noreferrer"&gt;Redirect URL&lt;/a&gt; to the published URL of your Shiny 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%2F1bwm39sbwk88wp35it6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bwm39sbwk88wp35it6k.png" alt="Fig: Configure Magic link redirect URL" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To test the authentication, open the published content under the Content tab on Posit Connect.&lt;/p&gt;

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

&lt;p&gt;When you run the app, you'll be redirected to the Descope auth screen. You can test the integration by entering your email ID on this screen and clicking the Continue button to log in with a magic link. You'll receive an email with a login link, and upon clicking it, you'll be redirected to the dashboard page of your Shiny 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%2Fyk5r6zj03eznat44gqq3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyk5r6zj03eznat44gqq3.png" alt="Fig: Descope auth screen" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a new Descope tenant
&lt;/h2&gt;

&lt;p&gt;After integrating vanilla authentication, the next step is to add &lt;a href="https://docs.descope.com/auth-methods/sso" rel="noopener noreferrer"&gt;SSO authentication&lt;/a&gt; using Descope. To do this, you first need to create a &lt;a href="https://docs.descope.com/tenant-management" rel="noopener noreferrer"&gt;Tenant&lt;/a&gt;. Tenants represent organizations that use your application and make it easier to group and manage users, permissions, and authentication flows within the application. In this tutorial, you'll create a tenant for an example organization and use it to configure the SSO flow.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;a href="https://app.descope.com/tenants" rel="noopener noreferrer"&gt;Tenants&lt;/a&gt; and click the + Tenant button to create a new tenant.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Implement SSO with OIDC using Descope
&lt;/h2&gt;

&lt;p&gt;OpenID Connect (OIDC) is an extension of the &lt;a href="https://oauth.net/2/" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; authorization protocol that provides a standardized way for applications to request and receive identity information about users. Instead of managing user credentials for each application, OIDC SSO lets you delegate the authentication mechanism to a single trusted IdP, reducing the maintenance overhead. Once you log in, the IdP returns standardized JSON web tokens (JWTs) on login-containing information about the user's identity. In this section, you'll learn how to set up Descope as an OIDC IdP with Posit Connect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Descope OIDC application in a separate project
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.descope.com/sso-integrations/applications/oidc-apps" rel="noopener noreferrer"&gt;Descope OIDC application&lt;/a&gt; acts as a federated IdP for the users. Based on your needs, Descope can act as a &lt;a href="https://docs.descope.com/sso-integrations/idp-vs-sp#service-provider-sp" rel="noopener noreferrer"&gt;Service Provider&lt;/a&gt; (SP) or an IdP. An SP is an entity that initiates an authentication request to an IdP, which verifies the user's identity. After authentication, the SP processes the response from the IdP to determine whether the user should be granted access to the application. In the previous section, you configured the Descope project as an SP entity for auth and SSO.&lt;/p&gt;

&lt;p&gt;You'll now create an OIDC application within Descope to turn it into an IdP, and subsequently, you'll also integrate it with your authentication flow. Since you're using Descope for both SP and IdP, to avoid cyclic dependency, both these entities should be configured in separate Descope projects.&lt;/p&gt;

&lt;p&gt;First, use the Descope portal to create a new project to host the OIDC application. This tutorial sets the project name as &lt;code&gt;descope-shiny-auth-sso&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%2F55liybj736gikuamf42o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55liybj736gikuamf42o.png" alt="Fig: Create new Descope project" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You also need to create an OIDC application under the newly created project. Navigate to &lt;a href="https://app.descope.com/applications" rel="noopener noreferrer"&gt;Applications&lt;/a&gt; and click the + Application button to register Generic OIDC Application.&lt;/p&gt;

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

&lt;p&gt;Click Create to create the OIDC application and note the Descope OIDC endpoint configuration, as you'll need it while configuring the SSO tenant in the original project.&lt;/p&gt;

&lt;p&gt;To configure OAuth with Posit Connect, you'll also need to create a new client secret. Navigate to the &lt;a href="https://app.descope.com/m2m/accessKeys" rel="noopener noreferrer"&gt;Access Keys&lt;/a&gt; page and click +Access key to create a new access 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%2Fm5hrgh80ljipjv5pslcd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5hrgh80ljipjv5pslcd.png" alt="Fig: Create a new access key" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Descope tenant to use OIDC SSO
&lt;/h3&gt;

&lt;p&gt;To integrate the OIDC application with the authentication flow, you need to configure SSO under the example.com tenant you created under the original Descope SP project. A tenant lets you configure an authentication method using an OIDC or SAML application. This section discusses how to configure SSO using an OIDC application, and in a later section, you'll learn how to configure SSO using SAML.&lt;/p&gt;

&lt;p&gt;Switch back to the Descope SP project and navigate to Tenants, and select the example.com tenant you created earlier. Choose Authentication Methods and select OIDC as the SSO authentication protocol. Make the following changes to configure SSO:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under Tenant Details, add &lt;code&gt;example.com&lt;/code&gt; to the SSO domains.&lt;/li&gt;
&lt;li&gt;Update the SSO configuration by adding the following values:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider name&lt;/strong&gt;: Add a value for the provider. For example, the tutorial uses &lt;code&gt;Descope&lt;/code&gt; as the provider name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID&lt;/strong&gt;: Use the Descope project ID for the &lt;code&gt;descope-shiny-auth-sso&lt;/code&gt; project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret&lt;/strong&gt;: Use the access key that you created above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes&lt;/strong&gt;: It sets the permissions for the access level that will be granted to the authenticated user. Add &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt; as the scopes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer&lt;/strong&gt;: Use the Issuer value from the OIDC application you created in the previous step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Endpoint&lt;/strong&gt;: Use the Authorization URL value from the OIDC application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Endpoint&lt;/strong&gt;: Use the Access Token URL value from the OIDC application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Info Endpoint&lt;/strong&gt;: Use the Access Token URL from the OIDC application by replacing &lt;code&gt;/token&lt;/code&gt; with &lt;code&gt;/userinfo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Finally, Save the changes for them to take effect.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configure SSO redirect URL
&lt;/h3&gt;

&lt;p&gt;You also need to configure the Posit Connect application URL as an SSO redirect URL. Since the Shiny app is already configured to use Descope, it doesn't require any new changes. Grab the application URL, navigate to &lt;a href="https://app.descope.com/settings/authentication/sso" rel="noopener noreferrer"&gt;SSO&lt;/a&gt; under Authentication methods, and set it as the Redirect URL.&lt;/p&gt;

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

&lt;p&gt;Note that the SSO redirect URL needs to be configured in the Descope SP project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the OIDC SSO integration
&lt;/h3&gt;

&lt;p&gt;Finally, to test the integration, open the application in the browser, enter your email ID, and click on Continue with SSO to initiate the SSO flow. Note that the email ID should use &lt;code&gt;example.com&lt;/code&gt; for SSO to work since you didn't configure any other domains in 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%2Fc1c09lljk2al16sihrkb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc1c09lljk2al16sihrkb.png" alt="Fig: SSO login" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll be redirected to the IdP login screen, which, in this case, is hosted by the &lt;code&gt;descope-shiny-auth-sso&lt;/code&gt; Descope project. You can verify this by comparing the project ID in the address bar with the Descope project 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%2F5c2n3utr81ngxnhuwmy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c2n3utr81ngxnhuwmy8.png" alt="Fig: Descope IdP login" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For testing, complete the login using one of the social login options or the magic link, and upon authentication, you'll be redirected back to the Shiny 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%2Fmxmbfi9f40wbgn5dj9m3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxmbfi9f40wbgn5dj9m3.png" alt="Fig: Shiny dashboard after login" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement SAML SSO with Descope
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.descope.com/sso-integrations/applications/saml-apps" rel="noopener noreferrer"&gt;SAML&lt;/a&gt; SSO is an XML-based open authentication standard that lets users log in to multiple web applications using a single set of credentials. SAML is a mature identity federation standard and is widely supported by legacy or internal enterprise applications. It serves a similar purpose to OIDC, but instead of JSON, it uses XML during authentication or authorization requests between the IdP and the SPs.&lt;/p&gt;

&lt;p&gt;In this section, you'll learn how to configure SAML SSO with Descope using a mock SAML application and configuring the tenant to use SAML SSO.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure mock SAML tenant for testing
&lt;/h3&gt;

&lt;p&gt;As you learned in the OIDC SSO section, Descope can act as both SP and IdP as needed, but this tutorial uses Descope as an SP and Mock SAML as an IdP. &lt;a href="https://mocksaml.com/" rel="noopener noreferrer"&gt;Mock SAML&lt;/a&gt; is a SAML 2.0 IdP that can test SAML SSO integrations quickly. If you don't have a SAML SP set up, you can use this tool to &lt;a href="https://docs.descope.com/sso/mock-saml-testing" rel="noopener noreferrer"&gt;test your Descope SAML integration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;a href="https://app.descope.com/tenants" rel="noopener noreferrer"&gt;Tenants&lt;/a&gt; and select the example.com tenant you created earlier. Choose Authentication Methods and change the SSO authentication protocol from OIDC to SAML. Make the following changes to configure SAML SSO:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under Tenant Details, add &lt;code&gt;example.com&lt;/code&gt; to the SSO domains.&lt;/li&gt;
&lt;li&gt;Update the SSO configuration, select the Retrieve the connection details dynamically option, and set the Metadata URL for IdP to the following:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mocksaml.com/api/saml/metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: If you have a SAML SP setup, you can set the Descope Metadata (URL) of the SAML application instead of a Mock SAML URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the SAML SSO integration
&lt;/h3&gt;

&lt;p&gt;To test the integration, open the application in the browser, enter your email ID, and click on Continue with SSO to initiate the SSO flow. Note that the email ID should use &lt;code&gt;example.com&lt;/code&gt; for SSO to work since you didn't configure any other domains in Descope. If you used a mock URL, you'll be redirected to a MockSAML login page where you can confirm your email ID and click 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%2F7lpkxo2fpjiko2eyggbz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7lpkxo2fpjiko2eyggbz.png" alt="Fig: SAML SSO login" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: If you configured a Descope SAML application with SP details, Descope will directly handle the authentication.&lt;/p&gt;

&lt;p&gt;After logging in, you'll be redirected to the dashboard 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%2Fz3bs77hbxnzdmk9jio3n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3bs77hbxnzdmk9jio3n.png" alt="Fig: Shiny dashboard after SAML login" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Access users from Shiny
&lt;/h2&gt;

&lt;p&gt;So you've now added authentication to the Shiny app using OIDC and SAML, but the Shiny application doesn't indicate that the user is logged in. This section discusses how to access the logged-in user metadata and display a welcome message in the UI.&lt;/p&gt;

&lt;p&gt;You'll add a UI element to the sidebar and update the server definition to set the welcome message when the user is authenticated. First, add a &lt;code&gt;welcomeText&lt;/code&gt; element to the sidebar by updating the &lt;code&gt;sidebarPanel&lt;/code&gt; to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;sidebarPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;# Add a welcome text element&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"welcomeText"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;# Existing inputs&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload Expense Data (CSV)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".csv"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;selectInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Select Category:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze"&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;Update the &lt;code&gt;user_authenticated&lt;/code&gt; observer under the server definition to set the welcome message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&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="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;user_info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;# use "name", else fallback to "email", else "User"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_info&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is.null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_info&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is.null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;welcomeText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;renderText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Welcome,"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_name&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="n"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Login successful!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"appContent"&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;Notice that the above code snippet tries to retrieve the user's name from the &lt;code&gt;user_info&lt;/code&gt; object and falls back to email if name is not present.&lt;/p&gt;

&lt;p&gt;Save and republish the changes to test them. Once you log in to the dashboard, a welcome message will be displayed.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Log out a user
&lt;/h2&gt;

&lt;p&gt;Your users also need logout functionality to enable them to end their sessions. To implement logout, first, add a logout button to the sidebar by updating the &lt;code&gt;sidebarPanel&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;sidebarPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"welcomeText"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;# Existing inputs&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload Expense Data (CSV)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".csv"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;selectInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Select Category:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;# Logout button&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"logout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Logout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sign-out-alt"&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;Next, add a &lt;code&gt;descopeLogout&lt;/code&gt; message handler in the &lt;code&gt;descope-auth.js&lt;/code&gt; file that will be invoked when the logout button is clicked:&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;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCustomMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descopeLogout&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wcElement&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;wcElement&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="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;Finally, add a logout observer under the server definition to invoke the &lt;code&gt;descopeLogout&lt;/code&gt; and &lt;code&gt;initDescopeAuth&lt;/code&gt; message handlers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;logout&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="n"&gt;hide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"appContent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Hide main content&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;sendCustomMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"descopeLogout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;sendCustomMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"initDescopeAuth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P2pBl7sYVGg1RWtv4jC7zfO9TnUN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;flowId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sign-up-or-in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;containerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"authContainer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;successInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_authenticated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;errorInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth_error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;Save and republish the changes to test them. Once you log in to the dashboard, you'll notice a Logout button being displayed next to the Analyze 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%2Fic751mtddt78av92e0pu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fic751mtddt78av92e0pu.png" alt="Fig: Logout button" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click the Logout button, you'll be logged out of the application and redirected to the Descope login page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Shiny authentication simpler with Descope
&lt;/h2&gt;

&lt;p&gt;Shiny makes it easier for data engineers and scientists to share their work with others as web applications without any HTML or JavaScript experience. Posit Connect is a publishing platform that lets you deploy Shiny apps directly from RStudio with a click of a button, eliminating the need to provision and maintain a web server.&lt;/p&gt;

&lt;p&gt;In this article, you learned how to implement authentication and authorization in a Shiny app using Descope. You can find this tutorial's complete source code on &lt;a href="https://github.com/maskaravivek/personal-finance-shiny" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Descope is a drag-and-drop customer authentication and identity management platform. Our low- or no-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 like &lt;a href="https://www.descope.com/customers/navan" rel="noopener noreferrer"&gt;Navan&lt;/a&gt;, GoFundMe, &lt;a href="http://you.com/" rel="noopener noreferrer"&gt;You.com&lt;/a&gt;, and Branch Insurance use Descope to reduce user friction, prevent account takeover, and get a unified view of their customer journey. To learn more, &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt; with our auth experts or join our dev community, &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Flet App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 24 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-flet-app-543b</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-flet-app-543b</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-flet" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Creating cross-platform applications has become much easier with frameworks like &lt;a href="https://flet.dev/" rel="noopener noreferrer"&gt;Flet&lt;/a&gt;. Flet allows developers to build modern web, desktop, and mobile apps using Python. Its simplicity and flexibility help developers focus on creating great user experiences without having to worry about platform-specific complexities.&lt;/p&gt;

&lt;p&gt;However, as your application grows, so does the need to secure it. Adding &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;secure authentication&lt;/a&gt; to your application helps to protect sensitive data by ensuring that only authorized users can access it. Descope is a modern authentication and user management platform that simplifies the integration of secure authentication features into your application. By providing out-of-the-box support for &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;, Descope offers a reliable solution for implementing authentication in your Flet app.&lt;/p&gt;

&lt;p&gt;In this guide, you will 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 Flet application. You'll learn how to add basic authentication to your Flet app using Descope &lt;a href="https://www.descope.com/learn/post/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 configure Descope to use &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; as the &lt;a href="https://docs.descope.com/sso-integrations/idp-vs-sp" rel="noopener noreferrer"&gt;identity provider (IdP)&lt;/a&gt;, enabling SSO for your Flet app.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why is adding SSO important for these types of apps?
&lt;/h2&gt;

&lt;p&gt;SSO is essential for B2B applications that serve multiple companies. Each company may already use its own IdP—such as Okta, Google, or Azure—for authentication. By adding SSO, your application allows users to log in with existing credentials, which eliminates the need to create and manage separate accounts. This simplifies the onboarding process and enhances security by relying on trusted authentication providers.&lt;/p&gt;

&lt;p&gt;SSO also &lt;a href="https://www.descope.com/blog/post/sso-benefits" rel="noopener noreferrer"&gt;improves the user experience&lt;/a&gt; by providing seamless access to different services within an organization. It allows employees to use their company credentials to log in, which reduces password fatigue and IT support overhead.&lt;/p&gt;

&lt;p&gt;For businesses handling sensitive data, SSO ensures centralized &lt;a href="https://www.descope.com/learn/post/authorization" rel="noopener noreferrer"&gt;access control&lt;/a&gt;, which makes it easier to enforce security policies while maintaining a smooth login process.&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://app.descope.com/" 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;h2&gt;
  
  
  Creating a Flet application
&lt;/h2&gt;

&lt;p&gt;This tutorial uses a prebuilt starter template so you can focus on implementing authentication. To clone the starter template to your local machine, execute the following command:&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-flet-auth-sso.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cd into the project folder, set up a virtual environment, activate it, and install the project dependencies:&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;descope-flet-auth-sso

python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv

&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate

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;p&gt;Here's an overview of the most important files in this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;main.py:&lt;/strong&gt; Sets up a simple Flet app with two views: a main view and a profile view. The &lt;code&gt;MyApp&lt;/code&gt; class initializes the app, sets the page theme to light, and handles route changes and view pops. When the route changes (for example, when navigating to &lt;code&gt;/profile&lt;/code&gt;), it updates the view accordingly, either showing the profile or the main view. It uses the &lt;code&gt;AuthManager&lt;/code&gt; class to handle user authentication (login/logout). The &lt;code&gt;route_handler&lt;/code&gt; method switches views based on the current route, and &lt;code&gt;view_pop&lt;/code&gt; allows navigating backward through views. Finally, the &lt;code&gt;run&lt;/code&gt; method starts the app with the main view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auth/auth_helpers.py:&lt;/strong&gt; Defines an &lt;code&gt;AuthManager&lt;/code&gt; class that manages the auth-related functions. The &lt;code&gt;handle_login&lt;/code&gt; method navigates the user to the &lt;code&gt;/profile&lt;/code&gt; route while the &lt;code&gt;handle_logout&lt;/code&gt; method navigates the user to the &lt;code&gt;/&lt;/code&gt; route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;views/main_view.py:&lt;/strong&gt; Displays a message with a login button that calls the &lt;code&gt;handle_login&lt;/code&gt; method when clicked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;views/profile_view.py:&lt;/strong&gt; Displays a message with a logout button that calls the &lt;code&gt;handle_logout&lt;/code&gt; method when clicked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can run the application using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flet run &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8550
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command opens the web version of the app. To view it, navigate to &lt;a href="http://localhost:8550" rel="noopener noreferrer"&gt;http://localhost:8550&lt;/a&gt; on your browser, and you should see the demo application homepage with a Login 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%2F43ye32nk43gavipuor3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F43ye32nk43gavipuor3a.png" alt="Root page" width="783" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the Login button and you should be navigated to the profile page:&lt;/p&gt;

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

&lt;p&gt;Right now, the profile page is accessible to anyone, which is not ideal. In the next section, you'll implement authentication to restrict access to authorized users.&lt;/p&gt;

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

&lt;p&gt;To implement authentication in your Flet app, you first need to create a project and design a &lt;a href="https://docs.descope.com/flows" rel="noopener noreferrer"&gt;flow&lt;/a&gt; on your &lt;a href="https://app.descope.com/" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt;. To get started, launch your Descope console, click the project dropdown, and select + Project to create a new 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%2Fvr1ecki7i38dvwm4eoi0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvr1ecki7i38dvwm4eoi0.png" alt="Creating a new project" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Create project page, provide "descope-flet-auth-sso" as the project name and click Create to create 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%2Ftqr4pgs1psvwzq0m1o52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqr4pgs1psvwzq0m1o52.png" alt="Proving project details" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next step is to design a &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope flow&lt;/a&gt;. Flows define the authentication process. You're going to add support for three authentication methods: magic links, social login, and SSO. You need to design a flow that will support all the authentication methods.&lt;/p&gt;

&lt;p&gt;Typically, you would need to design this flow from scratch, but to make it easy for you to follow along, this tutorial uses a pre-prepared flow, which is included in the starter template. You can find it in the project root folder in a file named sign-up-or-in.json. You just need to import this flow into the Descope console. To do this, navigate open the default sign-up-or-in flow in the flow editor by navigating to Flows &amp;gt; sign-up-or-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%2Fw3latwz2u4qfn71ifbnx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3latwz2u4qfn71ifbnx.png" alt="Open a flow in the editor" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the editor, select Import flow / Export flow &amp;gt; Import flow and upload the flow from your local machine:&lt;/p&gt;

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

&lt;p&gt;The flow you imported presents three login methods on the welcome screen: magic link, SSO, and social login. If the user selects the magic link, an email is sent to the provided address. When the user clicks the link in the email, they are asked for additional details if they're a new user. If they're not new, the flow returns a JSON Web Token (JWT) and ends. For social login or SSO, the user is redirected to the appropriate provider. After successful authentication, they receive a JWT, and the flow ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication with Descope
&lt;/h2&gt;

&lt;p&gt;Now that you have implemented the authentication flow in the Descope console, you can go ahead and configure your Flet app to use Descope as an OAuth provider. Flet allows you to implement authentication with any identity provider that supports the &lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth 2.0 Authorization code flow&lt;/a&gt;. By default, Flet ships with a few built-in OAuth providers, such as Azure and Google, but it also allows you to configure a custom OAuth provider, like Descope.&lt;/p&gt;

&lt;p&gt;To configure a custom OAuth provider, you need to configure the following in your Flet app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; A public identifier for the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client secret:&lt;/strong&gt; A secret that is only known to the application and the authorization server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization endpoint:&lt;/strong&gt; The endpoint that initializes the authorization flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token endpoint:&lt;/strong&gt; The endpoint that exchanges an authorization code for access tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User endpoint:&lt;/strong&gt; The endpoint that retrieves the authenticated user's information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URL:&lt;/strong&gt; The URL where the user is redirected to after authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User ID function:&lt;/strong&gt; A function that extracts the user ID from the returned user dictionary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To integrate Descope as the authentication provider for your Flet app, you first need to configure an OIDC app in the Descope dashboard to obtain the necessary endpoints required for the authorization code flow. To do this, navigate to Applications &amp;gt; OIDC default application 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%2F75i14isqhqb2pxsibqe9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75i14isqhqb2pxsibqe9.png" alt="Access the default Descope OIDC app" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to the SP Configuration section and take note of the Client ID, Authorization URL, and Access Token URL. You will use these values in your Flet 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%2Ffcv1525mxtukfbk1u299.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffcv1525mxtukfbk1u299.png" alt="Obtaining credentials" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the user endpoint, you'll use &lt;code&gt;https://api.descope.com/oauth2/v1/userinfo&lt;/code&gt;, which is discussed along with the other endpoints in the &lt;a href="https://docs.descope.com/getting-started/oidc-endpoints#available-oidc-endpoints" rel="noopener noreferrer"&gt;Descope documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You also need to generate a client secret. You can do this by navigating to M2M &amp;gt; + Access Key. On the Generate Access Key form, provide the key name, select Generate Key, 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%2F96gy475miprrlbzxuqer.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96gy475miprrlbzxuqer.png" alt="Generating a secret key" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that you have these values, you can go back to your Flet app and start implementing authentication. First, create a new file named .env 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 shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR-DESCOPE-CLIENT-ID&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR-DESCOPE-CLIENT-SECRET&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com/oauth2/v1/authorize"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com/oauth2/v1/token"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com/oauth2/v1/userinfo"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8550/oauth_callback"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sub"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_SCOPES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openid profile email"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some of the values have been prefilled for you as they will be the same for everyone. However, the client ID and client secret will be unique to you. Make sure you replace the placeholder values with the values you obtained from the Descope console.&lt;/p&gt;

&lt;p&gt;You also need a utility file to extract the environment variables and make them available for use in different parts of the application. Create a new file named config.py 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 python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&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;Config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&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;required&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&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;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;Environment variable &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is required but not set.&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;value&lt;/span&gt;

    &lt;span class="n"&gt;OAUTH_CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_CLIENT_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;OAUTH_CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_SCOPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_SCOPES&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 loads environment variables from a .env file and defines a Config class with a method to retrieve them, raising an error if a required variable is missing. It then uses this method to set various OAuth-related settings as class attributes.&lt;/p&gt;

&lt;p&gt;To define a custom OAuth provider, create a new file named auth_provider.py in the auth folder and add the code below:&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;flet.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuthProvider&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;BaseOAuthProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseOAuthProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&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;Config&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;authorization_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;token_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;redirect_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&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;user_id_fn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_USER_ID_FIELD&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 defines an &lt;code&gt;OAuthProvider&lt;/code&gt; class that inherits from the Flet &lt;code&gt;OAuthProvider&lt;/code&gt; class and initializes it with OAuth configuration settings from the &lt;code&gt;Config&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Open the auth/auth_helpers.py file. Here, you will modify the methods to log in and log out the user instead of just navigating them. First, replace the import statements 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flet&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoginEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, replace the &lt;code&gt;handle_login&lt;/code&gt; method with the following to trigger the login process using the OAuth provider configured for the 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_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="n"&gt;e&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;page&lt;/span&gt;&lt;span class="p"&gt;.&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the &lt;code&gt;handle_logout&lt;/code&gt; method with the following to log the user out by calling the logout function on the 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_logout&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;e&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;page&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the same class, add the following methods to configure methods that will be called when the user is successfully logged in or 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LoginEvent&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;e&lt;/span&gt;&lt;span class="p"&gt;.&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;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/profile&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;on_logout&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;e&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&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;These methods handle login and logout events: &lt;code&gt;on_login&lt;/code&gt; checks for errors during login and navigates to the &lt;code&gt;/profile&lt;/code&gt; route if successful, while &lt;code&gt;on_logout&lt;/code&gt; redirects the user to the home route (&lt;code&gt;/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Open the views/profile_view.py file and replace the &lt;code&gt;get_view&lt;/code&gt; method 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_view&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;return&lt;/span&gt; &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&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;/profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vertical_alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;horizontal_alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;controls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;horizontal_alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;controls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;foreground_image_src&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;page&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;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;picture&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="p"&gt;),&lt;/span&gt;
                        &lt;span class="n"&gt;bgcolor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREY_400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;page&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;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;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;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOLD&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;page&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;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;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_logout&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method returns a view for the profile page that displays the authenticated user's profile picture, name, and email in the center of the page, along with a Logout button.&lt;/p&gt;

&lt;p&gt;Open the main.py 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 python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;auth.auth_provider&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuthProvider&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following line of code to the &lt;code&gt;__init__&lt;/code&gt; method just before &lt;code&gt;self.auth_manager = AuthManager(page=self.page)&lt;/code&gt; to create an instance of the OAuth provider and attach it to the current 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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuthProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the event handler you created earlier to the page by adding the following code at the end of the &lt;code&gt;__init__&lt;/code&gt; method:&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_login&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_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_login&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_logout&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_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_logout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you need to protect the &lt;code&gt;/profile&lt;/code&gt; page to make sure it cannot be accessed by unauthenticated users. To do this, in the &lt;code&gt;route_handler&lt;/code&gt; method, replace the first block of the if statement 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="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;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;/profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;if&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="ow"&gt;is&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;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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&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;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProfileView&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;page&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;auth_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_logout&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_view&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 checks if the current route is &lt;code&gt;/profile&lt;/code&gt;; if the user isn't authenticated, it redirects them to the home page. Otherwise, it loads the &lt;code&gt;ProfileView&lt;/code&gt; and appends it to the page.&lt;/p&gt;

&lt;p&gt;At this point, you should be able to log in to the application using either magic links or social login. In the next section, you will implement SSO.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement 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 a simple identity layer built on top of OAuth 2.0. It helps enable SSO by allowing users to authenticate through trusted IdPs, such as Azure or Okta. With Descope, you can easily integrate OIDC SSO into your app, letting users log in securely with their existing credentials, which streamlines access without the need for new accounts.&lt;/p&gt;

&lt;p&gt;In this section, you'll implement SSO by configuring Okta as the IdP and Descope as the authentication service. To do this, open your Okta admin dashboard and navigate to Applications &amp;gt; Applications &amp;gt; Browse App Catalog:&lt;/p&gt;

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

&lt;p&gt;On the Browse App Integration Catalog page, search for "descope" and select the Descope 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%2Fwxe84xd8ni525v3kk2jk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwxe84xd8ni525v3kk2jk.png" alt="Searching the Descope app" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the app details page, select Add Integration:&lt;/p&gt;

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

&lt;p&gt;On the Add Descope page under General Settings, leave the default settings and click Next:&lt;/p&gt;

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

&lt;p&gt;On the Sign-On Options page, select OpenID Connect as the sign-on method, and under Advanced Sign-on Settings, in the Callback URL field, provide the value &lt;code&gt;https://api.descope.com/v1/oauth/callback&lt;/code&gt;. This is where the user will be redirected after successful authentication by Okta.&lt;/p&gt;

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

&lt;p&gt;Scroll down to the bottom of the page and click Done.&lt;/p&gt;

&lt;p&gt;On the app details page, select the Sign On tab and take note of the 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%2Fl9szok7q5dglr9rqvudx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9szok7q5dglr9rqvudx.png" alt="Obtaining credentials" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll also need a &lt;a href="https://docs.descope.com/b2b/multi-tenancy#tenants" rel="noopener noreferrer"&gt;tenant&lt;/a&gt; to use SSO. Back on your Descope console, navigate to Tenants &amp;gt; + Tenant. On the Create Tenant form, provide the tenant name and click 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%2Fmk6kjbvegw2tseowue8u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk6kjbvegw2tseowue8u.png" alt="Creating a tenant" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the details page of the tenant you just created and under Tenant Settings. Make sure you add your email domain to the list of allowed domains and click Save:&lt;/p&gt;

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

&lt;p&gt;Next, still on the tenant details page, select Authentication Methods &amp;gt; SSO &amp;gt; OIDC. Under Tenant Details &amp;gt; SSO Domains, make sure to provide your email domain. This domain is used 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%2F6tbs879eucdtjalopknb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tbs879eucdtjalopknb.png" alt="Configuring tenant authentication methods" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to the SSO configuration &amp;gt; Account Settings 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; Okta&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; Authorization code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also need to provide the required values for the SSO configuration &amp;gt; Connection Settings section. For this, you need to obtain 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;, which provides standardized metadata for OAuth and OIDC integrations. To obtain this, 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%2F2k986w9647u6i0b13bqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k986w9647u6i0b13bqz.png" alt="Obtaining Okta OAuth endpoints" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go back to the Descope console and provide the values you just obtained from the Okta well-known endpoint in the respective inputs under SSO configuration &amp;gt; Connection Settings 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%2Ffnd1932yc2k7b2x48zoz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnd1932yc2k7b2x48zoz.png" alt="Fully configured tenant" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To ensure that only authorized users can authenticate via the app and access your Flet application, you assign users to the Descope app you installed in your Okta console. To do this, open the app details page in Okta and select Assignments &amp;gt; Assign &amp;gt; Assign to Groups:&lt;/p&gt;

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

&lt;p&gt;On the Assign Descope Groups page, select the Assign button beside the Everyone group to allow all users in your organization to log in via SSO and click Done 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%2F6r9i2pve4eurbtydb8ds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6r9i2pve4eurbtydb8ds.png" alt="Assigning the app to everyone" width="800" height="481"&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;
  
  
  Adding a persistent login feature
&lt;/h2&gt;

&lt;p&gt;Currently, the app does not save the access token obtained from Descope. So if a user logs in, leaves the app, and then returns, they will need to log in again. To improve the user experience, you can add a function that saves the access token to client storage.&lt;/p&gt;

&lt;p&gt;However, you cannot store the token in client storage as plain text as this would make it vulnerable to malicious actors who could access and manipulate it. Instead, you need to encrypt the token using a secret key and save the encrypted value securely.&lt;/p&gt;

&lt;p&gt;Open the .env file in the project root folder and add the following:&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="nv"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cv3Gc9RG-9o8icwTBUEDb0HJC7M5A0b2FxT3WpWvg1I="&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"auth_token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Values for these variables have been provided, but you can change them to any string.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OAUTH_TOKEN_SECRET&lt;/code&gt; will be used to encrypt the access token before saving it to the client storage, while &lt;code&gt;OAUTH_TOKEN_KEY&lt;/code&gt; is the key under which the encrypted token will be saved in the client storage.&lt;/p&gt;

&lt;p&gt;To load the environment variables you just created into the Flet application, open the config.py file and add the following attributes to the &lt;code&gt;Config&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="n"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_TOKEN_KEY&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;Open the auth/auth_helpers.py file and add the following import statements:&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;flet.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decrypt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;encrypt&lt;/code&gt; and &lt;code&gt;decrypt&lt;/code&gt; are utility methods provided by Flet for encrypting text data using a symmetric algorithm. This means that the same key is used for encryption and decryption.&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;on_login&lt;/code&gt; method with the following to modify the &lt;code&gt;on_login&lt;/code&gt; method to encrypt and save the token to client storage once a user is successfully authenticated:&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;on_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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LoginEvent&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;e&lt;/span&gt;&lt;span class="p"&gt;.&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;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Save token in client storage
&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&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;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;encrypted_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt&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;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_SECRET&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encrypted_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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/profile&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 retrieves the access token, encrypts it using the secret key you set earlier, and stores the encrypted token in client storage. It then redirects the user to the profile page.&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;handle_login&lt;/code&gt; method with the following to modify the &lt;code&gt;handle_login&lt;/code&gt; method to check if a valid token is stored in the client storage before redirecting the user to the sign-in 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;saved_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# Retrieve token from client storage
&lt;/span&gt;    &lt;span class="n"&gt;encrypted_token&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_storage&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="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Decrypt retrieved token
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;encrypted_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;saved_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;encrypted_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Log in the user
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;saved_token&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&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;provider&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saved_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;saved_token&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="n"&gt;Config&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="nf"&gt;split&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 retrieves the saved encrypted token from the client storage, decrypts it, and passes it to the &lt;code&gt;self.page.login&lt;/code&gt; method. This way, if the token is valid, the application will obtain the user's info without prompting the user to reauthenticate.&lt;/p&gt;

&lt;p&gt;Lastly, replace the &lt;code&gt;handle_logout&lt;/code&gt; method with the following to modify the &lt;code&gt;handle_logout&lt;/code&gt; method to make sure that it removes the token from storage when a user logs 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_logout&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;e&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_storage&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="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_KEY&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;page&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persistent login should now be working as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demonstrating the application
&lt;/h2&gt;

&lt;p&gt;You can run the application to confirm that everything is working as expected.&lt;/p&gt;

&lt;p&gt;Run the application using the command &lt;code&gt;flet run -w --port 8550&lt;/code&gt;, navigate to &lt;a href="http://localhost:8550" rel="noopener noreferrer"&gt;http://localhost:8550&lt;/a&gt; on your browser, and click the Login button. You should be redirected to the sign-in 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%2Fs5bylau86okg3fwh0dbj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5bylau86okg3fwh0dbj.png" alt="Sign-in page" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, you can choose to sign in with any method, and after successful authentication, you will be redirected to the profile page, where your details are displayed:&lt;/p&gt;

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

&lt;p&gt;You can stop the application and run it again. Then, on the home route, click the Login button. You will be redirected to the profile page without being prompted to sign in again.&lt;/p&gt;

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

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

&lt;p&gt;Implementing authentication and SSO into your Flet app using Descope is a straightforward process that significantly enhances your application's security and user experience. By integrating features like magic link authentication, social logins, and SSO with OIDC, you can offer your users seamless and secure access, whether they are individuals or part of an organization using their existing credentials.&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. 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>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Panel App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 22 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-panel-app-1hk8</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-panel-app-1hk8</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-panel" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://panel.holoviz.org/index.html" rel="noopener noreferrer"&gt;Panel&lt;/a&gt; is an open source Python library for creating interactive data visualization applications. As these applications grow, &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;secure authentication&lt;/a&gt; becomes essential to protect the data, ensure compliance with security standards, and deliver a seamless user experience. &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;, a no-/low-code customer identity and access management (CIAM) platform, simplifies authentication by offering ready-to-use solutions for secure login, &lt;a href="https://www.descope.com/learn/post/sso" rel="noopener noreferrer"&gt;single sign-on (SSO)&lt;/a&gt;, and user access control. Its support for OpenID Connect (OIDC), a widely adopted standard for secure and scalable user authentication, makes it an excellent choice for securing Panel apps, allowing developers to focus on building core features while relying on a &lt;a href="https://www.descope.com/customers" rel="noopener noreferrer"&gt;trusted platform&lt;/a&gt; for authentication.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll learn how to integrate Descope into your Panel application by implementing secure user authentication, configuring access control, and providing a seamless SSO experience for your users. You will start by adding vanilla authentication with Descope via magic links, add support for &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social login&lt;/a&gt;, and lastly, &lt;a href="https://docs.descope.com/sso-integrations/customs-providers-sso-tenants/setup-guides/okta" rel="noopener noreferrer"&gt;implement SSO with Okta&lt;/a&gt; as the identity provider (&lt;a href="https://www.descope.com/learn/post/identity-provider" rel="noopener noreferrer"&gt;IdP&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is adding SSO important for these types of apps?
&lt;/h2&gt;

&lt;p&gt;SSO is often added to data visualization tools because these tools are typically designed for B2B use. Here are some of the advantages of integrating SSO into a data visualization tool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplifying access for end users:&lt;/strong&gt; B2B applications frequently cater to multiple companies with their own employees and systems. Each of these companies will have an IdP—such as Okta, Azure AD, or Google Workspace—which they use for secure access to tools and resources. Integrating SSO into your application allows users to sign in with their existing corporate accounts, which eliminates the need for another set of credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengthening security and compliance:&lt;/strong&gt; SSO ensures that authentication is handled through the organization's IdP, which is often configured with advanced security features like MFA and conditional access policies. This reduces the risk of unauthorized access and strengthens compliance with industry standards such as GDPR and HIPAA, which are crucial for data-driven applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplified management for developers:&lt;/strong&gt; From a developer's perspective, SSO integration reduces the complexity of user account management. With SSO, developers can offload the responsibility of managing user credentials and access roles to the IdP, which not only saves time but also reduces the risk of mishandling sensitive user information.&lt;/p&gt;

&lt;p&gt;Adding SSO to your application improves the user experience and future-proofs it to meet the needs of enterprise customers while maintaining high security standards.&lt;/p&gt;

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

&lt;p&gt;To follow along with this guide, you need to have 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/rL1qI3SdkSQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

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

&lt;p&gt;For simplicity, this tutorial uses a prebuilt application that you add authentication to. To clone the application to your local machine, launch your terminal and execute the following command:&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-panel-auth-sso.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create and activate a virtual environment for your application to run in:&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;descope-panel-auth-sso

python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv

&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install all the dependencies using 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;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;p&gt;The application is now set up. The two most important files in the application are admin.py and user.py. admin.py is used to set up an admin dashboard for a Panel application and will only be accessed by users with the appropriate roles after you have implemented auth and role-based access control. user.py sets up a Panel app that allows users to interact with a data set and view rolling averages and outliers for different variables. You can learn more about this application in the &lt;a href="https://panel.holoviz.org/getting_started/build_app.html" rel="noopener noreferrer"&gt;Panel docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run the application using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;panel serve admin.py user.py &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch your browser and navigate to &lt;a href="http://localhost:5006/" rel="noopener noreferrer"&gt;http://localhost:5006/&lt;/a&gt;. You'll see that you have two Panel apps running:&lt;/p&gt;

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

&lt;p&gt;At the moment, you can open any application. However, later on, you will implement authorization to ensure that the "Admin" application can only be accessed by users with the appropriate roles.&lt;/p&gt;

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

&lt;p&gt;Here, you will create a project on your Descope console and implement an &lt;a href="https://docs.descope.com/flows" rel="noopener noreferrer"&gt;authentication flow&lt;/a&gt; to set up your application's authentication process. This flow will enable your users to authenticate using any of the three methods we discussed: magic links, social logins, and SSO.&lt;/p&gt;

&lt;p&gt;To get started, launch your &lt;a href="https://app.descope.com/home" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt; and select + Project from the project dropdown to create a new 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%2Fnyzywayn6zlfgc19ryid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnyzywayn6zlfgc19ryid.png" alt="Creating a new project" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Create project modal, provide descope-panel-auth-sso as the project name and click Create to create 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%2Fldj9xp0jt9l8d8ezkiom.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldj9xp0jt9l8d8ezkiom.png" alt="Providing project details" width="794" height="808"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To create an authentication flow, select Flows &amp;gt; sign-up-or-in on the Descope console to open the default sign-up-or-in 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%2Fj95g3ahc6ujlw6du0zzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj95g3ahc6ujlw6du0zzo.png" alt="Opening a flow in the flow editor" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As mentioned earlier, you're going to customize this flow to support three different authentication methods: magic links, social logins, and SSO login. This tutorial uses an already-prepared flow that you just need to import.&lt;/p&gt;

&lt;p&gt;The project you set up earlier contains a file named sign-up-or-in.json, which is where you'll find the flow logic. You can import it into the flow editor by selecting Import flow / Export flow &amp;gt; Import flow inside the editor and uploading the flow from your local machine:&lt;/p&gt;

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

&lt;p&gt;The flow you just imported should now have all the authentication methods that you need:&lt;/p&gt;

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

&lt;p&gt;Magic links and social login authentication methods are now fully configured. You will configure SSO later.&lt;/p&gt;

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

&lt;p&gt;In this section, you'll configure Descope as a generic OAuth provider for your Panel application.&lt;/p&gt;

&lt;p&gt;Panel performs authentication through &lt;a href="https://oauth.net/2/" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt;, a widely used protocol that delegates authentication to third-party providers, such as Google and GitHub. By default, Panel ships with support for &lt;a href="https://panel.holoviz.org/how_to/authentication/configuration.html#oauth-provider" rel="noopener noreferrer"&gt;various OAuth providers&lt;/a&gt;, but it also allows you to configure authentication via any Generic OAuth provider that has configurable endpoints, such as Descope.&lt;/p&gt;

&lt;p&gt;To set up authentication with Descope, you need to configure specific settings. These can be passed through environment variables or via CLI arguments when you're running the application. In this guide, we will use environment variables for simplicity.&lt;/p&gt;

&lt;p&gt;Here are the required environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_KEY&lt;/code&gt;: A public identifier for the application&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_SECRET&lt;/code&gt;: A secret that is only known to the application and the authorization server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_AUTHORIZE_URL&lt;/code&gt;: The endpoint that initializes the authorization flow&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_TOKEN_URL&lt;/code&gt;: The endpoint that exchanges an authorization code for access tokens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_USER_URL&lt;/code&gt;: The endpoint that retrieves the authenticated user's information&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_COOKIE_SECRET&lt;/code&gt;: A random, unguessable string for encrypting cookies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_REDIRECT_URI&lt;/code&gt;: The URL where the user is redirected to after authentication&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_ENCRYPTION&lt;/code&gt;: For encrypting user information and access tokens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PANEL_OAUTH_SCOPE&lt;/code&gt;: Defines the scope of permissions requested from the authorization server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will obtain some values for these environment variables from the Descope console and generate others on the terminal.&lt;/p&gt;

&lt;p&gt;Create a new file named .env in the project root folder and add the content below. You will assign values for each environment variable as you proceed.&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="nv"&gt;PANEL_COOKIE_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5006"&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_ENCRYPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_SCOPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_AUTHORIZE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_TOKEN_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PANEL_OAUTH_USER_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To integrate Descope as your authentication provider, you first need to configure an OIDC app in the Descope dashboard. This configuration provides the necessary endpoints and credentials for your Panel application. You could choose to &lt;a href="https://docs.descope.com/sso-integrations/applications/oidc-apps#creating-an-oidc-application" rel="noopener noreferrer"&gt;create a new OIDC application&lt;/a&gt; for this integration, but in this example, you can simply use the default OIDC application that ships with Descope. Access the application by navigating to Applications &amp;gt; OIDC default application 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%2Favkidf7wr27e73zxsgsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favkidf7wr27e73zxsgsl.png" alt="Accessing the default OIDC app" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the OIDC app's details page, scroll down to the SP Configuration section, take note of the Client ID, Authorization URL, and Access Token URL, and assign them to the &lt;code&gt;PANEL_OAUTH_KEY&lt;/code&gt;, &lt;code&gt;PANEL_OAUTH_AUTHORIZE_URL&lt;/code&gt;, and &lt;code&gt;PANEL_OAUTH_TOKEN_URL&lt;/code&gt; environment variables in the .env file, respectively.&lt;/p&gt;

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

&lt;p&gt;For the &lt;code&gt;PANEL_OAUTH_USER_URL&lt;/code&gt; environment variable, assign the value &lt;code&gt;https://api.descope.com/oauth2/v1/userinfo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;PANEL_OAUTH_SCOPE&lt;/code&gt; environment variable, provide the value &lt;code&gt;openid email profile descope.claims descope.custom_claims&lt;/code&gt;. The &lt;code&gt;openid email profile&lt;/code&gt; scopes request basic user information, while the &lt;code&gt;descope.claims descope.custom_claims&lt;/code&gt; scopes request the user roles, permissions, tenants, and custom claims from the IdP. However, for the &lt;code&gt;descope.claims descope.custom_claims&lt;/code&gt; scopes to be provided, you need to go back to your OIDC application and add them to the Supported Claims list under IdP Configuration:&lt;/p&gt;

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

&lt;p&gt;Remember to save the changes.&lt;/p&gt;

&lt;p&gt;To obtain the value for the &lt;code&gt;PANEL_OAUTH_SECRET&lt;/code&gt; environment variable, create an access key by navigating to M2M &amp;gt; + Access Key on the Descope console. Name the key whatever you like and click Generate 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%2F3edbk0ne53awuh0ykhp9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3edbk0ne53awuh0ykhp9.png" alt="Creating an access key" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the value of the generated key, and assign it to the &lt;code&gt;PANEL_OAUTH_SECRET&lt;/code&gt; environment variable in the .env file.&lt;/p&gt;

&lt;p&gt;Make sure your virtual environment is activated, and then execute the following command in your terminal to generate a value for the &lt;code&gt;PANEL_COOKIE_SECRET&lt;/code&gt; environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;panel secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, activate the Python shell by executing the command &lt;code&gt;python&lt;/code&gt; in your terminal, and execute the following code to generate a value for the &lt;code&gt;PANEL_OAUTH_ENCRYPTION&lt;/code&gt; environment variable:&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;cryptography.fernet&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;

&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_key&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Descope is fully configured as a generic OAuth provider for Descope. At this point, you can run the application using the command &lt;code&gt;panel serve admin.py user.py --oauth-provider=generic --dev&lt;/code&gt; and test if authentication is working as expected.&lt;/p&gt;

&lt;p&gt;Keep in mind that only magic links and social login authentication methods should be working at this point. You will work on SSO in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement SSO with OAuth using Descope
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; is an industry-standard protocol for authorization that enables applications to securely access user resources without requiring them to share their credentials. OAuth acts as a "middleman" that allows users to grant limited access to their information stored on an IdP to the Service Provider (SP), such as your application. When combined with OIDC, which is built on top of OAuth, this protocol also enables SSO, which allows users to log in once with their existing credentials and gain access to multiple applications or systems without having to log in again.&lt;/p&gt;

&lt;p&gt;In this section, you'll configure Okta as the IdP and Descope as the OAuth service to integrate authentication and SSO into your Panel application.&lt;/p&gt;

&lt;p&gt;To get started, go to your Okta admin dashboard and navigate to Applications &amp;gt; Applications &amp;gt; Browse App Catalog:&lt;/p&gt;

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

&lt;p&gt;On the Browse App Integration Catalog page, search for "Descope" and select the Descope application:&lt;/p&gt;

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

&lt;p&gt;Next, on the application details page, select + Add Integration:&lt;/p&gt;

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

&lt;p&gt;On the Add Descope page, under General Settings, provide Descope Panel Auth and SSO as the Application label and click Next:&lt;/p&gt;

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

&lt;p&gt;On the Sign-On Options page, select OpenID Connect as the sign-on method, and under Advanced Sign-on Settings, in the Callback URL field, provide the value &lt;code&gt;https://api.descope.com/v1/oauth/callback&lt;/code&gt;. This is the URL the user will be redirected to after successful authentication by the IdP.&lt;/p&gt;

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

&lt;p&gt;Scroll down to the bottom of the page and select Done.&lt;/p&gt;

&lt;p&gt;On the app details page, select the Sign On tab and take note of your client ID and client 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%2Fvym8n8afjnfgxstxquy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvym8n8afjnfgxstxquy4.png" alt="Retrieving OIDC credentials" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Head back to the Descope console and select Tenants &amp;gt; + Tenant to create a new tenant. On the Create Tenant modal, provide Descope Panel Auth and SSO as the tenant name and click 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%2F24nad6iwv083d9uep4as.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24nad6iwv083d9uep4as.png" alt="Creating a new tenant" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the tenant details page, and under Tenant Settings, add your email domain to the list of allowed domains 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%2Ffrshlwl4232jbf3v8drj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffrshlwl4232jbf3v8drj.png" alt="Adding tenant email domain" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still on the tenant details page, navigate to Authentication Methods &amp;gt; SSO and select OIDC. Under Tenant Details, add your email domain, which will help to determine the SSO configuration to load when a user chooses to authenticate with 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%2Fmjulljk6i3qyrs33glw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmjulljk6i3qyrs33glw7.png" alt="Configuring OIDC SSO" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to the SSO configuration &amp;gt; Account Settings 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; Okta&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; Authorization code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also need to provide the values for the SSO configuration &amp;gt; Connection Settings section. For these, you'll need to obtain OAuth endpoints from the Okta "well-known" configuration. To obtain this, 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%2F11hn902o9g3bjvyllvro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11hn902o9g3bjvyllvro.png" alt="Obtaining Okta OAuth endpoints" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Head back to the Descope console, input these values in their respective endpoints under SSO configuration &amp;gt; Connection Settings, and save the changes.&lt;/p&gt;

&lt;p&gt;Under SSO Mapping &amp;gt; User Attribute Mapping, you can choose to configure how attributes from Okta will be mapped to Descope, but we will not be doing so in this guide. The default mapping is sufficient for this example. For more information on how to add custom claims to your Okta OIDC tokens, check out the &lt;a href="https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/main/#add-a-custom-claim-to-a-token" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You also need to assign users to the Descope app in Okta to grant them access. This step ensures that only authorized users can authenticate via the app and access your Panel application. To do this, open the app details page in Okta and select the Assignments &amp;gt; Assign &amp;gt; Assign to Groups:&lt;/p&gt;

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

&lt;p&gt;On the Assign Descope Panel Auth and SSO to Groups page, select the Assign button beside the Everyone group to allow all users in your organization to log in via SSO and click Done 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%2Fz25xih501hw2v5wlwqke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz25xih501hw2v5wlwqke.png" alt="Assigning the app to everyone" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Add authorization
&lt;/h2&gt;

&lt;p&gt;Authorization allows your app to determine what actions an authenticated user can perform and the data they can access. For example, in your Panel application, administrators might have access to advanced analytics and management tools, while regular users can only view specific dashboards.&lt;/p&gt;

&lt;p&gt;At the moment, any user can access any dashboard in your application. Let's limit what dashboard a user can access based on their roles.&lt;/p&gt;

&lt;p&gt;To do this, create a new file named main.py in the root project folder and add the following code, which retrieves user roles and redirects them to the appropriate dashboard:&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;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;panel&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;

&lt;span class="c1"&gt;# Load env variables
&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Enable Panel extension
&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sizing_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stretch_width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Define the authorization callback
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_info&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 callback to redirect users based on roles.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Extract user roles from the user_info dict
&lt;/span&gt;    &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_info&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&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="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;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;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&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="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="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="c1"&gt;# Assign the authorization callback
&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorize_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;authorize&lt;/code&gt; function checks the user's roles from the &lt;code&gt;user_info&lt;/code&gt; dictionary, which includes details about their roles in a specified tenant (retrieved from an environment variable). Depending on whether the user has the Tenant Admin role, they are redirected to either the admin dashboard (/admin) or the user dashboard (/user).&lt;/p&gt;

&lt;p&gt;You'll also need to protect the admin dashboard so that users don't try to manually navigate to the /admin endpoint on their browser. To do this, Open the admin.py 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 python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;panel&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Enable Panel extension
&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sizing_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stretch_width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Extract roles and permissions
&lt;/span&gt;&lt;span class="n"&gt;tenant_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_info&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&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="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="n"&gt;tenant_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="n"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tenant_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;permissions&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;# Admin dashboard content
&lt;/span&gt;&lt;span class="n"&gt;admin_content&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;
# You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re an admin. You have access to more data and functionality.

#### Your Roles:
&amp;lt;ul&amp;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;join&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;li&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; for role in roles) if roles else &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="n"&gt;assigned&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;/ul&amp;gt;

#### Your Permissions:
&amp;lt;ul&amp;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;join&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;li&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; for permission in permissions) if permissions else &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt; &lt;span class="n"&gt;assigned&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;/ul&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# Authorization callback for the admin dashboard
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_admin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_info&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;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;roles&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;True&lt;/span&gt;
    &lt;span class="k"&gt;return&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="c1"&gt;# Assign the authorization callback
&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorize_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorize_admin&lt;/span&gt;

&lt;span class="c1"&gt;# Create and configure the Material template
&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaterialTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Panel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin 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;main&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;admin_content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Serve the admin dashboard
&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;servable&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 the admin dashboard where the user roles and permissions are displayed and also checks if the user has the Tenant Admin role. If not, they are redirected to the user dashboard.&lt;/p&gt;

&lt;p&gt;Open the .env file in the project root folder and add the following content, replacing the placeholder value with the ID of the tenant you created earlier on the Descope console:&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="nv"&gt;DESCOPE_TENANT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR-DESCOPE-TENANT-ID&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, if you run the application using the command &lt;code&gt;panel serve admin.py user.py --oauth-provider=generic --dev&lt;/code&gt; and authenticate successfully, you'll be redirected to the default Panel landing page, which lists all the running applications. You need to add a function that automatically redirects the user to the appropriate app. To do this, create a new file named index.html 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 html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Refresh"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"0; url='/main"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Redirecting...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This HTML file acts as a redirect page for your Panel application. After successful authentication, users are redirected to &lt;a href="http://localhost:5006" rel="noopener noreferrer"&gt;http://localhost:5006&lt;/a&gt;, where this file automatically navigates them to the /main route. The /main route loads the main.py file, which determines whether users should be redirected to the admin or user dashboard based on their roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refreshing the access tokens
&lt;/h2&gt;

&lt;p&gt;In Panel, access tokens are cached as cookies. Depending on the OAuth provider you're using, these tokens may eventually expire. For example, Okta access tokens expire after thirty days. This means a user might remain logged into your application even though their access token has expired since it is cached.&lt;/p&gt;

&lt;p&gt;To ensure you always have access to a valid access token, open the .env file in the project root folder and add the following line:&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="nv"&gt;PANEL_OAUTH_REFRESH_TOKENS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that if a user tries to access your application with an expired token, the application will automatically fetch a new access token using the saved refresh token. If no refresh token is available, the user will be prompted to reauthenticate. As long as the user's session is active, the application will also schedule token refreshes ten seconds before the access token expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a logout feature
&lt;/h2&gt;

&lt;p&gt;Allowing users to log in to your Panel application is as simple as adding a widget and configuring the widget to redirect the user to the /logout endpoint.&lt;/p&gt;

&lt;p&gt;First, open the admin.py and add the following code below the &lt;code&gt;admin_content&lt;/code&gt; variable:&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;# Logout button
&lt;/span&gt;&lt;span class="n"&gt;logout_widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Button&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;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;button_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;danger&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;align&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Configure logout functionality
&lt;/span&gt;&lt;span class="n"&gt;logout_widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;js_on_click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;window.location.href = &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code creates a Logout button that redirects the user to the /logout URL when clicked.&lt;/p&gt;

&lt;p&gt;Add this button to the dashboard's header by replacing the code that creates the Material template 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="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaterialTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Panel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin 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;header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;logout_widget&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;admin_content&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;You also need to do the same to the user.py file. Open the file and add the following code below &lt;code&gt;signma_widget = pn.widgets.Button(...)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Logout button
&lt;/span&gt;&lt;span class="n"&gt;logout_widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Button&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;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;button_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;danger&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;align&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Configure logout functionality
&lt;/span&gt;&lt;span class="n"&gt;logout_widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;js_on_click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;window.location.href = &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, modify the code that creates the Material template, like this:&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;# Configure the template with the logout button in the header
&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaterialTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Panel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User 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;header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;logout_widget&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;variable_widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sigma_widget&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bound_plot&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;h2&gt;
  
  
  Testing the application
&lt;/h2&gt;

&lt;p&gt;Now that everything is fully configured, you can test the application. To do this, make sure your virtual environment is still active and execute 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;panel serve main.py admin.py user.py &lt;span class="nt"&gt;--oauth-provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;generic &lt;span class="nt"&gt;--index&lt;/span&gt; index.html &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;a href="http://localhost:5006" rel="noopener noreferrer"&gt;http://localhost:5006&lt;/a&gt; on your browser, and you should be navigated to a sign-in page that is powered by the Descope flow you configured 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%2F9cj7dnvsbnwydqpcuimb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9cj7dnvsbnwydqpcuimb.png" alt="Sign-in page" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On this page, you can choose to authenticate using any method. After successful authentication, you will be redirected to the user 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%2Fa8v7up8el07tlhryudcz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8v7up8el07tlhryudcz.png" alt="User dashboard" width="800" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This shows that the authorization callback is working as expected as the user was not redirected to the admin dashboard.&lt;/p&gt;

&lt;p&gt;Sign out of the app by clicking the Logout button. Go to the Descope console and navigate to the &lt;a href="https://app.descope.com/users" rel="noopener noreferrer"&gt;Users&lt;/a&gt; page. Here, identify the user you just signed in with, assign them the Tenant Admin 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%2Fhfynwya36ccutxqr5n72.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhfynwya36ccutxqr5n72.png" alt="Assigning the Tenant Admin role" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go back to &lt;a href="http://localhost:5006" rel="noopener noreferrer"&gt;http://localhost:5006&lt;/a&gt; on your browser and sign in again with the same credentials, and you'll now have access 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%2Ftukclw26w4pgnhaklbny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftukclw26w4pgnhaklbny.png" alt="Admin dashboard" width="800" height="173"&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 secure authentication and SSO into a Panel application using Descope. From implementing secure authentication flows on the Descope console to integrating Okta as an IdP and implementing role-based access control, you now have the essential skills to enhance both security and user experience in your Panel apps. As a next step, you can explore additional features like multifactor authentication (MFA) and support for multiple &lt;a href="https://docs.descope.com/sso-integrations/customs-providers-sso-tenants" rel="noopener noreferrer"&gt;SSO providers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Descope is a drag-and-drop customer authentication and identity management platform. Our no-/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 like GoFundMe, Navan, &lt;a href="http://you.com/" rel="noopener noreferrer"&gt;You.com&lt;/a&gt;, and Branch use Descope to reduce user friction, prevent account takeover, and get a unified view of their customer journey. 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>datascience</category>
      <category>python</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Next.js 15 vs. Next.js 16: What's the Difference?</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 20 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/nextjs-15-vs-nextjs-16-whats-the-difference-1fjo</link>
      <guid>https://dev.to/descope/nextjs-15-vs-nextjs-16-whats-the-difference-1fjo</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/nextjs15-vs-nextjs16" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next.js 16 marks a significant shift for the framework. What started as a simple SSR helper is now a comprehensive full-stack React engine.&lt;/p&gt;

&lt;p&gt;This release brings major changes: &lt;a href="https://nextjs.org/blog/turbopack-incremental-computation" rel="noopener noreferrer"&gt;Turbopack&lt;/a&gt; replaces Webpack as the default bundler, &lt;a href="https://nextjs.org/docs/app/getting-started/cache-components#how-rendering-works-with-cache-components" rel="noopener noreferrer"&gt;Cache Components&lt;/a&gt; introduces Partial Pre-Rendering (PPR), and middleware.ts becomes proxy.ts. The performance improvements are substantial, but you still may not be ready to switch over. You might be concerned about bugs, don't have time for migration work, or simply aren't ready to abandon Webpack yet.&lt;/p&gt;

&lt;p&gt;This article breaks down what's actually different between Next.js 15 and 16, so you can decide whether to upgrade now or wait until &lt;a href="https://nextjs.org/support-policy" rel="noopener noreferrer"&gt;v15's LTS support expires&lt;/a&gt;. If you do decide to switch over, we've included a short migration guide at the end to help ease your transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js 15 vs. 16 at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Next.js 15&lt;/th&gt;
&lt;th&gt;Next.js 16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bundler&lt;/td&gt;
&lt;td&gt;Webpack as the default bundler, with Turbopack as an optional/experimental alternative.&lt;/td&gt;
&lt;td&gt;Turbopack replaces Webpack, offering up to 10× faster &lt;a href="https://nextjs.org/docs/architecture/fast-refresh" rel="noopener noreferrer"&gt;Fast Refresh&lt;/a&gt; and 5× faster builds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching Model&lt;/td&gt;
&lt;td&gt;Fetch requests are cached by default with limited Partial Pre‑Rendering (PPR).&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://nextjs.org/docs/app/getting-started/cache-components" rel="noopener noreferrer"&gt;Cache Components&lt;/a&gt; with Partial Pre‑Rendering (PPR) for instant navigation and predictable caching defaults.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware Architecture&lt;/td&gt;
&lt;td&gt;Middleware API for routing and edge logic.&lt;/td&gt;
&lt;td&gt;Middleware replaced by proxy.ts for clearer network boundaries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Version &amp;amp; Compiler&lt;/td&gt;
&lt;td&gt;Introduced React 19 support along with the Speedy Web Compiler (SWC) for &lt;a href="https://quantum.cloud.ibm.com/docs/guides/transpile" rel="noopener noreferrer"&gt;transpilation&lt;/a&gt;.&lt;/td&gt;
&lt;td&gt;Offers full React 19.2 support and uses SWC with Turbopack integration for faster builds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Optimization&lt;/td&gt;
&lt;td&gt;Next.js Image component with built‑in optimization (WebP, AVIF).&lt;/td&gt;
&lt;td&gt;Continued Image optimization and tighter integration with Turbopack for faster builds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMP Support Status&lt;/td&gt;
&lt;td&gt;AMP is deprecated, but legacy support is still available.&lt;/td&gt;
&lt;td&gt;AMP is fully deprecated and no longer supported in v16.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance and bundling
&lt;/h2&gt;

&lt;p&gt;When deciding whether you should move from Next.js 15 to 16, performance is often the most important consideration.&lt;/p&gt;

&lt;p&gt;The bundler plays a big role in the framework's performance. Next.js has used Webpack as the default bundler since the first version of the framework was released in 2016. &lt;a href="https://vercel.com/blog/turbopack-moving-homes" rel="noopener noreferrer"&gt;Vercel's full move&lt;/a&gt; to Turbopack is one of the reasons Next.js 16 was such a pivotal release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js 15: Webpack default, Turbopack opt-in
&lt;/h3&gt;

&lt;p&gt;As with the three previous iterations (v14, v13, and v12), Next.js 15 uses &lt;a href="https://webpack.js.org/blog/2020-10-10-webpack-5-release/" rel="noopener noreferrer"&gt;version 5 of Webpack&lt;/a&gt;. It offered improved &lt;a href="https://www.geeksforgeeks.org/system-design/types-of-cache/#2-disk-cache" rel="noopener noreferrer"&gt;disk caching&lt;/a&gt;, optimized &lt;a href="https://nextjs.org/docs/architecture/fast-refresh" rel="noopener noreferrer"&gt;Fast Refresh&lt;/a&gt;, more stable long-term caching of assets, and &lt;a href="https://webpack.js.org/guides/tree-shaking/" rel="noopener noreferrer"&gt;enhanced tree shaking&lt;/a&gt; over its predecessors.&lt;/p&gt;

&lt;p&gt;In addition to the default implementation of WebPack, Next.js 15 also allowed developers to opt into an alpha version of Turbopack (using the --turbopack flag), a feature that had been present since &lt;a href="https://nextjs.org/blog/next-13" rel="noopener noreferrer"&gt;Next.js 13&lt;/a&gt;. This inclusion allowed developers to experiment with and acclimate themselves to the bundler. It also allowed them to influence development by testing, reporting bugs, and sharing their ideas on how it could be improved.&lt;/p&gt;

&lt;p&gt;Despite being in alpha, Turbopack was already showcasing boosts in build speed and hot reload times compared to Webpack; improvements that stemmed from its &lt;a href="https://dev.to/pranta/why-rewriting-everything-in-rust-wont-solve-all-your-problems-24d0"&gt;rewrite in Rust&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js 16: Turbopack as default bundler
&lt;/h3&gt;

&lt;p&gt;Next.js 16 fully replaced Webpack with Turbopack, much to the chagrin of developers who had an affinity for Webpack and disliked the fact that everything was being &lt;a href="https://dev.to/pranta/why-rewriting-everything-in-rust-wont-solve-all-your-problems-24d0"&gt;rewritten in Rust&lt;/a&gt;. However, Turbopack's use of Rust is precisely why it can outperform its JavaScript-based predecessor so easily.&lt;/p&gt;

&lt;p&gt;Turbopack uses Rust's low-level control, &lt;a href="https://doc.rust-lang.org/book/ch16-00-concurrency.html" rel="noopener noreferrer"&gt;fearless concurrency&lt;/a&gt;, and &lt;a href="https://docs.rs/crate/persistentcache/latest" rel="noopener noreferrer"&gt;persistent caching&lt;/a&gt; to deliver incremental bundling where only changed files are rebuilt as opposed to the entire dependency graph. This also makes &lt;a href="https://webpack.js.org/concepts/hot-module-replacement/" rel="noopener noreferrer"&gt;Hot Module Replacement (HMR)&lt;/a&gt; more efficient, since faster builds allow HMR to inject updated modules into the browser runtime more quickly.&lt;/p&gt;

&lt;p&gt;Since Rust avoids unpredictable pauses (like garbage collection in JavaScript), it allows for incremental updates to be applied consistently and quickly, which also makes cold builds faster.&lt;/p&gt;

&lt;p&gt;Let's use &lt;a href="https://github.com/r2hu1/MySocials" rel="noopener noreferrer"&gt;MySocials&lt;/a&gt; to showcase this. The Next.js 15 dev server took 35.6 seconds to initiate and run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzszie80dbpgro503x4f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzszie80dbpgro503x4f.png" alt="MySocials Next.js 15 dev server initiation and runtime" width="390" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After upgrading the project to Next.js 16, the dev server launch time was cut down to 6 seconds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkx6l6m2prcb3pnel0j8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkx6l6m2prcb3pnel0j8i.png" alt="MySocials Next.js 16 dev server initiation and runtime" width="375" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A warm build was even quicker at 3.8 seconds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxn85yj20s35egf4vnsk4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxn85yj20s35egf4vnsk4.png" alt="MySocials Next.js 16 warm build dev server initiation and runtime" width="405" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also found that compilation in development mode took Next.js &lt;code&gt;15.1&lt;/code&gt; seconds to complete, while its v16 counterpart was done in &lt;code&gt;21&lt;/code&gt; seconds. After editing the footer and reloading, v15 recompilation in Dev mode took &lt;code&gt;10.6&lt;/code&gt; seconds, compared to &lt;code&gt;4.6&lt;/code&gt; seconds in Next.js 16. In both instances, the changes were made available to the browser almost immediately thanks to HMR. Following additional rounds of testing and benchmarking, we found that a full Next.js 15 build took &lt;code&gt;251.67&lt;/code&gt; seconds, whereas the v16 build took &lt;code&gt;36.25&lt;/code&gt; seconds.&lt;/p&gt;

&lt;p&gt;Here's a side-by-side breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Next.js 15 (WebPack)&lt;/th&gt;
&lt;th&gt;Next.js 16 (Turbopack)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold Startup Time&lt;/td&gt;
&lt;td&gt;35.6s&lt;/td&gt;
&lt;td&gt;6s (Nearly 6x faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm Startup Time&lt;/td&gt;
&lt;td&gt;8.6s&lt;/td&gt;
&lt;td&gt;3.8s (2.2x faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev Compilation Speed&lt;/td&gt;
&lt;td&gt;121.7s&lt;/td&gt;
&lt;td&gt;21.5s (5~x faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMR / Fast Refresh Latency&lt;/td&gt;
&lt;td&gt;10.6s&lt;/td&gt;
&lt;td&gt;4.6s (2~x faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full production build&lt;/td&gt;
&lt;td&gt;251.67s&lt;/td&gt;
&lt;td&gt;36.25s (7~x faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;Next.js 16's new optimizations and features let you rapidly apply and test hot fixes. These are meaningful performance boosts that have short and long-term implications on the productivity of your team. Turbopack's incremental compilation is particularly favorable in monorepos and large apps with hundreds of packages. Next.js 16 only recompiles files that have new changes (along with their direct dependencies), saving you time and improving your project's ability to scale.&lt;/p&gt;

&lt;p&gt;With that being said, it's important to note that Turbopack is still maturing, and several bugs &lt;a href="https://github.com/vercel/next.js/issues?q=is%3Aissue%20state%3Aopen%20Turbopack" rel="noopener noreferrer"&gt;have been reported and still persist&lt;/a&gt;. Contrastingly, Webpack is stable, reliable, and extremely well-supported with a thriving ecosystem of plugins and loaders. You could also always use Turbopack as an alternative bundler in Next.js 15.&lt;/p&gt;

&lt;p&gt;However, Next.js 15 uses an alpha version of Turbopack. This means it's less stable and can only be run in dev mode. Features like Cache Components and React 19.2 support are also missing. Upgrading to Next.js 16 would allow you to take full advantage of Turbopack's capabilities. Although Turbopack is the default bundler, Next.js 16 still has full Webpack support. This means you can revert to it at any time after upgrading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache components and use cache
&lt;/h2&gt;

&lt;p&gt;v16 changes how caching is handled in Next.js. Where its predecessor used implicit caching as a default, Next.js 16 introduces a new explicit caching model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem with implicit caching in Next.js 15
&lt;/h3&gt;

&lt;p&gt;Next.js automatically decides what to cache and for how long, based on the &lt;a href="https://nextjs.org/docs/15/app/guides/caching#apis" rel="noopener noreferrer"&gt;APIs&lt;/a&gt; you use and where you use them. For example, data returned from &lt;a href="https://nextjs.org/docs/15/app/guides/caching#fetch" rel="noopener noreferrer"&gt;fetch requests&lt;/a&gt; is uncached unless you explicitly opt in, but rendering work and certain data requests are cached per &lt;a href="https://medium.com/@abdooy640/understanding-the-request-lifecycle-in-next-js-a-deep-dive-bcab508bfcb8" rel="noopener noreferrer"&gt;request lifecycle&lt;/a&gt; to improve performance.&lt;/p&gt;

&lt;p&gt;Because earlier versions of Next.js leaned heavily on implicit caching, many developers assumed that repeated calls to the same endpoint would be cached automatically. However, if you didn't explicitly set &lt;a href="https://nextjs.org/docs/15/app/api-reference/functions/fetch#optionscache" rel="noopener noreferrer"&gt;options&lt;/a&gt; like &lt;code&gt;force-cache or next:{ revalidate: 60 }&lt;/code&gt;, every request would hit the API fresh. This led to unexpected performance issues, especially when developers thought they were benefiting from caching but weren't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nextjs.org/docs/app/guides/caching#segment-config-options" rel="noopener noreferrer"&gt;Route segment&lt;/a&gt; caching was as confusing (if not more) because of how extensively caching behavior relied on the data-fetching API used in the segment. This meant that each route segment, whether a page, layout, or nested child, would be cached non-uniformly. For example, a segment using &lt;code&gt;getStaticProps&lt;/code&gt; would be cached at build time, while one using &lt;code&gt;getServerSideProps&lt;/code&gt; would fetch fresh data on every request. This implicit link between API choice and caching made it difficult to reason about how segments behaved without carefully tracking which API was in play.&lt;/p&gt;

&lt;p&gt;It could also lead to accidental over-caching. If developers used &lt;code&gt;getStaticProps&lt;/code&gt; without a revalidate &lt;a href="https://nextjs.org/docs/app/guides/caching#duration-1" rel="noopener noreferrer"&gt;interval&lt;/a&gt;, pages would remain cached indefinitely, even if the underlying data changed. This led to stale content being served to users until a new build was triggered.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Next.js 16 improves caching
&lt;/h3&gt;

&lt;p&gt;Next.js 16 makes caching explicit, granular, and predictable through &lt;a href="https://nextjs.org/docs/app/getting-started/cache-components" rel="noopener noreferrer"&gt;Cache Components&lt;/a&gt;. This feature addresses the pitfalls of &lt;code&gt;fetch&lt;/code&gt; caching by letting you wrap expensive components and functions in a cache boundary that you specify through the &lt;a href="https://nextjs.org/docs/app/api-reference/directives/use-cache" rel="noopener noreferrer"&gt;"use cache" directive&lt;/a&gt;. It essentially allows you to control how these elements should be cached and revalidated.&lt;/p&gt;

&lt;p&gt;This goes for route segment caching, too. Cache Components provide each segment or component with its own cache boundary. Because caching is opt-in and configurable, the risk of over-caching reduces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code example: before vs. after
&lt;/h3&gt;

&lt;p&gt;Here's an example showing implicit caching in v15:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pages/products.js (Next.js 15)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStaticProps&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;res&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="s2"&gt;https://api.example.com/products&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;products&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;res&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// refresh every 60 seconds&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="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductsPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;products&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="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&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;The &lt;a href="https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props" rel="noopener noreferrer"&gt;getStaticProps function&lt;/a&gt; automatically caches the page at build time as static HTML until the next build is initiated. You'll notice the inclusion of &lt;code&gt;revalidate&lt;/code&gt; in the &lt;code&gt;return&lt;/code&gt;. Without it, you risk accidental overcaching (stale data until a rebuild). &lt;/p&gt;

&lt;p&gt;v16 uses explicit caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/products/page.js (Next.js 16)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProducts&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;use cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Explicit directive: cache this function's result&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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="s2"&gt;https://api.example.com/products&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;json&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="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductsPage&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;products&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;getProducts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use cache&lt;/span&gt;&lt;span class="dl"&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="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&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;In this example, the &lt;code&gt;"use cache"&lt;/code&gt; directive is called on both the function (&lt;code&gt;getProducts&lt;/code&gt;) and the component level (&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;). The &lt;code&gt;getProducts&lt;/code&gt; function result is cached, while the rendered UI fragments are refreshed (revalidated) based on the &lt;a href="https://nextjs.org/docs/app/api-reference/directives/use-cache#revalidation" rel="noopener noreferrer"&gt;default profile revalidation rules&lt;/a&gt;. You can also place the &lt;code&gt;"use case"&lt;/code&gt; on the file-level (instead of on each function/component), essentially caching the entire page.&lt;/p&gt;

&lt;p&gt;While the v16 example uses time-based intervals in its current form, you can explicitly set it to use &lt;a href="https://medium.com/@sureshdotariya/next-js-16-tag-based-cache-invalidation-vs-isr-the-ultimate-guide-to-lightning-fast-updates-2c5aea56d832" rel="noopener noreferrer"&gt;tag-based invalidation&lt;/a&gt; or manual refresh. This lets you set clear revalidation rules, making updates predictable and easier to manage, instead of relying on implicit &lt;a href="https://nextjs.org/docs/pages/guides/incremental-static-regeneration" rel="noopener noreferrer"&gt;Incremental Static Regeneration (ISR)&lt;/a&gt; rules. By moving from implicit, API‑driven caching in v15 to explicit cache boundaries in v16, developers gain a simpler, more consistent mental model. You no longer have to guess how caching interacts with route segments or fetch calls; it's declared in code and easy to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing and edge changes
&lt;/h2&gt;

&lt;p&gt;Next.js 16 replaces &lt;code&gt;middleware.ts&lt;/code&gt; with &lt;code&gt;proxy.ts&lt;/code&gt;. This change was mainly to clarify the layer's role. Many developers found the name misleading or confusing (and for good reason). It was never a catch-all "middleware" layer, but instead a proxy that intercepts and forwards requests at the network boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with Next.js 15's middleware.ts
&lt;/h3&gt;

&lt;p&gt;Before the change, many teams used the middleware (now proxy) layer for complex logic (authentication, database queries, heavy transformations), which was not its intended purpose. Developers would confuse it with &lt;a href="http://express.js/" rel="noopener noreferrer"&gt;Express.js middleware&lt;/a&gt;, which encouraged developers to treat it as a general logic layer rather than a lightweight routing tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js 16's introduction of proxy.ts
&lt;/h3&gt;

&lt;p&gt;With proxy.ts, routing rules are explicit and separated from caching. Proxy runs at the edge by default, which aligns with Vercel's push toward &lt;a href="https://www.researchgate.net/figure/Basic-overview-of-edge-first-architecture_fig2_396393191" rel="noopener noreferrer"&gt;edge‑first architecture&lt;/a&gt; for Next.js 16 and future iterations of the framework. This ensures rewrites and redirects happen close to the user (&lt;a href="https://ehosseini.info/articles/edge-rendering/" rel="noopener noreferrer"&gt;edge side&lt;/a&gt;), improving latency.&lt;/p&gt;

&lt;p&gt;Developers should think of the proxy as a network gatekeeper, not a hidden logic layer. This reduces confusion around where to put caching, revalidation, or authentication logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration considerations
&lt;/h3&gt;

&lt;p&gt;When migrating from &lt;code&gt;middleware.ts&lt;/code&gt;/&lt;code&gt;middleware.js&lt;/code&gt; in Next.js 15 to &lt;code&gt;proxy.ts&lt;/code&gt;/&lt;code&gt;proxy.js&lt;/code&gt; in Next.js 16, you need to consider how your existing middleware logic maps to proxy rules. Simple routing logic from middleware maps cleanly to proxy rules, but heavier business logic should be moved into route handlers or server actions. In practice, if your middleware only rewrote &lt;code&gt;/old-route&lt;/code&gt; to &lt;code&gt;/new-route&lt;/code&gt; or redirected unauthenticated users to /login, those rules migrate directly into &lt;code&gt;proxy.ts&lt;/code&gt; without much change.&lt;/p&gt;

&lt;p&gt;The second consideration is identifying what breaks and what doesn't. What breaks are cases where the original middleware layer was overloaded with responsibilities it wasn't designed for, like database queries, complex authentication flows, or caching logic. You'll need to refactor those into server components, API routes, or dedicated backend services. The core routing behaviors, like rewrites, redirects, and header manipulation, are safe.&lt;/p&gt;

&lt;p&gt;Let's say your &lt;code&gt;middleware.js&lt;/code&gt; file looks like this:&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;// middleware.js (Next.js 15)&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;NextResponse&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;next/server&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;nextUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Redirect unauthenticated users&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;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="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="s2"&gt;auth&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;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;/login&lt;/span&gt;&lt;span class="dl"&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;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Rewrite /old-route to /new-route&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/old-route&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;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;/new-route&lt;/span&gt;&lt;span class="dl"&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;url&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;NextResponse&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;Your migrated &lt;code&gt;proxy.js&lt;/code&gt; will look like this:&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;// proxy.js (Next.js 16)&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;proxy&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;next/server&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="nf"&gt;proxy&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="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;url&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;nextUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Redirect unauthenticated users&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;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="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="s2"&gt;auth&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;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;/login&lt;/span&gt;&lt;span class="dl"&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;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Rewrite /old-route to /new-route&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/old-route&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;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;/new-route&lt;/span&gt;&lt;span class="dl"&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;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Forward all other requests&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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;You'll notice that redirects and rewrites migrate directly. The logic is almost identical. Any mention of "middleware" is replaced with "proxy".&lt;/p&gt;

&lt;h2&gt;
  
  
  React 19.2 integration
&lt;/h2&gt;

&lt;p&gt;Next.js 16 integrates React 19.2 (where Next.js 15 was built on React 18) as its default React runtime, bringing stable support for &lt;a href="https://nextjs.org/docs/13/app/building-your-application/rendering/server-components" rel="noopener noreferrer"&gt;Server Components&lt;/a&gt;, Actions, and the React Compiler. This tight integration makes caching, async server APIs, and edge execution more predictable and efficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  React 19.2 View Transitions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/viewTransition" rel="noopener noreferrer"&gt;View Transitions&lt;/a&gt; uses the browser's native View Transitions API to create smooth, animated changes between pages or UI states. It eliminates the need for complex animation libraries as the browser handles transitions. View Transitions also allows for seamless page‑to‑page animations, reducing the "hard cut" feel of traditional navigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  useEffectEvent
&lt;/h3&gt;

&lt;p&gt;Another notable feature introduced in React 19.2 is &lt;code&gt;useEffectEvent&lt;/code&gt;. It lets you extract non‑reactive logic from your effects into a reusable function called an Effect Event, ultimately solving the &lt;a href="https://www.palo-it.com/en/blog/stale-closures-impact-on-react-components" rel="noopener noreferrer"&gt;stale closure problem&lt;/a&gt;. &lt;code&gt;useEffectEvent&lt;/code&gt; ensures your effects always have access to the latest props and state without unnecessary re‑renders.&lt;/p&gt;

&lt;p&gt;Imagine you're building a chat app. When a user connects to a chat room, you want to show a notification styled with the current theme (light or dark). Your code in Next.js 15 (without &lt;code&gt;useEffectEvent&lt;/code&gt;) would look like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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;react&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;ChatRoom&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConnection&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://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;connection&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;connected&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="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Connected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// captures theme&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&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="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// re-runs whenever theme changes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notification logic is placed directly inside &lt;code&gt;useEffect&lt;/code&gt;. If the theme changes, the effect re‑runs unnecessarily, tearing down and reconnecting the chat. The Next.js 16 rewrite would look something like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffectEvent&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;react&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;ChatRoom&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;onConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEffectEvent&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="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Connected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// always uses latest theme&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConnection&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://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;connection&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;connected&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="nf"&gt;onConnected&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// stable, no stale closure&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&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="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// only re-runs when roomId changes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The use of &lt;code&gt;useEffectEvent&lt;/code&gt; keeps the connection stable while still using the latest theme for notifications. Dependencies are simpler, the chat connection isn't torn down unnecessarily, and notifications reflect the current theme.&lt;/p&gt;

&lt;h3&gt;
  
  
  The implications for Next.js apps
&lt;/h3&gt;

&lt;p&gt;View Transitions and &lt;code&gt;useEffectEvent&lt;/code&gt; both reduce the reliance on custom. In React 18 and earlier, developers often wrote hooks to manage stale closures in effects or to synchronize state with subscriptions. &lt;code&gt;useEffectEvent&lt;/code&gt; provides a stable callback that always sees the latest props and state. Similarly, View Transitions removes the need for custom hooks or context providers to manage navigation animations. Together, these features cut down on boilerplate and make Next.js apps leaner and easier to maintain.&lt;/p&gt;

&lt;p&gt;These features also contribute to more predictable rendering behavior. &lt;code&gt;useEffectEvent&lt;/code&gt; ensures effects don't re‑run unnecessarily, keeping connections or listeners stable while still reflecting fresh state. View Transitions integrates directly with the App Router, so transitions happen consistently across route changes without unexpected flickers or mismatched states. For developers, this means fewer surprises in how React and Next.js handle updates, and for users, it means smoother, more reliable interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stable support for the React Compiler
&lt;/h2&gt;

&lt;p&gt;Next.js 16 ships with React 19.2, and that includes the React Compiler as a first‑class integration. In practice, this means Next.js developers no longer need to scatter &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, or &lt;code&gt;React.memo&lt;/code&gt; throughout their codebase to prevent unnecessary re‑renders.&lt;/p&gt;

&lt;p&gt;The compiler analyzes your components at build time and automatically applies &lt;a href="https://www.geeksforgeeks.org/dsa/what-is-memoization-a-complete-tutorial/" rel="noopener noreferrer"&gt;memoization&lt;/a&gt; optimizations. For Next.js apps, this is especially impactful because of the App Router and &lt;a href="https://nextjs.org/docs/13/app/building-your-application/rendering/server-components" rel="noopener noreferrer"&gt;Server Components&lt;/a&gt; model. Server Components already reduce client‑side JavaScript, and the compiler ensures that the remaining &lt;a href="https://nextjs.org/docs/app/getting-started/server-and-client-components" rel="noopener noreferrer"&gt;client components&lt;/a&gt; render efficiently without developer micromanagement. Combined with Next.js 16's explicit caching, you get predictable performance both on the server and client.&lt;/p&gt;

&lt;p&gt;Developers can now write straightforward React code in Next.js without wrapping everything in memoization hooks. The compiler handles performance tuning automatically, so users experience smoother interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking changes and deprecations
&lt;/h2&gt;

&lt;p&gt;The move from &lt;code&gt;middleware&lt;/code&gt; to &lt;code&gt;proxy&lt;/code&gt; wasn't the only potential project-breaking change that was introduced with Next.js 16. The next/image component shifted toward native browser features and simpler defaults.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading" rel="noopener noreferrer"&gt;Lazy‑loading&lt;/a&gt; is now applied automatically, responsive sizing no longer requires the layout prop, and styling props like &lt;code&gt;objectFit&lt;/code&gt; and &lt;code&gt;objectPosition&lt;/code&gt; have been deprecated in favor of plain CSS. These changes reduce abstractions but require migration if you relied on older defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit and migration considerations
&lt;/h3&gt;

&lt;p&gt;You'll need to remove &lt;a href="https://nextjs.org/docs/pages/api-reference/components/image-legacy" rel="noopener noreferrer"&gt;deprecated props&lt;/a&gt; like &lt;code&gt;layout&lt;/code&gt;, &lt;code&gt;objectFit&lt;/code&gt;, and &lt;code&gt;objectPosition&lt;/code&gt;, then add &lt;code&gt;loading=eager&lt;/code&gt; or &lt;code&gt;priority&lt;/code&gt; to prevent lazy-loading delays. Use &lt;code&gt;fill&lt;/code&gt; for full‑width images or explicit dimensions for fixed sizing. This provides more responsive control.&lt;/p&gt;

&lt;p&gt;For styling, move image fit/position logic into CSS classes for consistency. This helps manage the &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/CLS" rel="noopener noreferrer"&gt;Cumulative Layout Shift (CLS)&lt;/a&gt;. Tools like &lt;a href="https://lighthouse-metrics.com/" rel="noopener noreferrer"&gt;Lighthouse&lt;/a&gt; or &lt;a href="https://www.webpagetest.org/" rel="noopener noreferrer"&gt;WebPageTest&lt;/a&gt; can help you verify if images are causing layout shifts. To optimize the delivery of your images, use modern formats like WebP and AVIF. Next.js 16 automatically serves optimized versions, but it's always a good idea to confirm that your source assets are high-quality.&lt;/p&gt;

&lt;p&gt;Remember to check file sizes. Large hero images should be compressed without visible quality loss. In addition to this, ensure images are cached at the edge for fast delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  AMP support Is deprecated
&lt;/h3&gt;

&lt;p&gt;In Next.js 16, AMP support is fully deprecated and removed. You can no longer configure pages with &lt;code&gt;amp: true&lt;/code&gt; or &lt;code&gt;amp: 'hybrid'&lt;/code&gt;, and the AMP optimizer is gone. Teams and organizations that had invested in building AMP‑specific pages for mobile performance or SEO will be the most impacted by this change.&lt;/p&gt;

&lt;p&gt;AMP support deprecation also affects developers who built workflows around AMP's restrictions, such as limiting JavaScript usage, relying on AMP‑specific components, or configuring hybrid AMP pages. Those patterns are no longer supported, so developers need to migrate to Next.js's unified rendering model.&lt;/p&gt;

&lt;p&gt;Instead of AMP's strict markup, they'll lean on Server Components, Actions, Turbopack, and explicit caching to achieve performance goals. This requires some refactoring, but ultimately simplifies codebases by removing the need for parallel AMP templates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating from AMP
&lt;/h3&gt;

&lt;p&gt;SEO has shifted over the last few years. Google no longer gives AMP pages preferential treatment. Instead, &lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals" rel="noopener noreferrer"&gt;Core Web Vitals&lt;/a&gt; (CLS, LCP, INP) are the ranking signals. A well‑optimized Next.js page can perform just as well as AMP. AMP required separate templates and restrictions. Next.js 16 lets you build a single React app with consistent tooling, reducing complexity.&lt;/p&gt;

&lt;p&gt;When migrating from AMP, you first need to audit your project's AMP pages. This means identifying any pages using &lt;code&gt;amp: true&lt;/code&gt; or &lt;code&gt;amp: 'hybrid'&lt;/code&gt;. Next, remove all AMP configs and consolidate into standard Next.js routes.&lt;/p&gt;

&lt;p&gt;Implement Server Component to minimize client JS, then move styling into CSS instead of AMP-specific markup. Replace AMP-restricted forms with Actions for mutations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration strategy: how to upgrade safely
&lt;/h2&gt;

&lt;p&gt;First, consider whether you should upgrade. For greenfield projects, upgrading to Next.js 16 makes sense. Starting fresh means you can adopt the new primitives we've discussed without worrying about legacy code. You'll be building on the latest architecture from day one, which reduces technical debt and ensures your app is aligned with the direction Next.js is heading.&lt;/p&gt;

&lt;p&gt;For mature production apps, the decision requires more caution. Migrating from Next.js 15 involves rethinking middleware, auditing caching behavior, and replacing unstable APIs with their stable equivalents. If your app is business‑critical, you'll want to plan a phased migration, starting with staging environments and regression testing before rolling out to production. The payoff is significant. You'll get better performance and more predictable caching. However, the risk of breaking existing flows means you should approach the upgrade deliberately rather than rushing.&lt;/p&gt;

&lt;p&gt;Team size and risk tolerance also play a big role. Smaller teams with limited resources may prefer to wait until the ecosystem stabilizes further, since they may not have the bandwidth to troubleshoot regressions. Larger teams, or those with higher risk tolerance, can benefit from adopting Next.js 16 earlier, especially if they have the capacity to run performance regression tests, audit caching, and adjust CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;The right timing depends on your context, but once you've weighed those factors, the next step is to define a clear upgrade path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended upgrade path
&lt;/h3&gt;

&lt;p&gt;The quickest way to &lt;a href="https://nextjs.org/docs/app/guides/upgrading/codemods#upgrade" rel="noopener noreferrer"&gt;upgrade&lt;/a&gt; your Next.js project from version 15 to 16 is by using the upgrade codemod. Open a terminal, navigate to your project's root folder, and run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary upgrade latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Ensure that &lt;a href="http://node.js/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; is installed before running the above command.&lt;/p&gt;

&lt;p&gt;The upgrade codemod is interactive. So once it's done downloading the necessary packages, it will present you with a set of upgrade options that are relevant to your project. These include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding the new Turbopack configuration to your next config file.&lt;/li&gt;
&lt;li&gt;Migrating from &lt;code&gt;next lint&lt;/code&gt; to the &lt;a href="https://eslint.org/docs/latest/use/command-line-interface" rel="noopener noreferrer"&gt;ESLint CLI&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Switching over to proxy from the middleware convention.&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;experimental_ppr&lt;/code&gt; Route Segment config from App Router pages and layouts.&lt;/li&gt;
&lt;li&gt;Drop all &lt;code&gt;unstable_&lt;/code&gt; prefixes from stabilized APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may not see some of these options if they're already up-to-date in your project. For instance, if your project is already using the latest ESLint CLI, it won't appear in the list:&lt;/p&gt;

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

&lt;p&gt;By default, all update options are toggled on. Once you're ready, you can hit the Enter key to perform the updates.&lt;/p&gt;

&lt;p&gt;Alternatively, you can upgrade your project manually, especially if the upgrade codemod fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manually upgrading Next.js 15 to 16
&lt;/h3&gt;

&lt;p&gt;Run the following command to upgrade Next.js to the latest version:&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;next@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then upgrade React using the following command:&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;react@latest react-dom@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: If you're using TypeScript, you'll need to update &lt;code&gt;@types/react&lt;/code&gt; and &lt;code&gt;@types/react-dom&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some libraries may not yet support React 19. Fortunately, Next.js 16 supports React 18.2.0+. That means, if you find version mismatches or peer dependency conflicts, the best way to get around this is by upgrading or downgrading to React 18 (which Next.js 16 still supports), like this:&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;react@18 react-dom@18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, you'll need to migrate from the &lt;code&gt;middleware&lt;/code&gt; to &lt;code&gt;proxy&lt;/code&gt;. First, find the middleware file in your root folder (&lt;code&gt;middleware.js&lt;/code&gt; or &lt;code&gt;middleware.ts&lt;/code&gt;) and rename it to proxy (&lt;code&gt;proxy.js&lt;/code&gt; if you're using JavaScript, or &lt;code&gt;proxy.ts&lt;/code&gt; if you're using TypeScript).&lt;/p&gt;

&lt;p&gt;Next, open your freshly renamed proxy file and replace the &lt;code&gt;middleware&lt;/code&gt; export function to &lt;code&gt;proxy&lt;/code&gt; (if available):&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;You'll also need to replace any configuration flags that contain &lt;code&gt;middleware&lt;/code&gt; as part of their name. For example, you'll need to change &lt;code&gt;skipMiddlewareUrlNormalize&lt;/code&gt; to &lt;code&gt;skipProxyUrlNormalize&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Alternatively, you can run the &lt;a href="https://nextjs.org/docs/app/guides/upgrading/codemods#middleware-to-proxy" rel="noopener noreferrer"&gt;migration codemod&lt;/a&gt; from your terminal, which will perform all the migration steps for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary middleware-to-proxy &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your Next.js 15 project uses experimental Partial Pre-Rendering (PPR), you'll need to remove every instance of it (flags, configuration options, and route-level segments). Vercel provides another helpful &lt;a href="https://nextjs.org/docs/app/guides/upgrading/codemods#remove-experimental-ppr" rel="noopener noreferrer"&gt;codemod&lt;/a&gt; to perform this action. Run the following command from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@latest remove-experimental-ppr &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then opt into PPR by adding the &lt;code&gt;cacheComponents&lt;/code&gt; option to your &lt;code&gt;next.config.js&lt;/code&gt; file:&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="cm"&gt;/** @type {import('next').NextConfig} */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&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="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js 16 stabilized a large number of plugins that were marked unstable in version 15. To ensure that your Next.js 16 project isn't calling deprecated APIs, you'll need to remove every mention of the &lt;code&gt;unstable_&lt;/code&gt; prefix from all stabilized APIs. You can do this manually or use the codemod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@latest remove-unstable-prefix &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enabling Webpack as a fallback
&lt;/h3&gt;

&lt;p&gt;If your project relies on WebPack loaders, plugins, or has a custom configuration, you can opt out of Turbopack. You can run Dev with the Webpack flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx next dev &lt;span class="nt"&gt;--webpack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can edit the &lt;code&gt;script&lt;/code&gt; section of your package:&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;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next dev --webpack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next build --webpack"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next lint"&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;This will force your project to use WebPack in both the production and development builds. If you need to use Turbopack for one of these builds (for instance, dev), you can simply remove the &lt;code&gt;--webpack&lt;/code&gt; flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-upgrade tooling and testing tips
&lt;/h2&gt;

&lt;p&gt;Developers looking for the latest bug fixes and feature updates may be tempted to opt into canary releases. Canary releases aren't guaranteed to be stable, as they may contain unfinished APIs, regressions, or performance issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to approach canary releases
&lt;/h3&gt;

&lt;p&gt;For most teams, the safer approach is to test canary builds in local development or staging environments, where you can evaluate new features without exposing end users to potential instability. You can run the following command to pin your project to the latest Next.js canary build:&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;next@canary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opting into canary releases is most beneficial for developers who maintain libraries, frameworks, or large applications that need to anticipate changes. Canary builds are best for local development, staging, or feature branches. Avoid deploying them to production unless you're comfortable with instability.&lt;/p&gt;

&lt;p&gt;Whether you're running the Canary or official stable release, you'll want to combine Next.js 16's tooling upgrades with a set of targeted testing practices. The first tool you should acquaint yourself with is the &lt;a href="https://nextjs.org/docs/app/guides/mcp" rel="noopener noreferrer"&gt;Next.js DevTools MCP Server&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Next.js DevTools MCP server
&lt;/h3&gt;

&lt;p&gt;The Next.js DevTools MCP Server is designed to give developers deep visibility into how their application is behaving at runtime, especially around caching and rendering. Prior to its introduction, developers often had to rely on logs, headers, or trial‑and‑error to understand what was cached, when it would revalidate, and why certain components were re‑rendering.&lt;/p&gt;

&lt;p&gt;Use it to inspect cache boundaries, to see exactly where &lt;code&gt;cache()&lt;/code&gt; is being applied to your code. This is crucial because caching in Next.js 16 is explicit.&lt;/p&gt;

&lt;p&gt;For revalidation timings, DevTools provide a clear view of how long cached data is valid and when it will be refreshed. Instead of guessing whether a component will revalidate after 30 seconds or only on demand, you can see the exact timing in the DevTools panel. This helps you audit your ISR or Cache Component settings and ensures that your content stays fresh without overloading your backend.&lt;/p&gt;

&lt;p&gt;DevTools can also show you whether a component was served from cache, revalidated, or freshly rendered. This is especially important in Next.js 16 because PPR means parts of a page may be cached while others are dynamic.&lt;/p&gt;

&lt;p&gt;In addition to the above, you'll also need to audit the Turbopack's cache behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auditing Turbopack's cache behavior
&lt;/h3&gt;

&lt;p&gt;Turbopack now writes build artifacts into a persistent cache directory &lt;code&gt;.next/cache&lt;/code&gt;. It's important to inspect the size and contents of this folder from time-to-time, especially if you're experiencing performance dips or build issues. Delete the contents of this folder to force Turbopack to rebuild everything from scratch. You can then measure how fast your rebuilds are against previous builds.&lt;/p&gt;

&lt;p&gt;To measure how long builds take on Linux/macOS machines, you can use:&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;time &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the following for Windows machines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Measure-Command &lt;span class="o"&gt;{&lt;/span&gt; npm run build &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Next.js 16 introduces proxy.js, Cache Components, and Turbopack's file cache to give developers clearer boundaries and faster builds.&lt;/p&gt;

&lt;p&gt;Migrating from 15 to 16 requires teams to adapt to a new philosophy. Routing logic is now separated cleanly from authentication, caching is transparent and auditable through DevTools MCP, and build performance benefits from persistent caching. These changes reduce guesswork, improve scalability, and align Next.js more closely with modern React practices.&lt;/p&gt;

&lt;p&gt;Ultimately, Next.js 16 is less of an incremental update and more of a re‑architecture. It challenges developers to rethink how they structure applications, but rewards that effort with faster builds, clearer caching strategies, and a more stable foundation for future growth.&lt;/p&gt;

&lt;p&gt;Authentication, access control, and user management are critical components of any Next.js app. Use Descope's &lt;a href="https://docs.descope.com/flows" rel="noopener noreferrer"&gt;no / low code workflows&lt;/a&gt; to easily add natively embedded auth to your Next.js app without worrying about custom code or keeping up with breaking changes. You can also use &lt;a href="https://www.descope.com/blog/post/vercel-marketplace-integrations" rel="noopener noreferrer"&gt;Descope integrations on the Vercel Marketplace&lt;/a&gt; to add auth for your users and MCP servers in a few clicks. &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up for a free Descope account&lt;/a&gt; to get started.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>performance</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
