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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Launch the VS installer and click Modify to modify your installation of Visual Studio:&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;.NET desktop development&lt;/li&gt;
&lt;li&gt;Desktop development with C++&lt;/li&gt;
&lt;li&gt;WinUI application development&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Still under Workloads, in the Gaming section, make sure that Game development with C++ is selected:&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;Navigate to Flows and then select Start from template.&lt;/p&gt;

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

&lt;p&gt;The flow template library allows you to filter by use cases and methods. Select Enchanted Link and Social (OAuth/OIDC) for your methods, and MFA for your use case. Select the Sign up or in version of the templates that matched your selected filters.&lt;/p&gt;

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

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

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

&lt;p&gt;Hover over the Welcome Screen element on the flow and select Edit.&lt;/p&gt;

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

&lt;p&gt;Delete the Google and Apple social buttons, and drag in a Discord button from the sidebar.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Then, create a blank game project and give it a name. This tutorial will use UNREAL_AUTH_DEMO as the project name. Make sure to select C++ as your project type.&lt;/p&gt;

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

&lt;p&gt;If everything went well, you'll see an empty game world showing up in the editor:&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Add a User Interface widget blueprint:&lt;/p&gt;

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

&lt;p&gt;Click User Widget as the parent class when prompted:&lt;/p&gt;

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

&lt;p&gt;Name your widget something practical like LoginMenu:&lt;/p&gt;

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

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

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

&lt;p&gt;Once your menu has been created, you should have a hierarchy that looks similar to this:&lt;/p&gt;

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

&lt;p&gt;And while your menu might look slightly different, it should look similar to this:&lt;/p&gt;

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

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

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

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

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

&lt;ol&gt;
&lt;li&gt;Create a new empty level (for example, you can name it MainMenuLevel).&lt;/li&gt;
&lt;li&gt;Open the level blueprint.&lt;/li&gt;
&lt;li&gt;On Begin Play, add a Create Widget node, select your LoginMenu widget, then connect it to an Add to Viewport node.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Save your level blueprint, exit the blueprint editor, and click Play from the level editor to see your new menu inside the level:&lt;/p&gt;

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

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

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

&lt;p&gt;Create a new C++ class (UserWidget parent) and name it LoginUIWidget.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;span class="nl"&gt;protected:&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SigninButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SignoutButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UTextBlock&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TextResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the following:&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Back in the editor, go to Edit &amp;gt; Project Settings, and then click Maps &amp;amp; Modes on the left sidebar menu. Change your Editor Startup Map and your Game Default Map to point to the MainMenuLevel you created.&lt;/p&gt;

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

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

&lt;p&gt;In your editor, you'll see a drawer called Output Log. Expand this drawer, scroll to the bottom, and see if your custom output message is there.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;While you're on this screen, update the Flow Hosting URL to point to the custom flow you created earlier:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;span class="nl"&gt;protected:&lt;/span&gt;
    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SigninButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UButton&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SignoutButton&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;UPROPERTY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BindWidget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UTextBlock&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TextResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

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

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

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

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

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

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

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

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

    &lt;span class="n"&gt;UFUNCTION&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;ExchangeAuthCodeForTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a high level, this header sets up:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

        &lt;span class="c1"&gt;// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build your code again using Build -&amp;gt; Build unreal_auth_demo. Once it's finished building, you can open up the Unreal editor again.&lt;/p&gt;

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

&lt;p&gt;Once your editor is open, run the game using the green play button again.&lt;/p&gt;

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

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

&lt;p&gt;Click Sign In. A browser window will open with the following login UI:&lt;/p&gt;

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

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

&lt;p&gt;The first time you sign in, Descope will ask for some extra information about you and verify your identity using MFA.&lt;/p&gt;

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

&lt;p&gt;If you entered your number correctly, you should receive a verification code via SMS:&lt;/p&gt;

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

&lt;p&gt;Once you've entered the code, your screen should look like this:&lt;/p&gt;

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

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

&lt;p&gt;Discord will open up with the following dialog:&lt;/p&gt;

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

&lt;p&gt;Click Authorize and you should see the following:&lt;/p&gt;

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

&lt;p&gt;Descope will now call the configured callback URL (in your case, &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;) to tell your server that the login was successful and to give you an authorization code:&lt;/p&gt;

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

&lt;p&gt;In the background, your code will use the ExchangeAuthCodeForTokens and FetchUserProfile methods to fetch proper authentication tokens and to retrieve user information. Once that's complete, it will update the UI in your game:&lt;/p&gt;

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

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

&lt;p&gt;Try giving that button a click:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Here's a diagram of the process:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

    &lt;span class="c1"&gt;// Insert the token into the `auth_jwt` cookie in the response&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the following code to make a request with the cookie in client-side JavaScript code:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/logout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User logged out.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider the following pros and cons regarding cookie storage before using it for your JWTs:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;If you're struggling to figure out if the JWT is being stored successfully or not, use the &lt;strong&gt;Application&lt;/strong&gt; tab in your browser's developer tools to analyze the local storage, session storage, and cookies associated with the current page:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;In &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;&lt;strong&gt;Settings &amp;gt; Project&lt;/strong&gt;&lt;/a&gt;, you'll see your &lt;strong&gt;Project ID&lt;/strong&gt;. This is a unique identifier that links your Streamlit app to Descope.&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In the new directory, create a file called &lt;code&gt;app.py&lt;/code&gt; and paste the following code:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is a demo app with Descope-powered authentication and SSO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;p&gt;Here you'll see a full list of supported providers. Select &lt;strong&gt;Google&lt;/strong&gt; for this example:&lt;/p&gt;

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

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

&lt;p&gt;Next, configure the redirect URL. This is the location users will return to after completing authentication. Since we're working locally, set it to your app's address, &lt;code&gt;http://localhost:8501&lt;/code&gt;, in the configuration provided, like this:&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Initialize Descope in your app:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;descope.descope_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DescopeClient&lt;/span&gt;

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

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is a demo app with Descope-powered authentication and SSO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a "Sign in with Google" button that launches the OAuth flow using the &lt;code&gt;descope_client.oauth.start()&lt;/code&gt; method:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# … collapsed repeated code
&lt;/span&gt;&lt;span class="n"&gt;descope_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DescopeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;To persist tokens and user data, update your code as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;descope.descope_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DescopeClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;descope.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthException&lt;/span&gt;

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

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# User is not logged in
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Handle possible login
&lt;/span&gt;        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# Reset URL state
&lt;/span&gt;        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Exchange code
&lt;/span&gt;            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;jwt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt_response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt_response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refreshSessionToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt_response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rerun&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AuthException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login failed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;At this point, your app only displays content when a user is logged in. You've successfully implemented Streamlit authentication with OAuth, laying the foundation for full Streamlit SSO with enterprise providers like &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; in the next section.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ol&gt;
&lt;li&gt;In the Descope console, create a new tenant: Go to the &lt;a href="https://app.descope.com/tenants" rel="noopener noreferrer"&gt;&lt;strong&gt;Tenants&lt;/strong&gt;&lt;/a&gt; page and click &lt;strong&gt;+ Tenant&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;In the tenant &lt;strong&gt;Authentication Methods&lt;/strong&gt; section, choose the &lt;strong&gt;SAML&lt;/strong&gt; protocol. Enter your email domain in the &lt;strong&gt;SSO Domains&lt;/strong&gt; field under &lt;strong&gt;Tenant Details&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;ol&gt;
&lt;li&gt;Go to your Okta account and click &lt;strong&gt;Admin&lt;/strong&gt; to access your admin dashboard:&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Applications &amp;gt; Applications&lt;/strong&gt; and click &lt;strong&gt;Browse App Catalog&lt;/strong&gt; to explore a comprehensive list of available integrations:&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Search for &lt;strong&gt;Descope&lt;/strong&gt; in the catalog and add the integration. This creates a new Okta app automatically.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;General Settings&lt;/strong&gt; tab, provide a label for your new Okta app.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Switch to the &lt;strong&gt;Assignments&lt;/strong&gt; tab and assign users who should have access to your Streamlit app via SSO:&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Switch to the &lt;strong&gt;Sign-On Options&lt;/strong&gt; tab and select &lt;strong&gt;SAML 2.0&lt;/strong&gt;. Copy the &lt;strong&gt;Metadata URL&lt;/strong&gt; then paste it into your tenant's SSO configuration in Descope. Next, copy the &lt;strong&gt;ACS URL&lt;/strong&gt; and &lt;strong&gt;Entity ID&lt;/strong&gt; from Descope into the &lt;strong&gt;Advanced Sign-on Settings&lt;/strong&gt; in Okta:&lt;/li&gt;
&lt;/ol&gt;

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

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

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

&lt;p&gt;These mappings define how user attributes are passed from Okta to Descope.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Back in Descope, update your tenant's &lt;strong&gt;SSO Mapping&lt;/strong&gt; settings so attributes from Okta align correctly with Descope's schema.&lt;/li&gt;
&lt;/ol&gt;

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

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

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;Go to your tenant's settings.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Tenant ID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add it to your &lt;code&gt;.streamlit/secrets.toml&lt;/code&gt; file with the &lt;code&gt;DESCOPE_TENANT_ID&lt;/code&gt; key.&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;st.container&lt;/code&gt; element in your code like this:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# … collapsed repeated code
&lt;/span&gt;&lt;span class="n"&gt;TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_TENANT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Get tenant ID from secret
# …
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# … google button here
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign in with SSO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_container_width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;sso_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8501&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sso_response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="c1"&gt;# Redirect to Okta
&lt;/span&gt;            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;meta http-equiv=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; content=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0; url=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;unsafe_allow_html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Click &lt;strong&gt;Sign In with SSO&lt;/strong&gt; to go to Okta for verification. Sign in with one of the users you assigned earlier. After successful authentication, you're redirected back to your Streamlit app with a &lt;code&gt;code&lt;/code&gt; query parameter—just like in the OAuth flow. Since you already implemented the code exchange, you'll be signed in automatically:&lt;/p&gt;

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

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

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

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

&lt;ul&gt;
&lt;li&gt;Go to the &lt;a href="https://app.descope.com/users" rel="noopener noreferrer"&gt;&lt;strong&gt;Users&lt;/strong&gt;&lt;/a&gt; page, click the kebab menu of the user you want to update, and click &lt;strong&gt;Edit&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;In the modal, select &lt;strong&gt;Tenant Admin&lt;/strong&gt; in the &lt;strong&gt;Roles&lt;/strong&gt; field and click &lt;strong&gt;Save&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Update your code to show an admin indicator:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# … collapsed repeated code
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# …
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tenant Admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roleNames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="c1"&gt;# Show admin-specific content
&lt;/span&gt;                &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ADMIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🤖&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should now be the view of the admin user when they're signed in:&lt;/p&gt;

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

&lt;p&gt;This approach ensures that only authorized users see privileged features, improving both the security and integrity of your app.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Streamlit SSO and authentication with Descope
&lt;/h2&gt;

&lt;p&gt;You just wired Streamlit SSO with Descope end to end: created a Descope project, added OAuth social login, implemented SAML SSO with Okta, and gated features with roles. You now have a secure pattern you can reuse across Streamlit apps, from personal dashboards to multi-tenant products.&lt;/p&gt;

&lt;p&gt;Ready to keep going? Use your &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Free Forever account&lt;/a&gt; to extend your app with &lt;a href="https://www.descope.com/learn/post/passwordless-authentication" rel="noopener noreferrer"&gt;passwordless&lt;/a&gt; options, &lt;a href="https://www.descope.com/learn/post/adaptive-authentication" rel="noopener noreferrer"&gt;adaptive MFA&lt;/a&gt;, and &lt;a href="https://www.descope.com/learn/post/scim" rel="noopener noreferrer"&gt;SCIM provisioning&lt;/a&gt;. Have questions or edge cases to discuss? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book time&lt;/a&gt; with the team for a focused walkthrough.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Next.js 14 Authentication and RBAC with App Router</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:44:55 +0000</pubDate>
      <link>https://dev.to/descope/nextjs-14-authentication-and-rbac-with-app-router-192l</link>
      <guid>https://dev.to/descope/nextjs-14-authentication-and-rbac-with-app-router-192l</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-nextjs14-app-router" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ensuring your application is secure through resilient authentication and authorization mechanisms is crucial in the &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js 14&lt;/a&gt; development process. This helps to ensure that only authenticated users can access the protected resources of your application and that each user can access only the resources they are allowed to access.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how to implement Next.js 14 authentication and role-based access control (RBAC) using the &lt;a href="https://nextjs.org/docs#app-router-vs-pages-router" rel="noopener noreferrer"&gt;App Router&lt;/a&gt; and Descope. Whether you're building a new application or enhancing an existing one, this guide will equip you with the skills to create secure login and access controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set up Descope authentication in Next.js 14&lt;/li&gt;
&lt;li&gt;Implement magic link login flows&lt;/li&gt;
&lt;li&gt;Add role-based authorization (RBAC)&lt;/li&gt;
&lt;li&gt;Protect routes with middleware&lt;/li&gt;
&lt;li&gt;Manage user sessions and tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding Next.js 14 authentication and RBAC
&lt;/h2&gt;

&lt;p&gt;Before implementing Next.js 14 authentication, let's clarify the core concepts you'll work with in this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication vs. authorization
&lt;/h2&gt;

&lt;p&gt;Both authentication and authorization are integral to developing a secure Next.js 14 application, but they serve distinct purposes. Authentication is the process of verifying a user’s identity and is typically achieved through a set of login credentials, such as email and passwords, magic links, passkeys or &lt;a href="https://www.descope.com/learn/post/authentication-types" rel="noopener noreferrer"&gt;other auth methods&lt;/a&gt;. &lt;a href="https://www.youtube.com/watch?v=qprypVZ6Pxo" rel="noopener noreferrer"&gt;Authorization&lt;/a&gt; determines whether the authenticated user is allowed to access specific resources or perform certain actions. This tutorial implements both using Next.js 14 with App Router.&lt;/p&gt;

&lt;p&gt;Want to learn more about the basics? See our complete guide: &lt;a href="https://www.descope.com/learn/post/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs. Authorization: What's the Difference?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  RBAC in this tutorial
&lt;/h2&gt;

&lt;p&gt;This tutorial uses role-based access control (RBAC) to manage who can do what in the blogging app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Editor role:&lt;/strong&gt; Can create and edit posts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin role:&lt;/strong&gt; Can view and publish posts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of assigning permissions to individual users, you'll assign them to roles. Then you’ll assign users to roles. This makes managing permissions scalable as your app grows. To learn more about RBAC concepts, benefits, and implementation strategies, see: &lt;a href="https://www.descope.com/learn/post/rbac" rel="noopener noreferrer"&gt;What Is RBAC: Your Simple Guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js 14 and App Router
&lt;/h2&gt;

&lt;p&gt;Next.js 14 supports both client-side rendering (CSR) and server-side rendering (SSR), each with different implications for authentication and authorization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side rendering (CSR):&lt;/strong&gt; The client renders content after the initial page load. This can cause a brief flash of unauthorized content before auth state is verified. You can improve UX with loading indicators or skeleton screens during auth checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-side rendering (SSR):&lt;/strong&gt; The server handles rendering before sending the content to the client. This approach prevents unauthorized content flashes since the server clocks the request until it verifies the auth state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comparing frameworks? See our comparison: &lt;a href="https://www.descope.com/blog/post/nextjs-vs-reactjs-vs-sveltekit" rel="noopener noreferrer"&gt;Svelte vs Next.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tutorial uses SSR with App Router, Next.js 14's file-system-based routing solution, to implement authentication that verifies on the server before rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Descope for Next.js 14 authentication
&lt;/h2&gt;

&lt;p&gt;Descope simplifies adding Next.js 14 authentication and authorization by offering an intuitive SDK and visual flows to build authentication screens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt; is a visual no-code interface to build screens and authentication flows for common user interactions with your application, such as login, sign-up, user invites, and &lt;a href="https://www.descope.com/learn/post/mfa" rel="noopener noreferrer"&gt;multi-factor authentication (MFA)&lt;/a&gt;. This feature abstracts away the implementation details of authentication methods, session management, and error handling, allowing you to focus on building the core features of your application rather than handling these complexities.&lt;/p&gt;

&lt;p&gt;Using Descope eliminates the need to write authentication and authorization logic from scratch, saving valuable development time and reducing the risk of security vulnerabilities. Descope's infrastructure ensures your application's authentication and authorization mechanisms are secure, scalable, and easy to maintain.&lt;/p&gt;

&lt;p&gt;The following sections explain how you can implement these features in a Next.js application using Descope. To follow along, you need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/download" rel="noopener noreferrer"&gt;Node.js v18&lt;/a&gt; installed &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/book/en/v2/Getting-Started-Installing-Git" rel="noopener noreferrer"&gt;Git CLI&lt;/a&gt; installed &lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;A Free Forever Descope account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;To demonstrate Next.js 14 authentication with RBAC, you'll build a simple blogging application using the editor and admin roles described above.&lt;/p&gt;

&lt;p&gt;To begin, you’ll need to create a new project. On the &lt;a href="https://app.descope.com/login" rel="noopener noreferrer"&gt;Descope Console&lt;/a&gt;, create a new project with the name &lt;code&gt;descope-nextjs-auth-rbac&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;Navigate to the project setup. Select &lt;code&gt;Consumers&lt;/code&gt; under &lt;code&gt;Who uses your application?&lt;/code&gt; and then click &lt;code&gt;Next&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;Select &lt;code&gt;Magic Link&lt;/code&gt; for &lt;code&gt;Which authentication methods do you want to use?&lt;/code&gt; and then click &lt;code&gt;Next&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;Skip the MFA method step and click &lt;code&gt;Go ahead without MFA&lt;/code&gt;. You can always set this up later:&lt;/p&gt;

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

&lt;p&gt;On the next page, you can view the flows generated for your project. Click &lt;code&gt;Next&lt;/code&gt; to generate these flows:&lt;/p&gt;

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

&lt;p&gt;Once the flows are generated, select &lt;code&gt;Project&lt;/code&gt; from the sidebar and take note of your project ID, which you’ll use in the next step:&lt;/p&gt;

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

&lt;p&gt;Next, you need to obtain a management key. Select &lt;code&gt;Company&lt;/code&gt; from the sidebar, select the &lt;code&gt;Management Keys&lt;/code&gt; tab on the &lt;code&gt;Company&lt;/code&gt; page, and click the &lt;code&gt;+ Management Key&lt;/code&gt; button to create a new management key. Provide the key name and, under &lt;code&gt;Project Assignment&lt;/code&gt;, select &lt;code&gt;Use this management key for all the projects in the company&lt;/code&gt;. Click the &lt;code&gt;Generate Key&lt;/code&gt; button and copy the value of your key:&lt;/p&gt;

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

&lt;p&gt;Once you have the key, you can implement authentication for the Next.js application. You will come back to the console to set up the RBAC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring the starter template
&lt;/h2&gt;

&lt;p&gt;This walkthrough uses a simple web app shell. To keep this guide focused on authentication and authorization, we've prepared a starter template for the blogging application. In this section, you'll clone and set up the template.&lt;/p&gt;

&lt;p&gt;To clone the template to your local machine, execute the following command in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git clone --single-branch -b starter-template https://github.com/kimanikevin254/descope-nextjs-auth-rbac.git&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Navigate into the project directory and install all the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd descope-nextjs-auth-rbac &amp;amp;&amp;amp; npm i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; project in the root folder and add the following content, which defines the location of the SQLite database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL="file:./dev.db"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;Run a &lt;a href="https://www.prisma.io/migrate" rel="noopener noreferrer"&gt;Prisma migration&lt;/a&gt; for the models defined in the &lt;code&gt;prisma/schema.prisma&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx prisma migrate dev --name init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;Run the app with the command &lt;code&gt;npm run dev&lt;/code&gt; and navigate to &lt;code&gt;http://localhost:3000/&lt;/code&gt; on your web browser. You should see a dashboard where the posts are displayed. At the moment, no posts are available:&lt;/p&gt;

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

&lt;p&gt;Users are able to write posts by clicking the &lt;code&gt;Start Writing&lt;/code&gt; button, which directs them to the &lt;code&gt;Write a Post&lt;/code&gt; page that has a rich text editor:&lt;/p&gt;

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

&lt;p&gt;At this stage, the template is ready, but don’t create any posts until you have implemented authentication and authorization.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding authentication to Next.js 14
&lt;/h2&gt;

&lt;p&gt;This section walks you through implementing authentication in your Next.js 14 application using Descope. You'll use code examples and screenshots to guide each step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Install the Descope Next.js SDK&lt;/li&gt;
&lt;li&gt;Wrap your app with Descope authentication&lt;/li&gt;
&lt;li&gt;Configure the “sign up or in” function&lt;/li&gt;
&lt;li&gt;Protect routes with authentication middleware&lt;/li&gt;
&lt;li&gt;Test the authentication flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get started, execute the following command in the terminal to install the &lt;a href="https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk" rel="noopener noreferrer"&gt;Descope Next.js SDK&lt;/a&gt;, which you’ll use to implement authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i @descope/nextjs-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/layout.js&lt;/code&gt; file and replace the existing code with the following to wrap the whole application with the Descope Auth Provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Inter } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@descope/nextjs-sdk";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
   title: "Descope - Next.js Auth",
   description:
       "Demonstrating how to add auth &amp;amp; RBAC to Next.js 14 with Descope",
};

export default function RootLayout({ children }) {
   return (
       &amp;lt;AuthProvider projectId={process.env.DESCOPE_PROJECT_ID}&amp;gt;
           &amp;lt;html lang="en"&amp;gt;
               &amp;lt;body
                   className={`${inter.className} max-w-screen-lg mx-auto py-4`}
               &amp;gt;
                   {children}
               &amp;lt;/body&amp;gt;
           &amp;lt;/html&amp;gt;
       &amp;lt;/AuthProvider&amp;gt;
   );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;You will set the value of &lt;code&gt;DESCOPE_PROJECT_ID&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; file later on.&lt;/p&gt;

&lt;p&gt;You can use the &lt;code&gt;sign-up-or-in&lt;/code&gt; flow that you generated when configuring the project to allow the user to sign in to the application. The &lt;code&gt;sign-up-or-in&lt;/code&gt; flow presents the user with a &lt;code&gt;Welcome&lt;/code&gt; screen where they are prompted to provide their email address.&lt;/p&gt;

&lt;p&gt;Once the user provides the email address and clicks the &lt;code&gt;Continue&lt;/code&gt; button, a magic link is sent to the provided email address. Once the user clicks the magic link, Descope verifies the link, and the user is authenticated. The flow then checks if the user is new or returning. If the user is new, they are prompted to provide additional information(their name), and their details are updated.&lt;/p&gt;

&lt;p&gt;Here’s a visual representation of the flow in practice:&lt;/p&gt;

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

&lt;p&gt;Open the &lt;code&gt;app/sign-in/page.js&lt;/code&gt; file and replace the existing code with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import { Descope } from "@descope/nextjs-sdk";
import axios from "axios";
import { useRouter } from "next/navigation";

export default function Page() {
   const router = useRouter();

   // Register user or redirect to home
   const handleEvent = async (event) =&amp;gt; {
       try {
           if (event.detail.firstSeen !== true) {
               return router.replace("/");
           }

           // Register the user
           const { data } = await axios.post("/api/register", {
               descopeUserId: event.detail.user.userId,
               email: event.detail.user.email,
               name: event.detail.user.name,
           });

           if (data.error) {
               alert("Something went wrong");
           } else {
               return router.replace("/");
           }
       } catch (error) {
           console.log(error);
       }
   };
   return (
       &amp;lt;Descope
           flowId="sign-up-or-in"
           onSuccess={(e) =&amp;gt; handleEvent(e)}
           onError={(e) =&amp;gt; alert("Something went wrong. Please try again.")}
       /&amp;gt;
   );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;In the preceding code, once the user signs up or in successfully, the event data returned from this component is passed to the &lt;code&gt;handleEvent&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;This function checks if the user is new by examining &lt;code&gt;event.details.firstSeen&lt;/code&gt;. If the user is not new, they are redirected to the home screen. Otherwise, it sends a POST request to the &lt;code&gt;/api/register&lt;/code&gt; endpoint with the user’s details to register the user. If the registration is successful, the user is redirected to the home page; otherwise, an error message is displayed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/api/register&lt;/code&gt; endpoint is defined in the &lt;code&gt;app/api/register/route.js&lt;/code&gt; files, and it adds the user to the local database.&lt;/p&gt;

&lt;p&gt;You also need to set up a middleware to enforce authentication for all the pages in this application. Create a file named &lt;code&gt;middleware.js&lt;/code&gt; in the project root folder and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { authMiddleware } from "@descope/nextjs-sdk/server";

export default authMiddleware({
   projectId: process.env.DESCOPE_PROJECT_ID,
   redirectUrl: process.env.SIGN_IN_ROUTE,
});

export const config = {
   matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
shell&lt;/p&gt;

&lt;p&gt;This code uses the &lt;code&gt;authMiddleware&lt;/code&gt; function provided by the Descope Next.js SDK to protect all routes and redirect unauthenticated users to the sign-in page.&lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;.env&lt;/code&gt; file with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DESCOPE_PROJECT_ID=&amp;lt;YOUR-PROJECT-ID&amp;gt;
DESCOPE_MANAGEMENT_KEY=&amp;lt;YOUR-MANAGEMENT-KEY&amp;gt;

SIGN_IN_ROUTE="/sign-in"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Make sure to replace the placeholder values with the values you obtained earlier.&lt;/p&gt;

&lt;p&gt;The authentication for your Next.js application is now complete.&lt;/p&gt;

&lt;p&gt;Before you test it, open the &lt;code&gt;app/write/page.js&lt;/code&gt; file. In this file, notice that the &lt;code&gt;savePost&lt;/code&gt; function requires the Descope user ID. You can retrieve this using the hooks provided by the Next.js SDK.&lt;/p&gt;

&lt;p&gt;Add the following import statement to the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Add the following statement that retrieves the user just before the &lt;code&gt;savePost&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { user } = useUser();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;You can now test the authentication flow.&lt;/p&gt;

&lt;p&gt;On your browser, navigate to &lt;code&gt;http://localhost:3000&lt;/code&gt;. You are redirected to &lt;code&gt;http://localhost:3000/sign-in&lt;/code&gt; since you have not signed in. It should look like this:&lt;/p&gt;

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

&lt;p&gt;Provide your email address, click the link sent to your inbox, and provide your name since this is the first time you’re signing in. Then you are redirected to the home page:&lt;/p&gt;

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

&lt;p&gt;This confirms that the authentication is working as expected.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding authorization to Next.js 14
&lt;/h2&gt;

&lt;p&gt;Currently, anyone can log in to the application, create posts, submit them for approval, and approve the posts. For this example, you want to specify that editors can write posts and then submit them for approval, and admins can publish the posts.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create roles and assign permissions in Descope&lt;/li&gt;
&lt;li&gt;Control UI visibility based on user roles&lt;/li&gt;
&lt;li&gt;Validate roles in API routes&lt;/li&gt;
&lt;li&gt;Configure session tokens for authorization&lt;/li&gt;
&lt;li&gt;Test role-based permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you implement this functionality, click the &lt;code&gt;Start Writing&lt;/code&gt; button and create a few posts to test the application with:&lt;/p&gt;

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

&lt;p&gt;Go to the &lt;code&gt;Descope Console&lt;/code&gt;, select &lt;code&gt;Authorization&lt;/code&gt; from the sidebar, and click the + Role button. In the &lt;code&gt;Add Role&lt;/code&gt; modal, provide “editor” as the name and “Can write posts and submit them for approval” as the description. Then click the &lt;code&gt;Add&lt;/code&gt; button:&lt;/p&gt;

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

&lt;p&gt;Repeat the process to create a role for the admin. Provide “admin” as the role and “Can toggle a post’s published status” as the description.&lt;/p&gt;

&lt;p&gt;Assign the “editor” role to the user who is logged in to the application. In the &lt;code&gt;Descope Console&lt;/code&gt;, select &lt;code&gt;Users&lt;/code&gt; from the sidebar to edit the user details:&lt;/p&gt;

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

&lt;p&gt;On the user details modal, select &lt;code&gt;+ Add Tenant / Role&lt;/code&gt;, assign the &lt;code&gt;editor&lt;/code&gt; role, and click &lt;code&gt;Save&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;Open the &lt;code&gt;app/page.js&lt;/code&gt; file and add the following import statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Add the following code before the &lt;code&gt;fetchPosts&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { user } = useUser();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;This hook allows you to retrieve the user’s details.&lt;/p&gt;

&lt;p&gt;Locate the following lines of code in the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Link
   href={"/write"}
   className="px-4 py-1 border rounded-lg bg-black text-white"
&amp;gt;
   Start Writing
&amp;lt;/Link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Replace it with the following to display only the &lt;code&gt;Start Writing&lt;/code&gt; button to editors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   user?.roleNames?.includes("editor") &amp;amp;&amp;amp; (
       &amp;lt;Link
           href={"/write"}
           className="px-4 py-1 border rounded-lg bg-black text-white"
       &amp;gt;
           Start Writing
       &amp;lt;/Link&amp;gt;
   )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/posts/[postId]/page.js&lt;/code&gt; file and add the following lines of code in their respective locations (indicated by the comments):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useUser } from "@descope/nextjs-sdk/client"; // After the import statement

const { user } = useUser(); // Before the return statement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Locate the following lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Button
   variant="default"
   className="px-6 mt-6"
   onClick={() =&amp;gt; togglePublishedStatus()}
&amp;gt;
   {post.published ? "Unpublish" : "Publish"}
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Replace it with the following to specify that only admins can publish/unpublish a post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   user?.roleNames?.includes("admin") &amp;amp;&amp;amp; (
       &amp;lt;Button
           variant="default"
           className="px-6 mt-6"
           onClick={() =&amp;gt; togglePublishedStatus()}
       &amp;gt;
           {post.published ? "Unpublish" : "Publish"}
       &amp;lt;/Button&amp;gt;
   )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;For additional security, you also validate these roles in the API router handlers. In the lib folder, create a new file named &lt;code&gt;descope.js&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { createSdk } from "@descope/nextjs-sdk/server";

export const descopeSdk = createSdk({
   projectId: process.env.DESCOPE_PROJECT_ID,
   managementKey: process.env.DESCOPE_MANAGEMENT_KEY,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;This code initializes a Descope client instance and exports it for use in other parts of the application.&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/api/posts/create/route.js&lt;/code&gt; file and add the following code just after &lt;code&gt;const data = await request.json()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Make sure the user has the editor role
const userRoles = descopeSdk.getJwtRoles(data.sessionToken);

if (!userRoles?.includes("editor")) {
   throw new Error("User is not an editor");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;This code ensures that the user has the editor role. If not, it throws an error.&lt;/p&gt;

&lt;p&gt;Remember to add the following import statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { descopeSdk } from "@/lib/descope";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/api/posts/toggleStatus/route.js&lt;/code&gt; file and add the following code just after &lt;code&gt;const data = await request.json()&lt;/code&gt; to throw an error if the user making the request does not have the admin role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Make sure the user has the admin role
const userRoles = descopeSdk.getJwtRoles(data.sessionToken);

if (!userRoles?.includes("admin")) {
   throw new Error("User is not an admin");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Remember to add the following &lt;code&gt;import&lt;/code&gt; statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { descopeSdk } from "@/lib/descope";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;With the route handlers verifying the user roles, you need to make sure that the session token is passed in the request body. Start by opening the &lt;code&gt;app/write/page.js&lt;/code&gt; file and retrieving the session token using the &lt;code&gt;useSession&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { sessionToken } = useSession();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Make sure the &lt;code&gt;useSession&lt;/code&gt; hook is imported into the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useSession, useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Add the retrieved session token to the body of the POST request in the &lt;strong&gt;savePost&lt;/strong&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { data } = await axios.post("/api/posts/create", {
   title,
   content,
   descopeUserId: user?.userId,
   sessionToken,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;app/posts/[postId]/page.js&lt;/code&gt; file and retrieve the session token using the &lt;code&gt;useSession&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { sessionToken } = useSession();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Make sure the hook is imported into the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useSession, useUser } from "@descope/nextjs-sdk/client";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;togglePublishedStatus&lt;/code&gt; function with the following code that ensures that the session token is passed to the route handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const togglePublishedStatus = async () =&amp;gt; {
   try {
       const { data } = await axios.put("/api/posts/toggleStatus", {
           postId: params.postId,
           sessionToken,
       });

       setPost(data.data);
   } catch (error) {
       alert("Something went wrong");
       console.log(error);
   }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all the authorization checks are complete. You can test to see if everything is working as expected.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://localhost:3000/sign-in&lt;/code&gt; and log in to the application. Since you assigned the editor role to the user, you can see the &lt;code&gt;Start Writing&lt;/code&gt; button:&lt;/p&gt;

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

&lt;p&gt;Click on a post to open the details page. However, you cannot see the &lt;code&gt;Publish/Unpublish&lt;/code&gt; button since only admins are allowed to see it:&lt;/p&gt;

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

&lt;p&gt;Now, go back to the &lt;code&gt;Descope Console&lt;/code&gt; and assign the user the &lt;code&gt;admin&lt;/code&gt; role.&lt;/p&gt;

&lt;p&gt;Go back to the application and refresh the page. Since the user now has the admin role, they can see the button to &lt;code&gt;Publish/Unpublish&lt;/code&gt; a post:&lt;/p&gt;

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

&lt;p&gt;This confirms that the authorization flow is working as expected.&lt;/p&gt;

&lt;p&gt;You can access the complete &lt;a href="https://github.com/kimanikevin254/descope-nextjs-auth-rbac/tree/main" rel="noopener noreferrer"&gt;Next.js 14 authentication code&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps for Next.js 14 authentication
&lt;/h2&gt;

&lt;p&gt;Your authentication system is production-ready. You can extend it by adding &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social logins&lt;/a&gt;, MFA, or additional roles—all without writing custom auth logic.&lt;/p&gt;

&lt;p&gt;This flexibility is what makes Descope's &lt;a href="https://www.descope.com/product" rel="noopener noreferrer"&gt;CIAM platform&lt;/a&gt; powerful for production applications. Use no-code workflows to add authentication methods, configure authorization rules, and manage users at scale, so you can focus on your application's core features instead of authentication infrastructure.&lt;/p&gt;

&lt;p&gt;Start building with a &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Free Forever account&lt;/a&gt; today. Have questions about implementing RBAC or Next.js authentication? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book time with our experts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Adding Authentication and SSO to a Reflex App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 27 Mar 2026 19:56:05 +0000</pubDate>
      <link>https://dev.to/descope/adding-authentication-and-sso-to-a-reflex-app-2ahk</link>
      <guid>https://dev.to/descope/adding-authentication-and-sso-to-a-reflex-app-2ahk</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/reflex-auth-sso" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reflex.dev/" rel="noopener noreferrer"&gt;Reflex&lt;/a&gt; is an open-source full-stack web framework written in and for Python that lets you build both frontend UI and backend logic in pure Python, i.e., you don’t need to manually write JavaScript or separately manage a React/Vue frontend and a separate backend.&lt;/p&gt;

&lt;p&gt;By adding Descope to your Reflex application, you can immediately take advantage of a full-fledged identity platform—complete with passwordless authentication, MFA, passkeys, social login, enterprise SSO, and customizable user journeys. &lt;/p&gt;

&lt;p&gt;This lets you keep writing your entire Reflex project in Python while relying on Descope to seamlessly deliver the advanced authentication and security capabilities needed for real-world, scalable applications.&lt;/p&gt;

&lt;p&gt;Let’s begin!&lt;/p&gt;

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

&lt;p&gt;It’s helpful to familiarize yourself with &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; and &lt;a href="https://reflex.dev/" rel="noopener noreferrer"&gt;Reflex&lt;/a&gt;. You’ll also need a &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; account, but don’t worry if you don’t have one yet—you’ll learn how to set one up shortly. The free tier will be sufficient for you to follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Descope Project
&lt;/h2&gt;

&lt;p&gt;The first step in connecting your Reflex app to Descope is to create a new Project in the Descope Console.&lt;/p&gt;

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

&lt;p&gt;In &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;Settings &amp;gt; Project&lt;/a&gt;, you’ll find your Project ID, which is a unique identifier used to link your Reflex app to Descope.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; Never paste this ID directly into your code. Instead, store it securely as an environment variable in a &lt;code&gt;.env&lt;/code&gt; file. We’ll configure this later in the blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Reflex app
&lt;/h2&gt;

&lt;p&gt;With your Descope Project set up, let’s move to the Reflex side. We’ll start with a &lt;a href="https://github.com/reflex-dev/templates/tree/main/company_dashboard" rel="noopener noreferrer"&gt;dashboard app template&lt;/a&gt; and then integrate authentication step-by-step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new directory for this app and clone the company dashboard template. Optionally, set up a Python virtual environment inside it.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;reflex-descope-auth&lt;/code&gt; to the &lt;code&gt;requirements.txt&lt;/code&gt; file. &lt;strong&gt;Note:&lt;/strong&gt; This is the &lt;a href="https://github.com/descope-sample-apps/reflex-descope-auth" rel="noopener noreferrer"&gt;Reflex Descope Auth plugin&lt;/a&gt;, which uses standard OIDC protocols to handle the login process in a reliable, familiar way.&lt;/li&gt;
&lt;li&gt;Now run this command to install all the dependencies using the command below:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Next, create a &lt;code&gt;.env&lt;/code&gt; file and set these environment variables in the project.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-descope-project-id&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;DESCOPE_FLOW_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-flow-id&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;DESCOPE_LOGOUT_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;
&lt;span class="py"&gt;SESSION_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;secure-random-secret&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can generate a secret using Python’s secrets module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import secrets; print(secrets.token_hex(32))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what your project structure should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── .gitignore
├── app
│   ├── __init__.py
│   ├── app.py
│   ├── components
│   │   ├── __init__.py
│   │   ├── documents_table.py
│   │   ├── header.py
│   │   ├── key_metrics.py
│   │   ├── sidebar.py
│   │   └── visitors_chart.py
│   └── states
│       ├── __init__.py
│       ├── auth_state.py
│       └── dashboard_state.py
├── apt-packages.txt
├── assets/
├── .env
├── requirements.txt
└── rxconfig.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;In the &lt;code&gt;app.py&lt;/code&gt; file, we’ll add a button that calls &lt;code&gt;start_login&lt;/code&gt;, a built-in method that initiates the login flow and redirects users to the Descope authorization endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The main page, which serves as the login entry point.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login with Descope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mt-8 px-8 py-3 text-white bg-red-600 rounded-lg font-semibold &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hover:bg-blue-700 transition-colors shadow-md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-col items-start max-w-xl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;container mx-auto flex flex-col lg:flex-row items-center justify-between gap-12 px-4 py-16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w-full bg-gray-50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dashboard_page&lt;/code&gt; function defines the main authenticated dashboard view of the application. It’s registered as a Reflex page using the &lt;code&gt;@rx.page()&lt;/code&gt; decorator with the root route (&lt;code&gt;/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When a user visits this page, the &lt;code&gt;on_load&lt;/code&gt; parameter triggers the &lt;code&gt;AuthState.check_login&lt;/code&gt; method to verify if the user is authenticated. If the user isn’t logged in, they’ll be redirected to the login flow automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@rx.page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_login&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dashboard_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The main dashboard page.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;header_bar&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;key_metrics_section&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nf"&gt;visitors_chart_section&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nf"&gt;documents_table_section&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p-6 space-y-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w-full h-[100vh] overflow-y-auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-row bg-gray-50 h-[100vh] w-full overflow-hidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on_mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DashboardState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_initial_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;callback&lt;/code&gt; page handles the authentication redirect after a user logs in. It’s defined using the &lt;code&gt;@rx.page()&lt;/code&gt; decorator and mapped to the &lt;code&gt;/callback&lt;/code&gt; route.&lt;/p&gt;

&lt;p&gt;When the user returns to this route after completing authentication, the &lt;code&gt;on_load&lt;/code&gt; parameter calls &lt;code&gt;AuthState.auth_redirect&lt;/code&gt;. This method processes the login response, typically validating tokens, storing session data, and determining whether the login succeeded or failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@rx.page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_redirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login Failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-red-500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Try Again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-col items-center space-y-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing login...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center space-x-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center justify-center h-screen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;auth_error&lt;/code&gt; page provides a dedicated route for handling login failures. It’s defined at the &lt;code&gt;/auth-error&lt;/code&gt; path and serves as a fallback whenever something goes wrong during the authentication process. For example, if token validation fails, the user denies consent, or the callback flow encounters an unexpected error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@rx.page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth-error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login Failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-red-500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Try Again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex flex-col items-center space-y-4 h-screen justify-center&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll also add additional routes to this file to handle page protection and move the dashboard to a dedicated page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dashboard_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth-error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Define AuthState in Reflex
&lt;/h2&gt;

&lt;p&gt;Authentication in Reflex is handled through reactive state classes. The reflex-descope-auth plugin provides a base class called &lt;code&gt;DescopeAuthState&lt;/code&gt;, which already implements the full authentication lifecycle for you. By subclassing it, we can customize how our app handles redirects and session checks after a user logs in or out. The &lt;code&gt;reflex-descope-auth&lt;/code&gt; plugin validates Descope tokens only once during &lt;code&gt;finalize_auth&lt;/code&gt;. After that, Reflex simply stores the session in state and does not re-check the &lt;code&gt;exp&lt;/code&gt; or &lt;code&gt;rexp&lt;/code&gt; claims in the token automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;finalize_auth&lt;/code&gt; is a built-in method that completes the authentication process after the redirect from Descope, exchanges the authorization code for tokens, verifies ID token, and creates a session token for the user.&lt;/p&gt;

&lt;p&gt;By using &lt;code&gt;yield&lt;/code&gt;, Reflex processes each step reactively—first finalizing auth, then deciding the next redirect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;reflex&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reflex_descope_auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DescopeAuthState&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DescopeAuthState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@rx.event&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;DescopeAuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalize_auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth-error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@rx.event&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logged_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check_login()&lt;/code&gt; is a custom method that acts as a page guard for any route that should only be accessible to authenticated users. It works by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking the reactive state variable &lt;code&gt;self.logged_in&lt;/code&gt; (which is automatically managed by &lt;code&gt;DescopeAuthState&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If the user isn’t authenticated (&lt;code&gt;self.logged_in&lt;/code&gt; is False), it immediately returns a redirect to the homepage (&lt;code&gt;/&lt;/code&gt;), which serves as the login page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures that only valid sessions can access private pages like &lt;code&gt;/dashboard&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing OAuth social logins, magic links, and SSO
&lt;/h2&gt;

&lt;p&gt;Once your Reflex app is wired up with the &lt;a href="https://github.com/descope-sample-apps/reflex-descope-auth" rel="noopener noreferrer"&gt;Reflex Descope Auth plugin&lt;/a&gt;, adding new authentication methods becomes incredibly simple. Descope flows let you drag, drop, and configure options like OAuth social logins, magic links, and SAML/OIDC SSO without changing any of your Reflex code.&lt;/p&gt;

&lt;p&gt;If you want users to sign in with &lt;a href="https://docs.descope.com/auth-methods/oauth/providers/setting-up-your-own-apps/google" rel="noopener noreferrer"&gt;Google&lt;/a&gt;, &lt;a href="https://docs.descope.com/auth-methods/oauth/providers/setting-up-your-own-apps/github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or any other providers, you can configure these &lt;a href="https://docs.descope.com/auth-methods/oauth/providers" rel="noopener noreferrer"&gt;OAuth providers&lt;/a&gt; in the authentication methods in the console and add the &lt;a href="https://docs.descope.com/auth-methods/oauth" rel="noopener noreferrer"&gt;OAuth&lt;/a&gt; action in the flow. For passwordless experiences, you can switch to magic link login by adding the built-in &lt;a href="https://docs.descope.com/auth-methods/magic-link" rel="noopener noreferrer"&gt;magic link&lt;/a&gt; action in your flow. And when your app needs enterprise authentication, you can add a &lt;a href="https://docs.descope.com/auth-methods/sso" rel="noopener noreferrer"&gt;SSO&lt;/a&gt; action to the same flow.&lt;/p&gt;

&lt;p&gt;If you’d like to try SSO end-to-end, you can spin up a tenant and walk through the setup using the &lt;a href="https://docs.descope.com/management/tenant-management/sso/mock-saml-testing" rel="noopener noreferrer"&gt;Mock SAML Testing guide&lt;/a&gt;. And if your organization already uses a SAML IdP such as Okta, ADFS, or PingFederate, you can simply plug in its metadata URL in place of the mock provider.&lt;/p&gt;

&lt;p&gt;With everything controlled through Descope’s visual flow editor, your Reflex integration remains the same, with the plugin automatically handling redirects and token exchange. Here’s an example flow that combines the social login, magic link and SSO authentication:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fockt4ewb087e90wh7v1s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fockt4ewb087e90wh7v1s.png" alt="Flow with social login, magic links and SSO authentication methods" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest hurdles is enabling each customer to set up Single-Sign-On (SSO) with their own identity provider (IdP). Descope’s &lt;a href="https://docs.descope.com/auth-methods/sso/sso-setup-suite" rel="noopener noreferrer"&gt;SSO Setup Suite&lt;/a&gt; simplifies how organizations configure Single Sign-On (SSO) for their tenants. Instead of manually exchanging metadata and troubleshooting IdP configurations, the suite offers a guided, self-service flow that allows tenant admins to set up and test their SSO integration (SAML or OIDC) end-to-end.&lt;/p&gt;

&lt;p&gt;With built-in steps for attribute mapping, SCIM provisioning, and domain routing, it significantly reduces setup time and support overhead by empowering customers to get their SSO running smoothly with minimal developer involvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying the user name and logging out a user
&lt;/h2&gt;

&lt;p&gt;Once a user successfully signs in, we can use &lt;code&gt;AuthState.userinfo&lt;/code&gt; to display the user’s profile details such as their name, while &lt;code&gt;AuthState.logged_in&lt;/code&gt; tracks the current authentication status. &lt;code&gt;userinfo&lt;/code&gt; comes from the JWT claims inside the session token. The plugin extracts claims from the token and &lt;code&gt;userinfo&lt;/code&gt; simply reads from there. To be able to add custom attributes, you can define &lt;a href="https://docs.descope.com/flows/actions/custom-claims" rel="noopener noreferrer"&gt;custom claims&lt;/a&gt; in Descope.&lt;/p&gt;

&lt;p&gt;In the code snippet below, the header displays a personalized welcome message along with a Logout button when the user is signed in and &lt;code&gt;AuthState.logout&lt;/code&gt; will allow the user to log out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;reflex&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.states.auth_state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthState&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;header_bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The header bar component.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logged_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-sm font-medium text-gray-700&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Logout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;px-4 py-1 text-sm text-white bg-red-600 rounded-md hover:bg-red-700&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center space-x-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flex items-center justify-between h-12 px-6 bg-white border-b border-gray-200&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard will then look similar to this:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Reflex SSO and authentication with Descope
&lt;/h2&gt;

&lt;p&gt;With Descope, implementing authentication in a Reflex app becomes both simple and highly flexible. Descope’s flows let you design every step of the user journey without managing complex logic yourself. The Reflex Descope Auth plugin then brings these flows directly into your application, handling the redirects and tokens. Together, they give you a powerful, low-code way to add secure, customizable authentication to any Reflex project.&lt;/p&gt;

&lt;p&gt;If you have any questions about Descope or a customer identity project we can help with, &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt; with our auth experts.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Gradio App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 25 Mar 2026 20:46:42 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-gradio-app-3oia</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-gradio-app-3oia</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-sso-gradio" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gradio.app/" rel="noopener noreferrer"&gt;Gradio&lt;/a&gt; is an open source Python package that allows you to create web-based interfaces for AI models, APIs, or any Python function. Its simplicity and flexibility make it a popular choice among developers who want to quickly prototype and deploy web-based interfaces without worrying about frontend development.&lt;/p&gt;

&lt;p&gt;Secure authentication methods are needed for these applications to help prevent sensitive data and models from being exposed and ensure that only authorized users can access certain features. &lt;a href="https://www.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; is an authentication and user management platform that simplifies the process of adding &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;secure authentication&lt;/a&gt; to your applications. It offers a range of authentication methods and out-of-the-box support for &lt;a href="https://www.descope.com/learn/post/oauth#oauth-and-openid-connect-(oidc)" rel="noopener noreferrer"&gt;OAuth 2.0 and OpenID Connect (OIDC)&lt;/a&gt;, which makes it easy to implement authentication in your Gradio app.&lt;/p&gt;

&lt;p&gt;In this guide, you’ll learn how to integrate Descope authentication and &lt;a href="https://www.descope.com/learn/post/sso" rel="noopener noreferrer"&gt;single sign-on (SSO)&lt;/a&gt; into a Gradio application. You’ll add basic auth to your Gradio application using &lt;a href="https://www.descope.com/use-cases/magic-links" rel="noopener noreferrer"&gt;magic links&lt;/a&gt; and &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social login&lt;/a&gt;. You’ll also implement SSO into the application by configuring Descope to use &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; as an identity provider(IdP).&lt;/p&gt;

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

&lt;p&gt;Many Gradio applications are used for business-to-business (B2B) purposes, where different organizations need secure access to their machine-learning models or APIs. In such cases, SSO is essential for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seamless access:&lt;/strong&gt; Employees can log in using their company credentials instead of managing separate usernames and passwords.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Improved security:&lt;/strong&gt; SSO reduces password fatigue and minimizes security risks associated with weak or reused passwords.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better user management:&lt;/strong&gt; IT administrators can enforce access policies, control permissions, and revoke access centrally through identity providers like Okta or Azure AD.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Descope account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://developer.okta.com/signup/" rel="noopener noreferrer"&gt;Okta developer account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;Python 3&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/book/en/v2/Getting-Started-The-Command-Line" rel="noopener noreferrer"&gt;Git CLI&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;A code editor and a web browser&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Creating a Gradio application
&lt;/h2&gt;

&lt;p&gt;To keep the focus on implementing authentication and SSO, the tutorial uses a prepared starter template that you’ll build on. To clone it to your local machine, execute the command below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--single-branch&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; starter-template https://github.com/kimanikevin254/descope-gradio-auth-sso.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gradio application you just cloned is mounted within a FastAPI application. This setup is necessary because Gradio only supports authentication with external OAuth providers, such as Descope, when it is mounted inside a FastAPI app.&lt;/p&gt;

&lt;p&gt;Here’s an overview of the most important files in this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/core/config.py&lt;/code&gt;: Loads environment variables and sets application configurations using Pydantic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/ui/gradio_apps/&lt;/code&gt;: Contains three simple Gradio applications:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;admin_dashboard.py&lt;/code&gt;: Displays user info and a logout button. In a real-world app, this would handle admin-specific features.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_dashboard.py&lt;/code&gt;: Similar to the admin dashboard but for regular users. This is where you would implement non-admin functionalities.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;login_page.py&lt;/code&gt;: A simple login page that prompts users to log in with a login button.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;app/ui/gradio_mount.py&lt;/code&gt;: Defines the GradioMounter class, which mounts all Gradio apps to the FastAPI application.&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt;: Defines the environment variables you will require in this project.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;You’ll explore the other files later.&lt;/p&gt;

&lt;p&gt;With the files cloned to your local machine, you can move on to setting up the application. Start by creating a virtual environment, activating it, and installing all the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="c"&gt;# Create the virtual environment&lt;/span&gt;

&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate &lt;span class="c"&gt;#Activate it&lt;/span&gt;

pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rename the &lt;code&gt;.env.example&lt;/code&gt; file to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the application using the command fastapi dev app/main.py and navigate to &lt;code&gt;http://localhost:8000/auth/&lt;/code&gt; to view the login page created using Gradio:&lt;/p&gt;

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

&lt;p&gt;To view the admin dashboard, navigate to &lt;code&gt;http://localhost:8000/gradio/admin&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;To view the user dashboard, navigate to &lt;code&gt;http://localhost:8000/gradio/user&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;As you can see, all the dashboards are publicly available, which poses some security risks. In the next sections, you’ll add authentication to protect them and authorization to make sure that only users with the appropriate role can access the admin dashboard.&lt;/p&gt;

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

&lt;p&gt;To integrate Descope as an external OAuth provider for your Gradio application, you’ll need to apply some configurations in your Descope console.&lt;/p&gt;

&lt;p&gt;Open your &lt;a href="https://app.descope.com/" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt; and create a new project by clicking the project dropdown and selecting &lt;strong&gt;+ Project&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;Provide a project name on the &lt;strong&gt;Create project&lt;/strong&gt; form and click &lt;strong&gt;Create&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;You need to define a &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;flow&lt;/a&gt; that will support the authentication methods you require for this project: magic links, social login, and SSO. To simplify the process, this tutorial uses a preconfigured flow included in the starter template. You’ll find it in the root folder as &lt;code&gt;sign-up-or-in.json&lt;/code&gt;. You only need to import it into Descope. To do this, navigate to &lt;strong&gt;Flows &amp;gt; sign-up-or-in&lt;/strong&gt; and select &lt;strong&gt;Import flow / Export flow &amp;gt; Import flow&lt;/strong&gt; inside the flow editor. This will allow you to upload the flow from your local machine:&lt;/p&gt;

&lt;p&gt;The flow you just imported offers three login options on the welcome screen: magic link, SSO, and social login. If a user chooses the magic link, they receive an email with a link. Clicking the link prompts new users to provide extra details while existing users get a JWT and complete the process. For social login or SSO, users are redirected to their provider, and after successful authentication, they receive a JWT, ending the flow.&lt;/p&gt;

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

&lt;p&gt;With Descope fully set up, you can now integrate it as a custom OAuth provider for your Gradio application. You’ll need to configure an OIDC app in the Descope dashboard to obtain the necessary endpoints and credentials for the authorization code flow.&lt;/p&gt;

&lt;p&gt;Here are the required credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; A unique public identifier assigned to the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client secret:&lt;/strong&gt; A confidential key shared only between the application and the authorization server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization endpoint:&lt;/strong&gt; The URL that starts the authentication process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access token endpoint:&lt;/strong&gt; The URL used to exchange an authorization code for access tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User info endpoint:&lt;/strong&gt; The URL that retrieves details about the authenticated user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URL:&lt;/strong&gt; The destination where users are sent after completing authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWKs URI:&lt;/strong&gt; The URL where the authorization server’s public keys are stored for verifying tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes:&lt;/strong&gt; Permissions that define what data and actions the application can access on behalf of the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will define these in the &lt;code&gt;.env&lt;/code&gt; file. Some values for the variables are already included in the starter template, as are common for everyone. To get the ones that are not provided, navigate to &lt;strong&gt;Applications &amp;gt; OIDC default application&lt;/strong&gt; on your Descope console. Scroll down to the &lt;strong&gt;SP Configuration&lt;/strong&gt; section, copy the value of the Client ID, and assign it to the &lt;code&gt;OAUTH_CLIENT_ID&lt;/code&gt; variable in the &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;

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

&lt;p&gt;Make sure you also replace the placeholder inside the &lt;code&gt;OAUTH_JWKS_URI&lt;/code&gt;'s value with the same client ID.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OAUTH_SCOPES&lt;/code&gt; variable in the &lt;code&gt;.env&lt;/code&gt; file specifies the scopes your application will request from Descope. However, Descope does not include the &lt;code&gt;descope.claims&lt;/code&gt; (user’s roles, permissions, and tenants) and &lt;code&gt;descope.custom_claims&lt;/code&gt; (user’s custom claims) scopes in its response by default unless explicitly configured.&lt;/p&gt;

&lt;p&gt;To enable these scopes, go to the &lt;strong&gt;OIDC default application&lt;/strong&gt; details page. In the &lt;strong&gt;IDP Configuration&lt;/strong&gt; section, add &lt;code&gt;descope.claims&lt;/code&gt; and &lt;code&gt;descope.custom_claims&lt;/code&gt; under &lt;strong&gt;Supported Claims&lt;/strong&gt;. Make sure to save the changes:&lt;/p&gt;

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

&lt;p&gt;To obtain the value for the &lt;code&gt;OAUTH_CLIENT_SECRET&lt;/code&gt; variable, navigate to &lt;strong&gt;M2M &amp;gt; + Access Key&lt;/strong&gt; on your Descope console. On the &lt;strong&gt;Generate Access Key&lt;/strong&gt; page, provide a name for your key and select &lt;strong&gt;Generate Key&lt;/strong&gt;. Copy the value of the generated key and assign it to the &lt;code&gt;OAUTH_CLIENT_SECRET&lt;/code&gt; variable in the &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;

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

&lt;p&gt;You now have all the required values for implementing the authentication logic. Open the &lt;code&gt;app/core/auth.py&lt;/code&gt; file and add the following method to the &lt;code&gt;Auth&lt;/code&gt; class to initialize the OAuth client with the necessary settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Initialize the OAuth client with settings
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;init_oauth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;
   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;authorize_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_AUTHORIZE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;access_token_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_ACCESS_TOKEN_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jwks_uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_JWKS_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;userinfo_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_USERINFO_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_SCOPES&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following methods to the same class. These methods will redirect the user to the authorization endpoint set up for the OAuth client and exchange the returned authorization code for an access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Redirect to authorization endpoint
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_NAME&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;authorize_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Exchange authorization code for access token   
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_NAME&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;authorize_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;OAuthError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_401_UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAuth error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following method to the same class to retrieve the currently authenticated user from the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get the current authenticated user name
&lt;/span&gt;   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method helps check if a request is authenticated when accessing API routes. If the user is not authenticated, you can take appropriate action, such as redirecting them to the login page.&lt;/p&gt;

&lt;p&gt;You’ll also need a method to protect Gradio apps by ensuring the user is authenticated and has the necessary roles. Add the following method to the &lt;code&gt;Auth&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Authenticate user and authorize based on path and roles
# To protect Gradio app routes
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate_and_authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
   &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
   &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
   &lt;span class="n"&gt;tenants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenants&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
   &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;

   &lt;span class="c1"&gt;# Avoid blocking the gradio queue requests
&lt;/span&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/gradio_api/queue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_ROLE&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pass. returning&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following method to the &lt;code&gt;Auth&lt;/code&gt; class to determine which Gradio app an authenticated user should see based on their roles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Determine the Gradio app to show the user based on their roles
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_redirect_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
   &lt;span class="n"&gt;tenants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenants&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
   &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
   &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You have not set up tenants in your Descope account. You can only access the user dashboard since the roles needed to access the admin dashboard are set up via the tenant.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOGIN_PAGE_PATH&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_ROLE&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_DASHBOARD_PATH&lt;/span&gt;
   &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_DASHBOARD_PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure the OAuth client is correctly initialized when the application starts, open the &lt;code&gt;app/main.py&lt;/code&gt; file and add the following code immediately after the middleware configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Initialize OAuth client
&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init_oauth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up all the auth routes by adding the code to the &lt;code&gt;app/api/routes/auth_router.py&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Redirect to OAuth login provider
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;redirect_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_callback&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Handle OAuth callback after successful login
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth_callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userinfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Log the error
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authentication error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth/error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Log out the current user   
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/logout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Authentication error page
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authentication failed. Please try again.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code defines routes that allow the user to log in, handle the OAuth callback, and log out of the application.&lt;/p&gt;

&lt;p&gt;Set up the dashboard routes that will display the appropriate dashboards to the user based on their roles by replacing the existing route in the &lt;code&gt;app/api/routes/dashboard_router.py&lt;/code&gt; file with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Main entry point, redirects based on user role
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;redirect_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user_redirect_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redirect_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOGIN_PAGE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Routing endpoint for Gradio dashboards
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/gradio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;redirect_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user_redirect_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redirect_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTTP_302_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, protect the Gradio dashboards by replacing the &lt;code&gt;mount_admin_dashboard&lt;/code&gt; and &lt;code&gt;mount_user_dashboard&lt;/code&gt; methods in the &lt;code&gt;app/ui/gradio_mount.py&lt;/code&gt; file with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Mount the admin dashboard with authorization   
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mount_admin_dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;admin_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;admin_dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_gradio_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;admin_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADMIN_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth_dependency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticate_and_authorize&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Mount the user dashboard with authorization
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mount_user_dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;user_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_gradio_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_dashboard_wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_DASHBOARD_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth_dependency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticate_and_authorize&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code adds the &lt;code&gt;auth_dependency&lt;/code&gt; parameter before mounting these apps, which runs before any Gradio-related route in your FastAPI app. This ensures that proper authentication and authorization checks are performed before displaying the dashboards.&lt;/p&gt;

&lt;p&gt;You can now log in to the application using either magic links or social login.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OIDC&lt;/a&gt; is an identity layer built on top of the OAuth 2.0 framework. It allows applications to authenticate users securely by delegating authentication to a trusted IdP, such as Okta. OIDC simplifies SSO by enabling users to log in once and access multiple applications without reentering credentials.&lt;/p&gt;

&lt;p&gt;To implement SSO, you can configure Okta as the IdP and Descope as the authentication service. Start by launching your Okta admin dashboard and navigating to &lt;strong&gt;Applications &amp;gt; Applications &amp;gt; Browse App Catalog&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;On the &lt;strong&gt;Browse App Catalog&lt;/strong&gt; page, search for &lt;code&gt;descope&lt;/code&gt;, and select &lt;strong&gt;Descope&lt;/strong&gt; from the search results:&lt;/p&gt;

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

&lt;p&gt;Select &lt;strong&gt;+ Add Integration&lt;/strong&gt; from the Descope app details page:&lt;/p&gt;

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

&lt;p&gt;On the &lt;strong&gt;General Settings&lt;/strong&gt; tab, leave everything as is and click &lt;strong&gt;Next&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;On the &lt;strong&gt;Sign-On&lt;/strong&gt; options tab, select &lt;strong&gt;OpenID Connect&lt;/strong&gt; as the sign-on method:&lt;/p&gt;

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

&lt;p&gt;Scroll down to the &lt;strong&gt;Advanced Sign-on Settings&lt;/strong&gt; section, and in the &lt;strong&gt;Callback URL&lt;/strong&gt; field, provide the value &lt;code&gt;https://api.descope.com/v1/oauth/callback&lt;/code&gt;. This defines the URL where the user will be redirected after getting successfully authenticated by Okta. Save the changes by clicking &lt;strong&gt;Done&lt;/strong&gt; at the bottom of the page:&lt;/p&gt;

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

&lt;p&gt;You also need to assign users to the Descope app you just configured. This will ensure that only authorized users can authenticate via the app. To do this, go to the app’s details page and the &lt;strong&gt;Assignments&lt;/strong&gt; tab and select &lt;strong&gt;Assign &amp;gt; Assign to Groups&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;On the &lt;strong&gt;Assign Descope to Groups&lt;/strong&gt; page, select the &lt;strong&gt;Assign&lt;/strong&gt; button beside the &lt;strong&gt;Everyone&lt;/strong&gt; group to allow all users in your organization to authenticate via the app, and select &lt;strong&gt;Done&lt;/strong&gt; to save the changes.&lt;/p&gt;

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

&lt;p&gt;Select the &lt;strong&gt;Sign On&lt;/strong&gt; tab and take note of your OIDC client ID and secret:&lt;/p&gt;

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

&lt;p&gt;Go to the Descope console to create a tenant that you will use with the Descope app you just configured in Okta. On your Descope console, navigate to &lt;strong&gt;Tenants &amp;gt; + Tenant&lt;/strong&gt;, provide the tenant details on the &lt;strong&gt;Create Tenant&lt;/strong&gt; form, and select &lt;strong&gt;Create&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;Open the details page of the Tenant you just created, add your email domain under the &lt;strong&gt;Tenant Settings &amp;gt; Details&lt;/strong&gt; section in the &lt;strong&gt;Email domain&lt;/strong&gt; field, and save the changes:&lt;/p&gt;

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

&lt;p&gt;Select &lt;strong&gt;Authentication Methods &amp;gt; SSO&lt;/strong&gt; from the tenant details page sidebar, and under &lt;strong&gt;Authentication Protocol&lt;/strong&gt;, select &lt;strong&gt;OIDC&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;Under &lt;strong&gt;Tenant Details &amp;gt; SSO Domains&lt;/strong&gt;, provide your email domain. This will help to determine which SSO configuration to load once a user chooses to authenticate using SSO:&lt;/p&gt;

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

&lt;p&gt;Scroll down to the &lt;strong&gt;SSO configuration &amp;gt; Account Settings&lt;/strong&gt; section and provide the required values as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider Name:&lt;/strong&gt; &lt;code&gt;Okta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; The Client ID you obtained from the Okta dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret:&lt;/strong&gt; The client secret you obtained from the Okta dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; &lt;code&gt;openid profile email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant Type:&lt;/strong&gt; &lt;code&gt;Authorization code&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To obtain the values needed for the &lt;strong&gt;SSO configuration &amp;gt; Connection Settings&lt;/strong&gt; section, you’ll need the OAuth endpoints from the &lt;a href="https://developer.okta.com/docs/concepts/auth-servers/#discovery-endpoints-org-authorization-servers" rel="noopener noreferrer"&gt;Okta “well-known” configuration&lt;/a&gt;. Navigate to &lt;code&gt;https://&amp;lt;YOUR-OKTA-INSTANCE&amp;gt;.okta.com/.well-known/openid-configuration&lt;/code&gt; on your browser, and take note of the issuer and authorization, token, user info, and JWKs endpoints.&lt;/p&gt;

&lt;p&gt;Make sure to replace &lt;code&gt;&amp;lt;YOUR-OKTA-INSTANCE&amp;gt;&lt;/code&gt; with the correct value, which is your organization’s Okta instance ID.&lt;/p&gt;

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

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

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

&lt;p&gt;SSO with Okta as the IdP is now fully configured.&lt;/p&gt;

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

&lt;p&gt;To run the application and confirm that everything is working as expected, use the command &lt;code&gt;fastapi dev app/main.py&lt;/code&gt; and navigate to &lt;code&gt;http://localhost:8000&lt;/code&gt; on your browser. You will be redirected to the login page:&lt;/p&gt;

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

&lt;p&gt;Click the &lt;strong&gt;Login&lt;/strong&gt; button, and you’ll be redirected to Descope’s sign-in page, which is powered by the flow you configured earlier. You can sign in using any of the available methods:&lt;/p&gt;

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

&lt;p&gt;After successful authentication, you’ll be redirected to the user dashboard. This is because you don’t have the appropriate roles to access the admin dashboard:&lt;/p&gt;

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

&lt;p&gt;If you manually tried to navigate to &lt;code&gt;http://localhost:8000/gradio/admin&lt;/code&gt;, you will get an error informing you that you’re not authenticated:&lt;/p&gt;

&lt;p&gt;To check if a user with the appropriate role can access the admin dashboard, open the &lt;a href="https://app.descope.com/users" rel="noopener noreferrer"&gt;users’ page&lt;/a&gt; on the Descope console, edit your user to assign them the &lt;code&gt;Tenant Admin&lt;/code&gt; role, and save the changes:&lt;/p&gt;

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

&lt;p&gt;Now, go back to the app, log out, and log in again. You should be redirected to the admin dashboard:&lt;/p&gt;

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

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

&lt;p&gt;In this guide, you learned how to integrate Descope as an OAuth provider for your Gradio application using OIDC. You also explored how to implement SSO with Okta as the identity provider and Descope as the authentication service. Additionally, you implemented role-based access control to protect Gradio app routes.&lt;/p&gt;

&lt;p&gt;Descope is a drag-and-drop customer authentication and identity management platform. Our no- or low-code CIAM solution helps hundreds of organizations easily create and customize their entire user journey using visual workflows—from authentication and authorization to MFA and federated SSO. Customers such as GoFundMe, &lt;a href="https://www.descope.com/customers/navan" rel="noopener noreferrer"&gt;Navan&lt;/a&gt;, &lt;a href="https://www.descope.com/customers/you-com" rel="noopener noreferrer"&gt;You.com&lt;/a&gt;, and &lt;a href="https://www.descope.com/customers/branch" rel="noopener noreferrer"&gt;Branch&lt;/a&gt; use Descope to reduce user friction, prevent account takeover, and get a unified view of their customer journey.&lt;/p&gt;

&lt;p&gt;To learn more, join our dev community, &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;, and explore the &lt;a href="https://docs.descope.com/" rel="noopener noreferrer"&gt;Descope documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a Custom Search Engine</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Tue, 09 Aug 2022 12:15:00 +0000</pubDate>
      <link>https://dev.to/techthrusters/custom-search-engine-3o1f</link>
      <guid>https://dev.to/techthrusters/custom-search-engine-3o1f</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Build a personalised search engine with Google's search API.&lt;/em&gt;&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Just a heads up that this is not actually an entire search engine but you can definitely learn to play and customise this project as per your needs using Google's API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We are going to build a custom search engine using Google's Search API to fetch queries and display the most relevant search results. &lt;/p&gt;

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

&lt;p&gt;To build this search engine, we're going to need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;Javascript&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://replit.com/~" rel="noopener noreferrer"&gt;Repl.it&lt;/a&gt; account&lt;/li&gt;
&lt;li&gt;Google account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll also be getting our hands on Google APIs, and you don't need to worry if you don't know about how you can use it, because we'll be taking you through that as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt; Getting Started&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;We are providing you the &lt;a href="https://replit.com/@mrunankpawar/CSEHCTBStarter" rel="noopener noreferrer"&gt;starter files&lt;/a&gt; for this project and &lt;strong&gt;fork&lt;/strong&gt; it so you can make the changes and get started with building the search engine. The starter file includes the styles and preliminary code to help you kickstart this 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%2F25helgvi6hgpzcblznmq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25helgvi6hgpzcblznmq.png" alt="Starter files" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once this is ready, head on over to the &lt;code&gt;index.html&lt;/code&gt; file and add the following between the &lt;code&gt;&amp;lt;body&amp;gt; &amp;lt;/body&amp;gt;&lt;/code&gt; tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src="brewgle.png" width="525" class="image"&amp;gt;

&amp;lt;div class="search"&amp;gt;
  &amp;lt;input type="text" id="search" class="input"&amp;gt;
  &amp;lt;button id="submit" class="searchbtn" onclick="submit()"&amp;gt;Search&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will reference the image with it's corresponding styling! Feel free to add your own to make it personalized. Then, we are going to add a search input and button that calls submit() when clicked.&lt;/p&gt;

&lt;p&gt;Now, we need to create a section for the content to appear! Add the following right below the &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div id="buttons" class="buttons"&amp;gt;
  &amp;lt;button id="allbutton" class="all" onclick="submit()"&amp;gt;&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div id="content"&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, let's reference the javascript files to connect the queries to the web!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script src="script.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script id="query" src="query.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;u&gt; Google API&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;Supercool—the interface is now complete! Now let's fetch some queries. In your &lt;code&gt;query.js&lt;/code&gt; file, you should see a function that calls &lt;code&gt;submit()&lt;/code&gt;. Within that function, paste the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.getElementById('buttons').style.display = 'block';
document.getElementById('content').innerHTML = '';
var val = document.getElementById('search').value;
var newelement = document.createElement('script');
newelement.src = `https://www.googleapis.com/customsearch/v1?key=API_KEY&amp;amp;cx=003606982592251140240:5xbiwoxb3m0&amp;amp;q=${val}&amp;amp;callback=hndlr`;
newelement.id = 'mainscript';
document.body.appendChild(newelement);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we need to set up the Google Search API! To get started, head on over to &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;Google's Developer Portal&lt;/a&gt; and make sure that you are logged in. Once complete, scroll down to Get a Key and create a project called Search. Once you click next, copy your API key and click Done. In your &lt;code&gt;query.js&lt;/code&gt; file, you will see a variable called &lt;code&gt;newelement.src&lt;/code&gt;. Where it says &lt;em&gt;API_KEY&lt;/em&gt;, paste in your API Key and you're good to go!&lt;/p&gt;

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

&lt;p&gt;So what is this URL and why is it so long?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;https://www.googleapis.com/customsearch/v1?&lt;/code&gt; lets us know that we are using the Google Custom Search API, the first version of it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key=API_KEY&lt;/code&gt; indicates the user of the API, as well as any limitations. For example, this will associate the number of queries per day with your account&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;cx=&lt;/code&gt; associates any searches with a search engine Id&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q=${val}&lt;/code&gt; is the actual query that a user inputs, which we will fetch from the submit() function in our HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;callback=hndlr&lt;/code&gt; is used as a parameter that is called after the function is executed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;u&gt; Formatting Query Results&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;Now that this is completed, let's format our content. Head over to the script.js file and you should see a function called hdnlr(response). Inside this function, add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try {

} catch(error) {

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essentially, the function will try some code, and if it does not work, will catch &amp;amp; output an error. Within the try function, add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.getElementById('content').innerHTML += `
  &amp;lt;div style="color: grey;"&amp;gt;
    Wohooo! About ${response.searchInformation.formattedTotalResults} results in ${response.searchInformation.formattedSearchTime} seconds!
  &amp;lt;/div&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fetches the amount of results and the time it took to retrieve, just like how you see on Google! Then, to fetch the actual information, add the following to your &lt;code&gt;try&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (var i = 0; i &amp;lt; response.items.length; i++) { 
  document.getElementById('content').innerHTML += `
  &amp;lt;div style="align-items: center;"&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;a style="color: grey; font-size: 12px; text-decoration: none;" href=${response.items[i].link} target="_blank"&amp;gt;${response.items[i].link}&amp;lt;/a&amp;gt;
    &amp;lt;a target="_blank" href=${response.items[i].link} style="text-decoration: none;"&amp;gt;
      &amp;lt;h2 style="margin-top: 2px;"&amp;gt;${response.items[i].title}&amp;lt;/h2&amp;gt;
    &amp;lt;/a&amp;gt;
    &amp;lt;div style="margin-top: -8px;"&amp;gt;
      ${response.items[i].htmlSnippet}
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;`;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, your code will fetch the &lt;code&gt;link, title and htmlSnippet&lt;/code&gt; to display for each query. To output any errors, add the following to the catch function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.getElementById('content').innerHTML = 'error!';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that, you've successfully fetched your meta data for each query! Now, click 'Run' at the top of your Repl and watch the magic happen!&lt;/p&gt;

&lt;p&gt;If your code outputs an error, feel free to reference the final code!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt; Magic Time!&lt;/u&gt;
&lt;/h2&gt;

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

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

&lt;p&gt;And that's it! If you click run and head to your replit link, you should see a full functional search engine at your service! Here's a link to the &lt;a href="https://replit.com/@mrunankpawar/SearchEngine" rel="noopener noreferrer"&gt;final code&lt;/a&gt; (excluding the API key) if you need any help!&lt;/p&gt;

&lt;h2&gt;
  
  
  Few important points to remember:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Please do not share your API key with anyone&lt;/li&gt;
&lt;li&gt;Do not exceed 100 queries per day (Because Google provides only 100 free queries a day, or else you can exceed your limit by paying the extra charges)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Happy Hacking and join &lt;a href="https://techbrewers.hackclub.com/" rel="noopener noreferrer"&gt;Hack Club TechBrewers&lt;/a&gt; for building more amazing stuff 🚀&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>searchengine</category>
      <category>programming</category>
    </item>
    <item>
      <title>A session on Introduction to SparkAR</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 22 Oct 2021 16:43:12 +0000</pubDate>
      <link>https://dev.to/hackthisfall/a-session-on-introduction-to-sparkar-59dd</link>
      <guid>https://dev.to/hackthisfall/a-session-on-introduction-to-sparkar-59dd</guid>
      <description>&lt;p&gt;I conducted a session on Introduction to Spark AR where we travelled through all the steps right from prerequisites to publishing an effect.&lt;/p&gt;

&lt;p&gt;Most of the folks attending the session were new to Augmented Reality so they were quite interested to learn how it works and how they themselves can create their own AR Filter.&lt;/p&gt;

&lt;p&gt;There were about 30-40 active folks who learnt something new in the session and also I got some requests &lt;/p&gt;

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

&lt;p&gt;Also got some important reviews which will help me to improvise my upcoming events also shared about Hack This Fall 2.0 at the event which will be helpful for the students to kickstart their hackathon journey.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Create a simple Facebook and Instagram AR filter</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Sun, 12 Sep 2021 15:59:45 +0000</pubDate>
      <link>https://dev.to/hackthisfall/create-a-simple-facebook-and-instagram-ar-filter-142m</link>
      <guid>https://dev.to/hackthisfall/create-a-simple-facebook-and-instagram-ar-filter-142m</guid>
      <description>&lt;p&gt;Hello folks!&lt;/p&gt;

&lt;p&gt;We all are using social media and have used various AR Filters which are available on Facebook and Instagram. Well, what if I tell you that we can create our own AR Filter and publish it so that everyone can use it.&lt;/p&gt;

&lt;p&gt;This blog post will go through all the steps needed to create a simple AR Filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install SparkAR Studio and SparkAR player
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Spark AR Studio: Spark AR Studio is the place where we will be creating the effect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spark AR Player: Spark AR Player is the place where we will be testing our effect (Android and iOS)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Keep your assets ready
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I recommend that all the images you want in your effect must be ready in one folder so that it will be easy to just add those images as material later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can create the assets with the help of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Photoshop&lt;/li&gt;
&lt;li&gt;AR Library &lt;/li&gt;
&lt;li&gt;Canva&lt;/li&gt;
&lt;li&gt;Any editing app you wish to use&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Open Spark AR studio
&lt;/h3&gt;

&lt;p&gt;Create a blank 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%2Fh18c4t9ngxbhga66kbha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh18c4t9ngxbhga66kbha.png" alt="Home.PNG" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is how our working environment looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Add a face tracker
&lt;/h3&gt;

&lt;p&gt;By adding a Face Tracker to your effect you can track the face movement of the user.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Right-click in the scene panel and add a Face tracker.&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%2Feonj2dq36ofrvxb3ye27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feonj2dq36ofrvxb3ye27.png" alt="facetracker.PNG" width="507" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Add planes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Once again right-click in the scenes panel and add a plane to your project.&lt;/li&gt;
&lt;li&gt;And drag it under the face-tracker. This will sync the plane with your face movement.&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%2Fue70i4fcrjacg2yvtf3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fue70i4fcrjacg2yvtf3h.png" alt="plane.PNG" width="436" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Add material to the plane.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Click on the plane in the scenes panel&lt;/li&gt;
&lt;li&gt;Now add material to your plane. (on the right side).&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%2Fe9j9if9ap61pp7y6fuad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9j9if9ap61pp7y6fuad.png" alt="material.PNG" width="278" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Try to rename the materials such that you will be able to understand them later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 7: Adding texture to the material.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;On the right side you will be able to see the texture. &lt;/li&gt;
&lt;li&gt;Select your assets from the drop-down or by choosing the file.&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%2Fi0580b0yqqsk7uyjhnx2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0580b0yqqsk7uyjhnx2.png" alt="texture.PNG" width="395" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Adjustments
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You can adjust the position of the plane by playing around with the x, y, and z axes.&lt;/li&gt;
&lt;li&gt;You can also perform image transformations by changing the values.&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%2Fj3byxox6h47e1mvlgekv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3byxox6h47e1mvlgekv.png" alt="adjustments.PNG" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9: Testing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt; Now, we are ready with our filter and it's time for testing.&lt;/li&gt;
&lt;li&gt;You can test it using Spark AR Player.&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%2Fv6ukro6sq43h4exohuq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6ukro6sq43h4exohuq5.png" alt="test.PNG" width="463" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10: Publish your effect
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Now it's time for the world to use your AR Filter. &lt;/li&gt;
&lt;li&gt;Click on the Publish button.&lt;/li&gt;
&lt;li&gt;You have to provide the required details for publishing the filter and you're done from your side.&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%2Fdb0165kk1sd7k27yc9yt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdb0165kk1sd7k27yc9yt.png" alt="publish.PNG" width="601" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Keep a small AR Filter demo video and also a logo for your AR Filter ready before clicking the Publish button&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Congratulations, you can now create and publish your own AR Filters.
&lt;/h4&gt;

&lt;h4&gt;
  
  
  Thank you.
&lt;/h4&gt;




&lt;h2&gt;
  
  
  HackThisFall 2.0
&lt;/h2&gt;

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

&lt;p&gt;Hey folks,&lt;br&gt;
I am thrilled to inform you that I have been accepted as a “&lt;strong&gt;Hackathon Evangelist&lt;/strong&gt;” for Hack This Fall 2.0!🎉&lt;/p&gt;

&lt;p&gt;Super excited to bring a change and contribute to the hacker community in a meaningful way!🚀&lt;/p&gt;

&lt;p&gt;🔗 Do register for &lt;a href="https://hackthisfall.tech/" rel="noopener noreferrer"&gt;Hack This Fall 2.0&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Use the referral code &lt;strong&gt;HTFHE001&lt;/strong&gt; while registering.&lt;/p&gt;

&lt;p&gt;See you there !!👋🏻 &lt;/p&gt;

</description>
      <category>facebook</category>
      <category>sparkar</category>
      <category>augmentedreality</category>
      <category>hackthisfall</category>
    </item>
    <item>
      <title>Hackathon Experience</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Sun, 17 Jan 2021 04:40:27 +0000</pubDate>
      <link>https://dev.to/mrunankpawar/hackathon-experience-1l4p</link>
      <guid>https://dev.to/mrunankpawar/hackathon-experience-1l4p</guid>
      <description>&lt;p&gt;&lt;strong&gt;My MLH Hackathon Experience :&lt;/strong&gt;&lt;br&gt;
-&amp;gt; My first hackathon I attended was SharkHacks and to be honest was really very nervous, I did not submit any project for it but tried to attend Workshops &amp;amp; Mini-events and at the closing ceremony I got an idea in my mind how to submit the projects. In the upcoming weekends I started attending the Hackathons and also submitted the projects and published demo videos on YouTube. Currently as I'm writing this post I'm also participating in MLH LocalHackDay : Build which is a weeklong event and it's great fun here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why MLH?&lt;/strong&gt;&lt;br&gt;
-&amp;gt; Major League Hacking Events are mostly during the weekends. These hackathons are for everyone may it be a newbie programmer or a seasoned player. &lt;br&gt;
So if you are thinking of having something to do this weekend then why not participate in a Hackathon? And not to forget the connect and exposure we get by participating and demoing our projects is just wow.&lt;br&gt;
Because of these hackathons we are able to work and complete a project within the deadline.&lt;br&gt;
Major League Hacking Hackathons aren't just about coding but they have some awesome mini-events as well in which you can chill and have fun. &lt;br&gt;
MLH also has some awesome swags like stickers, tees, postcards,etc. for their participants and demoers. And who doesn't love to have swags!&lt;/p&gt;

</description>
      <category>majorleaguehacking</category>
    </item>
    <item>
      <title>Hacktoberfest : The best experience</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 16 Oct 2020 07:15:48 +0000</pubDate>
      <link>https://dev.to/mrunankpawar/hacktoberfest-the-best-experience-5bdf</link>
      <guid>https://dev.to/mrunankpawar/hacktoberfest-the-best-experience-5bdf</guid>
      <description>&lt;h2&gt;
  
  
  What I Learned From Hacktoberfest
&lt;/h2&gt;

&lt;p&gt;It was my first time at the #hacktoberfest and really it was an amazing experience. I'm looking up to participating in upcoming Hacktoberfest now!! Thank you Digital Ocean for such a great adventure.&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
    </item>
  </channel>
</rss>
