<?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>What Is PKCE, How It Works &amp; Flow Examples</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 22 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/what-is-pkce-how-it-works-flow-examples-3pjm</link>
      <guid>https://dev.to/descope/what-is-pkce-how-it-works-flow-examples-3pjm</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/learn/post/pkce" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You'd think the most secure &lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth&lt;/a&gt; flow wouldn't need a patch, but the standard Authorization Code flow has a blind spot. It can't guarantee that the app redeeming an authorization code is the same one that requested it. That gap opens the door to interception and Cross-Site Request Forgery (CSRF) attacks. Proof Key for Code Exchange (PKCE) closes it.&lt;/p&gt;

&lt;p&gt;In this guide, we'll explore what PKCE is and how it stops these attacks. We'll break down the standard Authorization Code flow, pinpoint where PKCE adds value, and examine why organizations are embracing it, even before it's officially mandatory in &lt;a href="https://www.descope.com/blog/post/oauth-2-0-vs-oauth-2-1" rel="noopener noreferrer"&gt;the latest OAuth standard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main points&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authorization Code flow has a verification gap. It can't confirm the app exchanging the code is the one that requested it.&lt;/li&gt;
&lt;li&gt;PKCE binds request and token exchange. A dynamic code challenge verifies the legitimacy of the client.&lt;/li&gt;
&lt;li&gt;Public clients need it, all clients benefit. PKCE protects apps with or without client secrets.&lt;/li&gt;
&lt;li&gt;PKCE is mandatory in OAuth 2.1. It's no longer optional—it's the new standard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is PKCE?
&lt;/h2&gt;

&lt;p&gt;PKCE, pronounced "pixie," is a security extension for OAuth 2.0's Authorization Code flow. While it's designed for scenarios where the client secret cannot be securely stored, all applications can benefit from PKCE. In fact, while it's already recommended in the best practices for OAuth, PKCE is a requirement for all clients using the in-development OAuth 2.1 specification.&lt;/p&gt;

&lt;p&gt;As an enhancement for standard OAuth, PKCE can benefit all types of applications for two big reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSRF attacks&lt;/strong&gt;: When a malicious site or app initiates an authorization request without the user's knowledge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization code interception/injection&lt;/strong&gt;: When an attacker intercepts an authorization code and exchanges it for &lt;a href="https://www.descope.com/learn/post/access-token" rel="noopener noreferrer"&gt;access tokens&lt;/a&gt; before the legitimate application does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The standard Authorization Code flow and where PKCE steps in
&lt;/h2&gt;

&lt;p&gt;OAuth provides several different "grant types"—standardized methods for obtaining access tokens. Grant types are associated with different scenarios, with each one offering a different balance of security and convenience. One of these is the Authorization Code grant type, widely considered to be the most versatile and secure of the bunch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization Code flow
&lt;/h3&gt;

&lt;p&gt;The "vanilla" Authorization Code flow is meant for applications that can maintain a server-side component to securely store credentials. Here's how it works:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User initiates login&lt;/strong&gt;: The user chooses to grant your application permissions via OAuth, such as by choosing "Log in with Service (e.g., Google)" in your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Code request&lt;/strong&gt;: The client requests an Authorization Code from the authorization server, including information about what the app is and what permissions it's requesting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt for consent&lt;/strong&gt;: The authorization server asks the user to authenticate and provide consent for the app to access their resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User authentication&lt;/strong&gt;: The user logs in to the authorization server and approves the requested permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization code return&lt;/strong&gt;: The authorization server redirects the user back to your application with a temporary Authorization Code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token exchange&lt;/strong&gt;: Your application sends this code to the authorization server along with your app credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access token issued&lt;/strong&gt;: The authorization server validates everything and returns ID and access tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource access&lt;/strong&gt;: Your application uses the access token to request protected resources&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this flow, the application never sees the user's credentials. Instead, the user authenticates directly with the authorization server, which then provides the app with a temporary authorization code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The security gap
&lt;/h3&gt;

&lt;p&gt;The standard Authorization Code flow has a fundamental flaw: there's no way to verify that the client exchanging an authorization code for tokens is the same client that initiated the request. This raises several concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An attacker who intercepts the authorization code can use it to obtain access tokens because there's no verification that ties the code to the original requesting application.&lt;/li&gt;
&lt;li&gt;Even if the application uses a client secret (essentially a password shared between the app and authorization server), this only proves the client's identity, not that this specific client originally requested this specific authorization code.&lt;/li&gt;
&lt;li&gt;Since there's nothing binding the initial request to the token exchange, the flow is also vulnerable to CSRF attacks, in which a user could be tricked into initiating an unintended authorization flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This security gap affects all types of applications, though it's especially problematic for clients that can't securely store client secrets. This is where PKCE comes in, binding the initial authorization request and the token exchange.&lt;/p&gt;

&lt;h2&gt;
  
  
  PKCE flow
&lt;/h2&gt;

&lt;p&gt;Without PKCE, OAuth authorization code flows don't have a way to verify which specific client sent this specific request. To understand how PKCE eliminates this vulnerability, we merely need to look at its name: Proof Key for Code Exchange, meaning you need proof you originated the authorization request to exchange the code for tokens.&lt;/p&gt;

&lt;p&gt;To achieve this, PKCE has the requesting application create a new type of secret, a "code verifier." This is used to create a "code challenge," which the authorization server uses to confirm which app sent the request. Here's how it works:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User initiates login&lt;/strong&gt;: Just like in a standard Authorization Code flow, the user initiates the process by selecting the prompt associated with granting your application permissions via OAuth, like "Log in with Service (e.g., Google)."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code verifier creation&lt;/strong&gt;: Before starting the flow, your application generates a random secret. This is not the same as a client secret—it's a special component called a "code verifier." Client secrets can still be used alongside it. Your application also creates a "code challenge" by transforming the verifier, usually by hashing it (a one-way process that can't be reversed or decoded).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization code request&lt;/strong&gt;: The application requests an authorization code from the server and includes the code challenge (along with the hashing method, like SHA-256).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt for user consent&lt;/strong&gt;: The authorization server prompts the user to authenticate and provide consent for the requested permissions (same as standard flow).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User authentication&lt;/strong&gt;: The user logs in and approves the permissions (same as standard flow).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization code return&lt;/strong&gt;: The authorization server redirects back to your application with the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token exchange (with verification)&lt;/strong&gt;: Your application sends the code to the authorization server along with the original code verifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server verification&lt;/strong&gt;: The authorization server compares the previously shared code challenge to the recently shared original code verifier before issuing any tokens. For example, if the method used was hashing with SHA-256, the server will also hash the code verifier and ensure it matches the code challenge; the two strings should be the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access token issued&lt;/strong&gt;: If the code verifier matches up with the code challenge, the authorization server returns ID and access tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource access&lt;/strong&gt;: Your application can now use these tokens to request the necessary resources (as in the standard flow).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using &lt;a href="https://docs.descope.com/getting-started/oidc-endpoints#using-pkce-in-your-endpoints" rel="noopener noreferrer"&gt;PKCE in your endpoints&lt;/a&gt; enables the authorization server to verify that the client requesting tokens is the same one that made the request. Even if an attacker intercepts the authorization code, they can't exchange it for tokens without the original code verifier. Only the legitimate application has this in its untransformed (e.g., unhashed) state.&lt;/p&gt;

&lt;h2&gt;
  
  
  PKCE in public vs. confidential clients
&lt;/h2&gt;

&lt;p&gt;In OAuth terminology, clients are either "public" or "confidential" based on their ability to securely store credentials. Public clients are apps that cannot safely store a client secret because their code is fully exposed to the user or can be extracted easily. Public apps include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-page apps (SPAs) running entirely in the browser&lt;/li&gt;
&lt;li&gt;Native mobile apps&lt;/li&gt;
&lt;li&gt;Certain types of desktop apps, like those that don't use TPMs (Trusted Platform Modules) to securely store credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Public clients need PKCE because they can't rely on client secrets for security. Confidential clients, on the other hand, can securely store credentials because they either run in controlled server environments or the code and secrets are otherwise inaccessible to end users. So, why would you want to use PKCE for a confidential client?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protection against authorization code injection attacks and CSRF&lt;/strong&gt;: As previously mentioned, CSRF and authorization code injection attacks are potential threats to all types of applications. &lt;a href="https://datatracker.ietf.org/doc/rfc9700/" rel="noopener noreferrer"&gt;OAuth 2.0 Best Practices&lt;/a&gt; recommend PKCE for confidential clients because it "provides strong protection against misuse and injection of authorization codes" and "prevents CSRF even in the presence of strong attackers."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjunctive security that complements (not replaces) client secrets&lt;/strong&gt;: The &lt;a href="https://oauth.net/2/pkce/" rel="noopener noreferrer"&gt;OAuth PKCE spec&lt;/a&gt; makes no bones about it—PKCE is not a replacement for client secrets or authentication. While it was originally designed to protect public clients, PKCE proved useful as an add-on to existing mechanisms by preventing CSRF attacks, prompting the question: "Why not add PKCE if you can?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protection against implementation vulnerabilities&lt;/strong&gt;: In the absence of PKCE, it's possible for a client implementation to fail at properly verifying state parameters (an OAuth mechanism used to deter CSRF attacks). Because PKCE is verified by the authorization server, it provides protection even when client-side verification is flawed or incomplete.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PKCE benefits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PKCE Benefit&lt;/th&gt;
&lt;th&gt;Public Clients&lt;/th&gt;
&lt;th&gt;Confidential Clients&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eliminates need for client secret&lt;/td&gt;
&lt;td&gt;✅ Required — can't store secrets securely&lt;/td&gt;
&lt;td&gt;❌ Still uses client secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prevents authorization code interception&lt;/td&gt;
&lt;td&gt;✅ Essential to prevent token theft&lt;/td&gt;
&lt;td&gt;✅ Adds an extra layer of protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigates CSRF attacks&lt;/td&gt;
&lt;td&gt;✅ Protects flows in exposed environments&lt;/td&gt;
&lt;td&gt;✅ Strengthens existing CSRF defenses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommended in OAuth 2.0 best practices&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required in OAuth 2.1&lt;/td&gt;
&lt;td&gt;✅ Mandatory&lt;/td&gt;
&lt;td&gt;✅ Mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protects against poor client implementations&lt;/td&gt;
&lt;td&gt;✅ Helps when state checks are missing or weak&lt;/td&gt;
&lt;td&gt;✅ Useful fallback in misconfigured setups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  PKCE use cases and examples
&lt;/h2&gt;

&lt;p&gt;Although PKCE was originally developed to secure authorization code flows on public clients, the reasons for adoption can vary across use cases and application types. Even so, there are virtually no scenarios in which an additional layer of seamless security is unwelcome.&lt;/p&gt;

&lt;p&gt;Ideal PKCE use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native mobile applications&lt;/strong&gt;: In the original PKCE scenario, native mobile apps are public clients that can't securely store secrets. Without PKCE, these applications couldn't use the authorization code flow without exposing credentials to anyone with the knowledge and tools to find them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-page applications (SPAs)&lt;/strong&gt;: SPAs benefit from PKCE in the same way native mobile apps do: all their code runs in the browser, meaning there's no server-side storage for client secrets. SPAs rely on PKCE to use the authorization code flow safely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop applications&lt;/strong&gt;: Some desktop applications can leverage TPMs and other mechanisms to secure client secrets even if the application runs entirely on a user device, but many don't have this design. Like other public apps, these desktop clients need PKCE to use the authorization code flow securely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth 2.0 best practices and 2.1 compliance&lt;/strong&gt;: OAuth 2.0 best practices recommend PKCE be used for every client, not just public ones. OAuth 2.1, despite being in its draft stage, has already been adopted by many organizations. It makes PKCE mandatory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PKCE and MCP adoption
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.descope.com/learn/post/mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; is a standardized way for Large Language Models (LLMs) and AI agents to connect with external tools, APIs, and data sources. The &lt;a href="https://www.descope.com/blog/post/mcp-auth-spec" rel="noopener noreferrer"&gt;authorization specification for MCP&lt;/a&gt; formally adopted OAuth 2.1, requiring developers leveraging the protocol to "implement OAuth 2.1 with appropriate security measures for both confidential and public clients." That means using PKCE.&lt;/p&gt;

&lt;p&gt;Because MCP serves as the "universal remote" that serves up &lt;a href="https://www.descope.com/blog/post/outbound-apps" rel="noopener noreferrer"&gt;AI agents to connect with external tools&lt;/a&gt;, OAuth was the logical choice for &lt;a href="https://www.descope.com/learn/post/authorization" rel="noopener noreferrer"&gt;authorization&lt;/a&gt;. OAuth already has a strong, proven foundation spanning over a decade, making developing a new standard just for AI use cases a moot point. OAuth 2.1 provides the latest and most secure set of requirements, and PKCE adds a crucial layer of protection to a budding ecosystem still finding its footing.&lt;/p&gt;

&lt;p&gt;MCP's embrace of OAuth 2.1 (and thus PKCE) is particularly significant because of the AI protocol's widespread acceptance and sudden rise to prominence. It's already seen adoption by OpenAI's Agents SDK and industry leaders like Google, with the tech giant releasing their own "complementary" &lt;a href="https://www.descope.com/learn/post/a2a" rel="noopener noreferrer"&gt;Agent2Agent protocol&lt;/a&gt; that can work hand-in-hand with MCP. Looking to the future, as MCP becomes more prevalent, so does PKCE.&lt;/p&gt;

&lt;h2&gt;
  
  
  No-hassle OAuth Authorization Code flows with PKCE
&lt;/h2&gt;

&lt;p&gt;PKCE may sound like a complicated identity concept at first glance, but it's easily integrated with the right tools. Descope is a comprehensive external identity and access management solution that makes complex auth challenges drag &amp;amp; drop simple.&lt;/p&gt;

&lt;p&gt;Descope can be configured as an &lt;a href="https://docs.descope.com/getting-started/oidc-endpoints#guide-to-using-oidc-endpoints" rel="noopener noreferrer"&gt;OIDC Provider&lt;/a&gt; to easily add PKCE-based flows to your app. Our &lt;a href="https://www.descope.com/blog/post/mcp-auth-sdk" rel="noopener noreferrer"&gt;MCP Auth SDKs&lt;/a&gt; help make remote MCP servers OAuth-compliant by implementing OAuth, PKCE, dynamic client registration and more in just three lines of code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; for a Free Forever account with Descope to see how PKCE-enhanced OAuth flows can enhance your users' auth journey. Got questions about Descope? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book time&lt;/a&gt; with our auth experts.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Gemini vs. ChatGPT for Coding: A Developer's Guide</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 20 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/gemini-vs-chatgpt-for-coding-a-developers-guide-4k2f</link>
      <guid>https://dev.to/descope/gemini-vs-chatgpt-for-coding-a-developers-guide-4k2f</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/gemini-vs-chatgpt" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Artificial intelligence (AI) is no longer just a buzzword floating around tech conferences or your Twitter feed. It's sitting right next to you, reviewing your code, naming your functions, and occasionally hallucinating—with surprising confidence. For modern full-stack developers, AI assistants have become essential tools, speeding up boilerplate tasks, helping debug errors, and even suggesting entire architectures when you're just trying to rename a variable.&lt;/p&gt;

&lt;p&gt;This three-part series takes a closer look at today's most talked-about AI copilots: ChatGPT, Claude, Gemini, and GitHub Copilot. &lt;a href="https://www.descope.com/blog/post/claude-vs-chatgpt" rel="noopener noreferrer"&gt;Part one&lt;/a&gt; pitted Claude against ChatGPT. This second installment compares Gemini vs. ChatGPT, focusing on how these two chatbots perform in real-world coding workflows. You'll learn where each shines, how they integrate into your workflows, and whether Google's Gemini is really &lt;a href="https://www.inc.com/ben-sherry/google-says-its-new-gemini-ai-is-smarter-and-cheaper-than-openais-best/91172562" rel="noopener noreferrer"&gt;the serious contender it claims to be&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemini vs ChatGPT for coding generation quality&lt;/li&gt;
&lt;li&gt;Gemini vs ChatGPT in troubleshooting capabilities&lt;/li&gt;
&lt;li&gt;Gemini vs ChatGPT in integration possibilities&lt;/li&gt;
&lt;li&gt;Gemini vs ChatGPT in business applications&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gemini vs. ChatGPT at a glance
&lt;/h2&gt;

&lt;p&gt;Before we get into a hands-on comparison of coding tasks between Gemini and ChatGPT, it's helpful to establish some baseline comparisons. Here's what sets these two programs apart at a high level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;th&gt;ChatGPT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning, problem-solving, and analytical skills&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Pro excels in structured reasoning tasks and breaking down complex problems into manageable steps, making it particularly effective for in-depth analyses and codebase refactoring.&lt;/td&gt;
&lt;td&gt;ChatGPT (GPT-4.1) demonstrates superior performance in coding and instruction-following tasks, with significant improvements over previous models that make it highly efficient for software development and debugging.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document analysis and summarization&lt;/td&gt;
&lt;td&gt;Gemini may have a slight edge in processing scale,&lt;/td&gt;
&lt;td&gt;ChatGPT excels in summarization quality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emotional intelligence and conversation style&lt;/td&gt;
&lt;td&gt;Gemini is known to maintain a professional and factual tone, suitable for tasks requiring precision and clarity.&lt;/td&gt;
&lt;td&gt;ChatGPT offers a more conversational and adaptive interaction style, capable of adjusting its responses based on user tone and making it ideal for collaborative and creative tasks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time data and web access&lt;/td&gt;
&lt;td&gt;Gemini integrates with Google Search to provide real-time information, enhancing its responses with up-to-date data.&lt;/td&gt;
&lt;td&gt;ChatGPT features real-time web browsing capabilities that allow it to access and cite current information from the internet, thereby improving the relevance and accuracy of its responses.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost, access, and plans&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Pro is available through the Gemini Advanced plan for $19.99/month, which includes Google One storage perks and more. The model is also available for preview in Gemini's free plans, although it is greatly restricted.&lt;/td&gt;
&lt;td&gt;ChatGPT offers the smaller GPT-4.1-mini model on the free plan with generous usage restrictions, while the full GPT-4.1 model is accessible on the Plus plan at $20/month and above, providing access to advanced tools like memory, vision, file handling, and plugins.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the whole, some of the most impactful differences between the models are in the logic factors. Gemini is ideal for comprehensive, structured problem-solving, while ChatGPT is better suited for rapid, code-centric tasks. The two AI coding tools' biggest point of convergence is in document analysis and summary. With a context window of 1 million for the best models available from both chatbots, both models are capable of handling large documents.&lt;/p&gt;

&lt;p&gt;Now, let's take a deeper dive into what exactly Gemini and ChatGPT are and how each AI coding tool performs on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Gemini's utility for coding
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://gemini.google.com/?hl=en-IN" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt; is Google's next-generation family of large language models (LLMs), developed by &lt;a href="https://deepmind.google/" rel="noopener noreferrer"&gt;Google DeepMind&lt;/a&gt;. Introduced in December 2023, Gemini is a multimodal AI system capable of understanding and generating text, images, audio, and video. The Gemini models are optimized for various use cases and come in different sizes: Pro, Flash, and Flash-Lite.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwq1kfgaefw67mdx128ks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwq1kfgaefw67mdx128ks.png" alt="Fig: Comparisons between Gemini 2.5 and other models (Image credit: Google)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The following are its key features and capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal processing&lt;/strong&gt; – Gemini 2.5 Pro natively supports text, code, images, audio, and video inputs, enabling seamless integration across various data types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended context window&lt;/strong&gt; – It boasts a context window of up to 1 million tokens, allowing for the processing of extensive documents and codebases. This capability is great for improving productivity for tasks that involve multiple, possibly humongous files. More on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced reasoning&lt;/strong&gt; – Similarly to Claude 3.7, Gemini 2.5 Pro employs a "thinking" approach to break down complex problems into manageable steps, enhancing its problem-solving capabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced coding performance&lt;/strong&gt; – Gemini 2.5 Pro excels in code generation and transformation, scoring 63.8 percent on the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/google-deepmind/gemini-model-thinking-updates-march-2025/" rel="noopener noreferrer"&gt;SWE-Bench Verified benchmark&lt;/a&gt;, indicating its proficiency in software engineering tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with the Google ecosystem&lt;/strong&gt; – It integrates seamlessly with Google Workspace and Cloud services, which eases adoption, especially in Enterprise setups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemini 2.5 Pro utilizes a transformer-based architecture optimized for multimodal inputs. The model's extended context window supports in-depth analysis of large data sets, making it suitable for tasks like summarizing lengthy documents or analyzing extensive code repositories.&lt;/p&gt;

&lt;p&gt;Most commercially available models (i.e., Claude's and OpenAI's non-4.1 models) reportedly show significant drops in response quality after using 32,000 tokens, which equates to at least approximately 16 percent of their total context window (Claude offers 200,000, and OpenAI 128,000). If you were to use that ratio for a 1 million context window, you still get to use over 160,000 tokens in a single high-quality conversation, which is much more than the competitors. However, it's important to note that this is only available in the Gemini Advanced plan, not the regular free Gemini plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding ChatGPT's utility for coding
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://chatgpt.com/" rel="noopener noreferrer"&gt;ChatGPT&lt;/a&gt; is OpenAI's flagship conversational AI, widely used for coding, research, and creative tasks. It's popular across domains like software development, academic research, creative writing, and data processing. Since its debut in November 2022, it has become nearly synonymous with consumer-facing AI tools. ChatGPT supports a range of models from &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;OpenAI's model family&lt;/a&gt;, with capabilities spanning natural language, image, and audio inputs.&lt;/p&gt;

&lt;p&gt;As of May 2025, the default model in ChatGPT is GPT-4o (short for "omni"), a multimodal model designed for fast, context-rich, and interactive use across text, vision, and speech. However, ChatGPT has very recently (May 14, 2025) started offering its GPT-4.1 and GPT-4.1-mini models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flhxktt0kuae4tizqlpvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flhxktt0kuae4tizqlpvi.png" alt="Fig: A comparison of the GPT-4.1 family's intelligence by latency (Image credit: OpenAI)" width="644" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GPT-4.1 brings significant enhancements over its predecessors. It's designed to excel in coding tasks, instruction following, and long-context comprehension. It supports a context window of up to 1 million tokens, enabling the processing of extensive documents and codebases effectively. It is available to subscribers on ChatGPT Plus, Pro, and Team plans, with the GPT-4.1-mini available to free users.&lt;/p&gt;

&lt;p&gt;Its key features are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced coding capabilities&lt;/strong&gt; – GPT-4.1 exhibits &lt;a href="https://openai.com/index/gpt-4-1/" rel="noopener noreferrer"&gt;superior performance&lt;/a&gt; in coding tasks, surpassing previous models like GPT-4o and GPT-4.5 in benchmarks such as SWE-Bench Verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved instruction following&lt;/strong&gt; – The model exhibits better adherence to complex instructions, making it more reliable for tasks requiring multistep reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expanded context window&lt;/strong&gt; – With support for up to 1 million tokens, GPT-4.1 can handle large-scale documents and data sets without the need for chunking or summarization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal capabilities&lt;/strong&gt; – GPT-4.1 maintains multimodal functionalities that allow it to process and generate text and image inputs, enhancing its versatility across various applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The newly launched GPT-4.1 supports a large context window of up to 1 million tokens (through the OpenAI APIs), bringing it on par with Gemini's offerings. However, the current ChatGPT pricing page reflects that even ChatGPT Plus users are limited to a maximum context window of 32,000 tokens to any OpenAI model, which is quite limited, to say the least.&lt;/p&gt;

&lt;p&gt;ChatGPT is built on OpenAI's GPT-4.5 architecture, a transformer-based model fine-tuned through a combination of unsupervised learning, human-in-the-loop supervision, and reinforcement learning with human feedback (RLHF). This setup enhances its pattern recognition and creative generation abilities. However, prompt quality still plays a major role in getting effective results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini vs. ChatGPT: Code generation quality
&lt;/h2&gt;

&lt;p&gt;Similarly to the comparison of Claude and ChatGPT in part one, we'll use two coding prompts—one for a frontend component and another for a backend script—to evaluate the coding abilities of the two. The prompts will contain requirements but will also be purposefully vague in some aspects to see how (or if) the models fill in the gaps on their own.&lt;/p&gt;

&lt;p&gt;From the frontend coding tests, both tools performed well. ChatGPT seems to give ready-to-use, up-to-date code snippets, while Gemini is extremely detailed in its code explanations.&lt;/p&gt;

&lt;p&gt;The backend coding tests were more revealing in terms of differences. ChatGPT is great for quick backend tasks like adding an endpoint to an internal app. However, no real security measures were implemented in its code snippet, apart from adding a few headers. Conversely, Gemini has gone all out to make sure it satisfies each requirement on the list. It's great for vibe coding but not very accessible for devs looking for a quick starting point for their app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend code generation with both AI coding tools
&lt;/h3&gt;

&lt;p&gt;For the first test pitting Gemini against ChatGPT, let's start with the fundamental frontend coding task of creating a React component. Here's the prompt we'll use for this test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a Next.js component that displays a list of products fetched from an API endpoint. Each product should show its name, price, and availability status. Format the price as currency (e.g., $12.99), and display a loading indicator while the data is being retrieved. Ensure the component handles errors gracefully by showing an error message if the API request fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Importantly, this prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specifies a framework (Next.js) as opposed to a plain React component;&lt;/li&gt;
&lt;li&gt;Requires API data fetching and asynchronous handling;&lt;/li&gt;
&lt;li&gt;Instructs on data formatting (currency);&lt;/li&gt;
&lt;li&gt;Includes UI states (loading and error handling); and&lt;/li&gt;
&lt;li&gt;Asks for multiple data fields per item (name, price, and availability).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These factors will inform what we look for as we evaluate each tool's outputs.&lt;/p&gt;

&lt;h4&gt;
  
  
  ChatGPT's response to our frontend coding test
&lt;/h4&gt;

&lt;p&gt;Let's take a look at how ChatGPT's free version responds to this prompt:&lt;/p&gt;

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

&lt;p&gt;ChatGPT first summarizes how it will address the prompt and then generates a code snippet for the Next.js component. Then, it includes a sample backend API response format to make it easy for you to understand the backend data schema that it uses. Finally, it provides a short snippet demonstrating how to use the Next.js component on a page.&lt;/p&gt;

&lt;p&gt;Let's analyze the code returned by the prompt.&lt;/p&gt;

&lt;p&gt;The first thing that stands out: you cannot plug and play this component in your Next.js app because it uses an imaginary backend URL (&lt;code&gt;/api/fetch&lt;/code&gt;). If you already have a backend ready to use at this point, you can replace the value of &lt;code&gt;APP_URI&lt;/code&gt; with the URL to your backend and then test this code. In other cases, you would need to redo the &lt;code&gt;fetchProducts&lt;/code&gt; function to use a set of dummy values as posts while you're developing the component. Because of this, it wouldn't be fair to call it an "easy-to-use" placeholder for the external data source.&lt;/p&gt;

&lt;p&gt;Next, all the mentioned properties are included, and the price has been formatted as requested in the prompt using the JS &lt;code&gt;Intl&lt;/code&gt; object, which is a good practice.&lt;/p&gt;

&lt;p&gt;Finally, the loading state has been handled appropriately as well.&lt;/p&gt;

&lt;p&gt;You'll also find an example API object that the component expects, along with an example of how to import and use the component. These are nice to have, and you can easily construct an API in Next.js using the example object to try out the component.&lt;/p&gt;

&lt;h4&gt;
  
  
  Gemini's response to our frontend coding test
&lt;/h4&gt;

&lt;p&gt;When tasked with the same prompt, here's what Gemini responds 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%2F1jdxyihwislig910dy4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1jdxyihwislig910dy4d.png" alt="Fig: Gemini's frontend response" width="800" height="1691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemini leads with the code for the Next.js component, followed by a detailed explanation of how it works. Towards the end, it provides the code for an API endpoint you can temporarily create if you want to test out your component with an actual backend API.&lt;/p&gt;

&lt;p&gt;Similarly to the ChatGPT response, you cannot plug and play this component in your Next.js app. It uses the imaginary backend URL (&lt;code&gt;/api/fetch&lt;/code&gt;) as well. However, it realizes that Next.js supports creating APIs easily and hence also provides an example API code. But if you look at the file location mentioned in the prompt for creating the API route, as well as the function declaration style &lt;code&gt;export default function handler(req, res) {&lt;/code&gt;, you'll realize that these are meant for an older version of Next.js. This hints that Gemini might be working with outdated Next.js training data.&lt;/p&gt;

&lt;p&gt;The JSX structure of the component looks pretty much the same as ChatGPT's version. However, another important detail it missed is the &lt;code&gt;"use client"&lt;/code&gt; declaration at the beginning of the file. It uses hooks in the component, which require the component to be a client-side-only component for them to work in Next.js 13 and above. Gemini missed this detail, most probably because it meant to write the code for a version of Next.js older than 13. The code explanation that it provides is quite detailed, though.&lt;/p&gt;

&lt;p&gt;The use of Tailwind classes is a major difference from the ChatGPT response. It's a good practice since the default &lt;code&gt;create-next-app&lt;/code&gt; CLI currently gives you the option to install Tailwind when creating a new project. This means it is going to come in handy for a large number of Next.js developers, regardless of whether they are experienced devs or "vibe coders."&lt;/p&gt;

&lt;p&gt;For someone coding an app from scratch, this is an excellent starting point. For someone who already uses a different styling system and maybe has more style guidelines to adhere to (such as no icons or a different color scheme), a few more prompts should do the trick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend code generation with both AI coding tools
&lt;/h3&gt;

&lt;p&gt;Here's the prompt for the backend task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write an Express.js API endpoint in Node.js that accepts a POST request with a JSON payload containing a product's name, price, and category. The endpoint should validate the input and store the product details in a product database. Also, it should handle errors reasonably. Make sure your implementation considers common security best practices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As we evaluate each AI coding tool's response, we'll be looking for how well it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles the POST request&lt;/li&gt;
&lt;li&gt;Implements validations&lt;/li&gt;
&lt;li&gt;Stores the details in a dummy database&lt;/li&gt;
&lt;li&gt;Responds with descriptive error details&lt;/li&gt;
&lt;li&gt;Uses Helmet to add secure HTTP headers to the response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's see how each model performs.&lt;/p&gt;

&lt;h4&gt;
  
  
  ChatGPT's response to our backend coding test
&lt;/h4&gt;

&lt;p&gt;Here's what ChatGPT's free version responds 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%2Fe1c0cknpe380tfn39qkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1c0cknpe380tfn39qkz.png" alt="Fig: ChatGPT's backend response" width="800" height="1072"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT starts with a quick summary of how it will address the prompt and then provides the code for the endpoint along with an npm command to install the required dependencies. Towards the end, it summarizes the security considerations it took into account when generating the code.&lt;/p&gt;

&lt;p&gt;The code snippet works, and the response is quite concise itself, with the code snippet meeting all of the requirements laid out in the prompt. As an added benefit, it uses Joi for validations, which is better than manually writing them in cases like these.&lt;/p&gt;

&lt;h4&gt;
  
  
  Gemini's response to our backend coding test
&lt;/h4&gt;

&lt;p&gt;Here's what Gemini's free version responds 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%2Fimages.ctfassets.net%2Fxqb1f63q68s1%2F6zw7Ns0nen4WyRGlScXwnH%2Ffb0221df50a9550470609060cf7ac503%2Fscreencapture-gemini-google-app-bb96fd70327ac549-2025-05-22-21-25-00__1_.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2Fxqb1f63q68s1%2F6zw7Ns0nen4WyRGlScXwnH%2Ffb0221df50a9550470609060cf7ac503%2Fscreencapture-gemini-google-app-bb96fd70327ac549-2025-05-22-21-25-00__1_.png" alt="Fig: Gemini's backend response" width="800" height="3487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemini lists out the components it chooses to build the solution with, then spends some time explaining how to create a new Node.js project and set it up. It also includes a database setup file and detailed instructions on creating validation middleware and the API endpoint. Towards the end, you will also see plenty of instructions on how to test out the API once you have set it up.&lt;/p&gt;

&lt;p&gt;As you can see, it's an extremely lengthy response compared to ChatGPT's. It uses Helmet and Joi as well. However, it goes a step further and gives you the file structure of the project, along with a detailed dummy database. The validation setup is quite detailed, providing a surplus of error messages and allowing you to easily remove any that you do not want in your code.&lt;/p&gt;

&lt;p&gt;You'll also find detailed instructions on how to set up the code and run the app, along with plenty of explanations on how the code works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini vs. ChatGPT: Troubleshooting capabilities
&lt;/h2&gt;

&lt;p&gt;Debugging and writing test cases are inherently context-heavy tasks, which make them difficult to evaluate without the specific structure and quirks of a real-world project. This section is based on some hands-on experience as well as widespread community feedback. For instance, many developers on platforms like Reddit have echoed similar sentiments when &lt;a href="https://www.reddit.com/r/Bard/comments/1kp514r/gemini_pro_vs_chatgpt_pro/" rel="noopener noreferrer"&gt;comparing Gemini and ChatGPT&lt;/a&gt; for everyday software maintenance tasks.&lt;/p&gt;

&lt;p&gt;On the whole, debugging works differently with each AI coding tool. Gemini is methodical and excellent at sticking to best practices, whereas ChatGPT can be more flexible and adaptable. In a similar vein, ChatGPT has an edge in test generation in complex, dynamic environments.&lt;/p&gt;

&lt;p&gt;Both models are generally on par in terms of contextual awareness, with Gemini offering more stable formatting and longer-term memory in extended sessions, while ChatGPT offers more fluid, natural interaction, but sometimes at the cost of precision in code formatting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging with both AI coding tools
&lt;/h3&gt;

&lt;p&gt;Gemini performs best when debugging is framed as a step-by-step reasoning task. It excels at identifying logical flaws, uninitialized variables, and inconsistent states when given well-structured, isolated code. It is also great at sticking to best practices, which can sometimes help avoid debugging detours on the whole. However, it tends to struggle with ambiguous or incomplete context, especially in cases where understanding a broader system or multiple file relationships is critical. Its debugging output can feel methodical but sometimes lacks flexibility when the problem isn't clearly defined.&lt;/p&gt;

&lt;p&gt;ChatGPT handles debugging in a more conversational and exploratory style. It's capable of asking clarifying questions, simulating test scenarios, and even suggesting minimal code diffs. In real-world usage, it tends to be more effective at identifying root causes across loosely scoped snippets and explaining the implications clearly. Its strength lies in adaptability; even when prompts are vague, ChatGPT often gets closer to what the developer intended compared to Gemini. ChatGPT is generally preferred for live debugging scenarios due to its responsiveness, conversational depth, and ability to reason across incomplete or messy inputs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Comparing Gemini and ChatGPT responses
&lt;/h4&gt;

&lt;p&gt;The tendencies described above illustrate what developers can expect when debugging with Gemini or ChatGPT, respectively. However, these tendencies do not always hold true.&lt;/p&gt;

&lt;p&gt;For example, let's take the same problem from the first part of this series, where we saw ChatGPT struggling to provide the right, updated solution:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I am trying to add a static form as a standalone component to an Angular 19.2 app, but receiving the following error: No provider for HttpClient How do I fix it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what Gemini responds 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%2Fmwbyxwqjk0dewwoptp2o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwbyxwqjk0dewwoptp2o.png" alt="Fig: Gemini's response to Angular error" width="800" height="1001"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemini describes the potential cause of the problem and then recommends solutions depending on the Angular version your project is using. Then, to help the users understand the issue better, it also goes on to explain why these solutions fix the problem.&lt;/p&gt;

&lt;p&gt;Unlike ChatGPT (and Claude in part one of the series), Gemini is able to pinpoint the issue and the correct, up-to-date solution for it. Surprisingly, if you ask its cutoff date, it turns out to be 2023, when Angular 19.2 was nowhere near release. And yet, it is able to put together the right solution because, in reality, the right solution has been present since Angular 17+, and the old &lt;code&gt;HttpClientModule&lt;/code&gt; solution was deprecated only in Angular 18 in 2024.&lt;/p&gt;

&lt;p&gt;This means that Gemini demonstrated a better understanding of best practices by sticking to an up-to-date, recommended solution rather than blurting out the solution that it has seen more times in its training data.&lt;/p&gt;

&lt;p&gt;For reference, here's what ChatGPT responded 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%2F690b1vr1ufywvx6zurz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F690b1vr1ufywvx6zurz3.png" alt="Fig: ChatGPT's response to Angular error" width="800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT recommended the deprecated solution of using &lt;code&gt;HttpClientModule&lt;/code&gt; to solve the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test generation across both AI coding tools
&lt;/h3&gt;

&lt;p&gt;Gemini 2.5 Pro is fully capable of generating unit tests for straightforward components. ChatGPT with GPT-4.1, on the other hand, delivers strong results in small test generation tasks and scales better in more complex use cases. When working with projects that span multiple files or languages, such as a multimodule Android app in Kotlin, ChatGPT can infer dependencies, generate mocks, and select appropriate test frameworks more reliably.&lt;/p&gt;

&lt;p&gt;While both tools handle simple unit test generation well, ChatGPT pulls ahead in complex environments where framework compatibility and test coverage matter most.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contextual awareness of both AI coding tools
&lt;/h3&gt;

&lt;p&gt;Both Gemini 2.5 Pro and ChatGPT (GPT-4.1) demonstrate strong contextual awareness when it comes to understanding and editing existing codebases. As seen in previous examples throughout this article, both tools can infer variable relationships, follow control flow, and suggest changes that align with the logic and style of the original code.&lt;/p&gt;

&lt;p&gt;That said, ChatGPT occasionally stumbles with formatting, especially when generating longer code snippets, sometimes introducing typos or indentation issues that wouldn't compile without tweaking. Gemini, by contrast, tends to produce more cleanly formatted suggestions out of the box, though it may require slightly more guided prompting in loosely scoped edits.&lt;/p&gt;

&lt;p&gt;In back-and-forth conversations, both tools are capable of maintaining context and building upon prior messages. Of course, it is imperative that the prompts are clear and scoped to a consistent thread. However, ChatGPT (GPT-4.1) can occasionally "forget" or slightly drift from the previous context in complex multiturn exchanges, especially if they involve formatting-sensitive outputs like HTML, YAML, or code blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini vs. ChatGPT: Integration possibilities
&lt;/h2&gt;

&lt;p&gt;The best AI tool is the one that can most easily integrate into your existing workflows. Here's a comparison of Gemini and ChatGPT (and their models) in terms of their integration possibilities.&lt;/p&gt;

&lt;p&gt;With respect to integrated development environment (IDE) integrations, both AI coding tools support key capabilities like Visual Studio Code. They also offer a clean and polished feel to all integrations, with the caveat that response quality can vary with prompt complexity.&lt;/p&gt;

&lt;p&gt;In terms of API and external connections, each has clear utility. If you're building inside the Google Cloud ecosystem or integrating with Google Workspace tools, Gemini 2.5 Pro is the natural fit. However, for broader adoption, GPT-4.1 provides a more flexible, developer-first API experience with faster onboarding, easier experimentation, and support for a wider range of workflows out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  IDE integrations in both AI coding tools
&lt;/h3&gt;

&lt;p&gt;Gemini offers official support for IDEs, like Visual Studio Code and JetBrains IDEs (including IntelliJ), through its &lt;a href="https://docs.cloud.google.com/gemini/docs/codeassist/write-code-gemini" rel="noopener noreferrer"&gt;Gemini Code Assist&lt;/a&gt; extension. These plugins bring AI-powered autocomplete, code explanations, and debugging tips directly into the development environment. The experience is tightly integrated and designed to reduce context switching, letting developers chat with Gemini, request code completions, or ask for improvements without leaving their editor. However, while Gemini Code Assist is generally reliable, some early reviews have pointed out inconsistencies in code suggestion quality, especially for less common frameworks or languages.&lt;/p&gt;

&lt;p&gt;ChatGPT also supports Visual Studio Code through community-developed extensions. These provide live access to GPT-4o for code generation, explanation, and problem-solving tasks. The experience is polished, with ChatGPT typically accessible via a sidebar or command palette. Reliability is strong overall, with consistent results and minimal lag, though (as with Gemini) the quality of responses can vary based on the clarity of the prompt and the complexity of the task.&lt;/p&gt;

&lt;h3&gt;
  
  
  External service connections in both AI coding tools
&lt;/h3&gt;

&lt;p&gt;Gemini 2.5 Pro is accessible via the &lt;a href="https://ai.google.dev/" rel="noopener noreferrer"&gt;Gemini API&lt;/a&gt;, hosted on Google Cloud, and supports a wide set of features for developers integrating AI into production environments. Authentication is handled through standard Google Cloud IAM and API key mechanisms, with granular project-level access controls. Gemini's function calling support allows developers to define callable backend functions for setting up AI-powered workflows across applications. Its documentation is polished and enterprise-friendly, though some users may find the setup process more Google Cloud–native than developer-friendly, especially those not already embedded in the ecosystem.&lt;/p&gt;

&lt;p&gt;ChatGPT with GPT-4.1, on the other hand, is available via the OpenAI API and is widely regarded for its ease of integration. Authentication uses simple API keys, and the platform supports function calling, tool use, and Retrieval Augmented Generation (RAG) out of the box. GPT-4.1 brings improvements in multistep API workflows and is compatible with custom GPTs, allowing developers to inject external APIs and tools into personalized assistants. OpenAI's developer documentation is extensive, example-rich, and easy to onboard with, even for teams without deep infrastructure experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini vs. ChatGPT: Business applications
&lt;/h2&gt;

&lt;p&gt;Gemini, integrated within Google Workspace and Google Cloud, is a useful and easily accessible tool for enterprises looking to boost efficiency and collaboration. For instance, &lt;a href="https://workspace.google.com/blog/customer-stories/finquery-innovates-gemini-google-workspace" rel="noopener noreferrer"&gt;FinQuery&lt;/a&gt;, a fintech company, uses Gemini to expedite brainstorming sessions, draft emails more swiftly, manage intricate project plans, and assist engineering teams in debugging code and evaluating new monitoring tools.&lt;/p&gt;

&lt;p&gt;Another example is Lawme, a legal tech platform that used Gemini's long-context capabilities to simplify contract drafting and client onboarding, reporting improved efficiency and better context retention across legal workflows.&lt;/p&gt;

&lt;p&gt;ChatGPT is widely adopted across various sectors for its adaptability and powerful language processing capabilities. At &lt;a href="https://www.wsj.com/articles/six-months-thousands-of-gpts-and-some-big-unknowns-inside-openais-deal-with-bbva-5d6f1c03" rel="noopener noreferrer"&gt;BBVA&lt;/a&gt;, over 3,300 employees across departments—like legal, risk, marketing, and HR—adopted ChatGPT Enterprise. Eighty percent of users said the tool saved them at least two hours per week, significantly improving overall productivity. In healthcare, &lt;a href="https://www.thetimes.co.uk/article/care-home-boss-on-using-ai-were-dealing-with-peoples-lives-enterprise-network-7pbmc9358" rel="noopener noreferrer"&gt;Radfield Home Care&lt;/a&gt; used ChatGPT to support tasks such as staff training, compliance documentation, and client communication, improving service quality while reducing operational load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini vs. ChatGPT for coding: Final insights
&lt;/h2&gt;

&lt;p&gt;Both Gemini with Gemini 2.5 Pro and ChatGPT powered by GPT-4.1 are formidable AI assistants, but they serve slightly different priorities and ecosystems.&lt;/p&gt;

&lt;p&gt;Gemini 2.5 Pro is exceptionally strong in structured reasoning, long-context tasks, and Google Cloud–native applications. Its ability to handle massive token windows and process highly detailed inputs makes it a compelling choice for legal tech, document-heavy workflows, and enterprise systems where integration with Gmail, Docs, or &lt;a href="https://cloud.google.com/vertex-ai?hl=en" rel="noopener noreferrer"&gt;Vertex AI&lt;/a&gt; is key. If your team works with complex documentation or large-scale data or already relies on Google infrastructure, Gemini is a natural fit.&lt;/p&gt;

&lt;p&gt;ChatGPT with GPT-4.1, in contrast, is designed for speed, flexibility, and general-purpose productivity. It excels in fast prototyping, dynamic API integration, and emotionally intelligent interactions, making it especially valuable in cross-functional teams, client-facing workflows, and creative development. Its wide plugin support and custom GPT ecosystem offer a significant advantage for teams that need quick results across a broad set of tools.&lt;/p&gt;

&lt;p&gt;In short, Gemini 2.5 Pro wins on coding depth and scale, while GPT-4.1 wins on versatility and accessibility. However, choosing between Gemini and ChatGPT isn't about which model is better but about which one complements your development style and team culture. As AI continues to evolve, the most successful developers will be the ones who know when to pick the right tool and how to wield it well.&lt;/p&gt;

&lt;p&gt;That wraps up the second part of our series comparing leading AI coding assistants. In the final installment, we'll take a look at Microsoft Copilot and explore how it stacks up against the others in terms of real-world development experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the most out of Gemini and/or ChatGPT
&lt;/h2&gt;

&lt;p&gt;In the Gemini vs ChatGPT for coding debate, both models prove themselves to be excellent AI coding tools that dev teams are already leveraging to great effect. The one you choose will likely come down to ecosystem fit and use case, but it's helpful to understand how they stack up if you have access to both. Our tests found that Gemini tends to excel in high-structure contexts, while ChatGPT tends to be a bit more dynamic and flexible. Looking forward, knowing how to use both will be key to dev success.&lt;/p&gt;

&lt;p&gt;For more developer-focused breakdowns and tool comparisons, subscribe to our blog or follow us on &lt;a href="https://www.linkedin.com/company/descope/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/descopeinc" rel="noopener noreferrer"&gt;X&lt;/a&gt;, and &lt;a href="https://bsky.app/profile/descope.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>gemini</category>
      <category>programming</category>
    </item>
    <item>
      <title>Developer's Guide to AI Coding Tools: Claude vs. ChatGPT</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 18 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/developers-guide-to-ai-coding-tools-claude-vs-chatgpt-p80</link>
      <guid>https://dev.to/descope/developers-guide-to-ai-coding-tools-claude-vs-chatgpt-p80</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/claude-vs-chatgpt" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As AI's capabilities continue to advance, it's playing an increasingly significant role in software development. From writing boilerplate code to debugging, explaining documentation, and even generating entire application components, AI-based tools have greatly impacted traditional app development workflows.&lt;/p&gt;

&lt;p&gt;Conversational coding assistants have emerged as particularly valuable tools, allowing developers to use natural language to request code snippets, architectural suggestions, or environment setup instructions, or to help in understanding unfamiliar frameworks. These assistants aim to reduce context switching, boost productivity, and provide an on-demand form of pair programming.&lt;/p&gt;

&lt;p&gt;In this three-part series, we'll compare the most prominent players in this space: Claude, ChatGPT, Microsoft Copilot, and Gemini. This first part focuses on ChatGPT and Claude, comparing their strengths, limitations, and usability for day-to-day development tasks. Through some example prompts and the tools' responses, you'll learn how they handle code generation, reasoning, error explanation, integration into your workflow, and more. By the end, you should have a good idea about which tool is a better fit for your development stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://platform.claude.com/docs/en/intro" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; is &lt;a href="https://www.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic's&lt;/a&gt; flagship conversational AI assistant, built to prioritize safety, high-context understanding, and transparency. &lt;a href="https://master-spring-ter.medium.com/claude-shannon-the-genius-who-inspired-claude-ai-and-shaped-the-digital-world-352a930f81d6" rel="noopener noreferrer"&gt;Named after Claude Shannon&lt;/a&gt;, the father of information theory, it reflects Anthropic's mission to align AI systems closely with human intent. Designed with developers and knowledge workers in mind, Claude offers a chat interface that excels at long-form reasoning, contextual comprehension, and clear, step-by-step responses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F153tw058fwspq87eyial.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F153tw058fwspq87eyial.png" alt="Fig: Anthropic's vision for Claude's expanding role (Image credit: Anthropic)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Like all things AI, Claude has seen significant upgrades since its initial release. Claude 3.7 Sonnet has achieved &lt;a href="https://www.anthropic.com/news/claude-3-7-sonnet" rel="noopener noreferrer"&gt;62.3 percent accuracy on the SWE-bench Verified test&lt;/a&gt;, which evaluates the ability of AI models to solve real software problems. This is a significant improvement from Claude 3.5 Sonnet's 49 percent accuracy. Claude 3.7 Sonnet also introduces other significant enhancements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid reasoning model&lt;/strong&gt;: Claude 3.7 Sonnet introduces a hybrid reasoning approach, allowing users to choose between rapid responses and &lt;a href="https://www.anthropic.com/research/visible-extended-thinking" rel="noopener noreferrer"&gt;extended, step-by-step thinking&lt;/a&gt;. This flexibility enables the model to handle both straightforward queries and complex problem-solving tasks effectively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended thinking mode&lt;/strong&gt;: In this mode, Claude takes additional time to analyze problems in detail, plan solutions, and consider multiple perspectives before responding. This feature enhances performance in areas such as mathematics, physics, programming, and other complex domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large context window&lt;/strong&gt;: &lt;a href="https://www.anthropic.com/claude/sonnet" rel="noopener noreferrer"&gt;Claude 3.7 Sonnet offers a substantial context window&lt;/a&gt;, capable of processing up to 200,000 tokens. This capacity allows the model to handle extensive documents and codebases, maintaining coherence over long interactions. However, the free plan has undisclosed limits on context window and message sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifacts and Projects&lt;/strong&gt;: The model includes the features &lt;a href="https://zapier.com/blog/claude-artifacts/#what" rel="noopener noreferrer"&gt;Artifacts&lt;/a&gt; for live code previews and collaborative editing, as well as &lt;a href="https://www.anthropic.com/news/projects" rel="noopener noreferrer"&gt;Projects&lt;/a&gt; for organizing chat history and collaborative workflows. These are very useful in app development environments where context switching between multiple files and projects is common.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt;: Alongside Claude 3.7 Sonnet, Anthropic has introduced &lt;a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, a command line tool for agent-based programming. This tool allows developers to delegate complex programming tasks directly through their terminal, streamlining the development process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude 3.7 Sonnet is also trained using Anthropic's &lt;a href="https://www.anthropic.com/research/constitutional-ai-harmlessness-from-ai-feedback" rel="noopener noreferrer"&gt;Constitutional AI&lt;/a&gt; methodology. This training approach adheres to the model's responses with a set of human-defined ethical principles, helping provide responsible and thoughtful outputs.&lt;/p&gt;

&lt;p&gt;As mentioned before, the hybrid reasoning capability allows Claude to switch between immediate responses and more deliberate, in-depth analysis, depending on the task's complexity. This adaptability makes it particularly effective for nuanced, human-like conversations and long-term context retention, which is essential for tasks such as debugging large codebases or conducting in-depth research…&lt;/p&gt;

&lt;h2&gt;
  
  
  ChatGPT
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://platform.openai.com/docs/guides/chat" rel="noopener noreferrer"&gt;ChatGPT&lt;/a&gt; is OpenAI's flagship conversational AI assistant, widely used for tasks ranging from software development and research to creative writing and data analysis. Often treated as a Google-like synonym for AI, ChatGPT has evolved into a versatile tool since its launch in November 2022. ChatGPT supports multiple models from &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;the large lineup of OpenAI models&lt;/a&gt;, offering enhanced capabilities across text, voice, and vision modalities.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqitriqeh0ulkweg445c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqitriqeh0ulkweg445c.png" alt="Fig: Former OpenAI CTO Mira Murati debuting ChatGPT-4o (Image credit: OpenAI)" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As of May 2025, the default model in ChatGPT for free and Plus users is GPT-4o ("o" for "omni"), OpenAI's latest multimodal model. Key features of this model include the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced performance&lt;/strong&gt;: GPT-4o offers &lt;a href="https://openai.com/index/hello-gpt-4o/#:~:text=Output-,Model%20evaluations,-As%20measured%20on" rel="noopener noreferrer"&gt;improved performance over its predecessors&lt;/a&gt;, making it more efficient for various applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice mode&lt;/strong&gt;: The advanced voice mode allows for humanlike spoken conversations, with response times &lt;a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC11392067/#:~:text=It%20can%20respond%20to%20audio,in%20applications%20requiring%20rapid%20reactions." rel="noopener noreferrer"&gt;as low as 232 milliseconds&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image generation&lt;/strong&gt;: GPT-4o includes &lt;a href="https://openai.com/index/gpt-4o-image-generation-system-card-addendum/" rel="noopener noreferrer"&gt;native image-generation capabilities&lt;/a&gt;, succeeding DALL·E 3, and can create realistic images based on textual prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;: GPT-4o is available to all ChatGPT users, with free users having access within usage limits and Plus subscribers enjoying higher usage caps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GPT-4o has a maximum supported context window of 128,000 tokens. However, the &lt;a href="https://openai.com/chatgpt/pricing/#:~:text=Response%20times%2C%20Fastest-,Context%20window,-8K" rel="noopener noreferrer"&gt;context window on the free plan&lt;/a&gt; is very limited compared to the paid plans (8,000 on free vs. 32,000 and 128,000 on the paid plans). However, &lt;a href="https://community.openai.com/t/gpt-4o-context-window-is-128k-but-getting-error-models-maximum-context-length-is-8192-tokens-however-you-requested-21026-tokens/802809" rel="noopener noreferrer"&gt;some users&lt;/a&gt; also report a maximum context length of about 8,000 tokens, even when accessed via the API on a paid subscription.&lt;/p&gt;

&lt;p&gt;At its core, OpenAI's GPT-4.5 architecture, a transformer-based model primarily trained using unsupervised learning, complemented by supervised fine-tuning and reinforcement learning from human feedback powered ChatGPT. This enables it to recognize patterns more effectively, draw connections, and generate creative insights without requiring elaborate prompting… Still, as with any AI chatbot, its effectiveness is greatly impacted by the prompt clarity.&lt;/p&gt;

&lt;p&gt;To provide up-to-date and context-rich answers, ChatGPT can employ &lt;a href="https://help.openai.com/en/articles/8868588-retrieval-augmented-generation-rag-and-semantic-search-for-gpts" rel="noopener noreferrer"&gt;Retrieval Augmented Generation (RAG)&lt;/a&gt;. This technique enhances the model's responses by injecting external context into its prompts at runtime, allowing it to access and incorporate real-time information beyond its static training data. ChatGPT excels in various applications, including coding, writing, and data analysis. Its enhanced natural language processing capabilities enable more fluid and humanlike interactions, making it a valuable tool for a wide range of tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  General overview of the tools
&lt;/h2&gt;

&lt;p&gt;Before getting into the nitty-gritty of software development, let's quickly compare these two general chatbot skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning, problem-solving, and analytical skills&lt;/strong&gt;: Claude 3.7 Sonnet outperforms ChatGPT-4o in structured reasoning and complex problem-solving, particularly in tasks requiring precision and logical analysis. Its hybrid reasoning model allows for both quick responses and extended, step-by-step thinking, making it particularly effective for professional, analytical, and detail-oriented tasks such as analyzing complex if/then or case-based conditions or maintaining consistent reasoning across multiple steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document analysis and summarization&lt;/strong&gt;: Claude 3.7 Sonnet demonstrates superior capabilities in analyzing and summarizing extensive documents, such as legal contracts and historical archives, thanks to its large context window and advanced natural language processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emotional intelligence and conversational style&lt;/strong&gt;: ChatGPT-4o offers a more engaging and emotionally expressive conversational style, capable of simulating humanlike interactions across text, voice, and vision. However, it's worth noting that &lt;a href="https://www.theverge.com/news/658315/openai-chatgpt-gpt-4o-roll-back-glaze-update" rel="noopener noreferrer"&gt;recent (April 2025) updates led to overly supportive and sometimes disingenuous responses&lt;/a&gt;, prompting OpenAI to roll back certain features to maintain authenticity. So it's been iffy in this regard for a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time data and web access&lt;/strong&gt;: Both options support real-time web access. ChatGPT provides this feature across its platforms. Similarly, Claude has introduced web search capabilities, allowing it to access the latest events and information to boost accuracy in tasks benefiting from recent data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost, access, and plans&lt;/strong&gt;: ChatGPT is accessible to free users with limited usage and offers a Plus plan at $20/month for enhanced features and higher usage limits. Claude is available through various platforms, including integrations that offer free access, and Anthropic provides a Pro plan at $20/month for additional capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While they share similar pricing plans and real-time web access capabilities, each tool has distinct strengths: Claude excels at complex problem-solving, while ChatGPT offers more engaging conversations. But what do these strengths mean for software development tasks?&lt;/p&gt;

&lt;h2&gt;
  
  
  Code generation quality
&lt;/h2&gt;

&lt;p&gt;Let's start by comparing the quality of the code generated by the two. To accomplish this, we will use two coding prompts, one for a frontend component and another for a backend script. The prompts will contain requirements but also be purposefully vague in some aspects for us to understand how (or if) the models fill in the gaps on their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend code generation
&lt;/h3&gt;

&lt;p&gt;For the first test, let's try out a simple, age-old task of creating a React component. Here's the prompt that will be used for this test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a React component that displays a list of blog posts fetched from an API. Each post should include a title, author, and published date. Format the date nicely and show a loading state while data is being fetched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you can see, the prompt is quite succinct but expects the final result to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query data externally (or at least leave an easy-to-use placeholder for that);&lt;/li&gt;
&lt;li&gt;include some essential properties (title, author, and published date) while leaving it up to the model to add other properties, like a header image, categories, and so on;&lt;/li&gt;
&lt;li&gt;format one of the properties nicely, which is quite vague (but a human could interpret some meaning from it); and&lt;/li&gt;
&lt;li&gt;show a loading state, which would involve logic for conditional rendering and UI adjustments to accommodate an additional subcomponent (basically, the other set of elements that you will only see when the component is in the loading state).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's take a look at how ChatGPT's free version responds to this prompt:&lt;/p&gt;

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

&lt;p&gt;Let's analyze the code returned by the prompt.&lt;/p&gt;

&lt;p&gt;First impression: you cannot plug and play this component in your React app. That's because it uses an imaginary backend URL. If you already have a backend ready to use at this point, you can replace the value of &lt;code&gt;APP_URI&lt;/code&gt; with the URL to your backend and then test this code. In other cases, you will need to redo the &lt;code&gt;fetchPosts&lt;/code&gt; function to use a set of dummy values as posts while you're developing the component. Because of this, it wouldn't be fair to call it an "easy-to-use" placeholder for the external data source.&lt;/p&gt;

&lt;p&gt;Next, all the essential properties are included, and the date has been formatted adequately using the JS Date object, which is a good practice. Finally, the loading state has been handled appropriately as well.&lt;/p&gt;

&lt;p&gt;Now, let's take a look at how Claude's free version handles the same task. Here's what Claude responds 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%2F08zo2hsavnhj71ubomcm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08zo2hsavnhj71ubomcm.png" alt="Fig: Claude's React response" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For React (and pretty much all web-frontend-related tasks), you get a built-in preview of the generated code in the Claude UI. This significantly improves the chat-development experience as you can send further prompts to refine the app and watch it change very quickly compared to having to plug in the code in your app and hot reload it.&lt;/p&gt;

&lt;p&gt;Here's what the code looks like:&lt;/p&gt;

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

&lt;p&gt;Claude implements all the requirements (essential fields, formatted data field using the JS Date object, and even an emulated external data source that is easy to work with during frontend development). It also does a few extra things on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI design using Tailwind CSS, following a popularly used layout style (cards)&lt;/li&gt;
&lt;li&gt;Icons from the open source project &lt;a href="https://lucide.dev/guide/" rel="noopener noreferrer"&gt;Lucide React&lt;/a&gt; for the author and publishing date fields&lt;/li&gt;
&lt;li&gt;Basic animations such as spin effects on the loading icon and hover effects on the cards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For someone coding an app from scratch, this is an excellent starting point. For someone who already uses a different styling system and maybe has more style guidelines to adhere to (such as no icons or a different color scheme), a few more prompts should do the trick.&lt;/p&gt;

&lt;p&gt;While Claude definitely seems to be a better experience for frontend development, let's try out a backend development task next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend code generation
&lt;/h3&gt;

&lt;p&gt;Here's the prompt for the backend task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write a secure Flask API endpoint in Python that accepts a POST request with a JSON payload, including a user's name and email, validates the input, and returns a confirmation message&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what ChatGPT's free version responds 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%2F5xhz53n2bhjjtriipm8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xhz53n2bhjjtriipm8q.png" alt="Fig: ChatGPT's Python response" width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code snippet works, and the response is quite concise, meeting all the requirements laid out in the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle POST request&lt;/li&gt;
&lt;li&gt;Run null checks on data&lt;/li&gt;
&lt;li&gt;Validate email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Such a response can be great for quick tasks like adding an endpoint to an internal app. However, no real security measures have been implemented in this code snippet. Another tiny detail to note is that ChatGPT struggles with code highlighting in long code snippets. You can already notice that in the line of code below the comment &lt;code&gt;# Return confirmation above&lt;/code&gt;, where it incorrectly highlights the formatted string. Now, let's see if Claude does it differently.&lt;/p&gt;

&lt;p&gt;Here's what Claude's free version responds 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%2Fi0u8311ksqjxj0d448mu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0u8311ksqjxj0d448mu.png" alt="Fig: Claude's Python response" width="800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once again, the response is structured better due to the use of Claude Artifacts. Also, the code highlighting seems to be much better than ChatGPT's. The code snippet seems to work and covers everything laid out in the prompt.&lt;/p&gt;

&lt;p&gt;However, on a closer look, you can see that Claude tried to implement data integrity verification using HMAC to fulfill the "secure Flask API" requirement. This doesn't make sense for the use case as all of the data is being sent by the frontend at once for storage in the backend. An HMAC signature-based data integrity would work when the data was already present in the database and needed to be verified (for instance, when issuing short-lived, signed URLs).&lt;/p&gt;

&lt;p&gt;In this case, implementing CORS or rate limiting to avoid API abuse would have made more sense.&lt;/p&gt;

&lt;p&gt;If you prompt ChatGPT to make the endpoint secure, it implements rate limiting. In contrast, when you highlight the issues with Claude's HMAC implementation, it awkwardly updates the code to issue unique confirmation IDs for each new user registration. Upon being given a second nudge, it implements a bunch of security measures:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Adds a password field&lt;/li&gt;
&lt;li&gt;Adds input sanitization&lt;/li&gt;
&lt;li&gt;Implements rate limiting&lt;/li&gt;
&lt;li&gt;Enforces HTTPS&lt;/li&gt;
&lt;li&gt;Adds security headers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To sum up, while both tools can generate working and adequate code, Claude beats ChatGPT by a large margin when it comes to developer experience and technical details like best practices. Of course, the response will only be as good as your prompting skills, and you need to be careful to double-check everything, as both tools can hallucinate quite confidently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting (bug fixing) capabilities
&lt;/h2&gt;

&lt;p&gt;Debugging is one of the hardest tasks to benchmark in isolation. Real-world debugging rarely involves a single faulty line. It requires an understanding of project context, architecture, business logic, and edge cases. So it's difficult to evaluate these AI tools meaningfully without embedding them in a real workflow. This also makes a strong case against using chatbots for debugging: It's not easy to provide a complete project context to a chat-based code assistant when you're working on large projects.&lt;/p&gt;

&lt;p&gt;The analysis in this section is informed by prior experience and widely shared public sentiment, including popular discussions such as &lt;a href="https://www.reddit.com/r/OpenAI/comments/1dscub9/okay_yes_claude_is_better_than_chatgpt_for_now/" rel="noopener noreferrer"&gt;this Reddit thread&lt;/a&gt;, which highlights Claude's advantage in step-by-step reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging
&lt;/h3&gt;

&lt;p&gt;When it comes to debugging, Claude tends to provide thorough, step-by-step debugging support. It not only identifies errors but also explains root causes and suggests structurally sound fixes, often with helpful comments and design considerations. This makes it a better choice when you're trying to understand why something broke.&lt;/p&gt;

&lt;p&gt;ChatGPT is more direct and concise, often spotting basic bugs and producing a clean fix quickly. For simpler issues or when you're short on time, ChatGPT performs well. However, it may need extra prompting to dive deeper into architectural or semantic bugs.&lt;/p&gt;

&lt;p&gt;However, it can't be emphasized enough that it's important to be careful when debugging with any AI tool as it can overlook issues or confidently suggest wrong solutions. For example, both Claude and ChatGPT recommend a deprecated solution to the following Angular issue:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I am trying to add a static form as a standalone component to an Angular 19.2 app but receiving the following error: No provider for HttpClient How do I fix it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what Claude responds 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%2Fcruz05e7dfk8y6qn2g5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcruz05e7dfk8y6qn2g5v.png" alt="Fig: Claude's response to Angular error" width="800" height="798"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's what ChatGPT responds 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%2Fjr30ecn5v5dw8033sge6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr30ecn5v5dw8033sge6.png" alt="Fig: ChatGPT's response to Angular error" width="800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, when you call out the deprecated solution, Claude is quickly able to figure out the right solution (which, in this case, is to use &lt;code&gt;provideHttpClient&lt;/code&gt;), while ChatGPT presents another deprecated solution.&lt;/p&gt;

&lt;p&gt;Here's Claude's answer:&lt;/p&gt;

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

&lt;p&gt;And here's ChatGPT's:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4fy1cp49y49jp8y03lp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4fy1cp49y49jp8y03lp.png" alt="Fig: ChatGPT's response with a confidently incorrect answer" width="800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's likely that Angular 19.2 was released after the cut-off date of GPT-4o, and since the free version does not have access to web browsing capabilities, it confidently presents made-up answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test generation
&lt;/h3&gt;

&lt;p&gt;When it comes to test generation, both Claude and ChatGPT perform impressively for small, self-contained functions. They generate unit tests quickly, structure them well, and cover common happy and edge cases with little friction.&lt;/p&gt;

&lt;p&gt;However, as soon as the test scope increases—say, when writing tests for a multifile Android project written in Kotlin—Claude has a clear edge. It offers broader coverage, a stronger grasp of framework-specific conventions (like JUnit, MockK, or Espresso), and better test-file organization. Because of a larger context window, Claude is more likely to reference earlier files or set up code when generating integration tests or mocks across modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contextual awareness
&lt;/h3&gt;

&lt;p&gt;Contextual understanding helps when you're working with existing codebases or iterating on complex tasks over multiple turns. As seen in the previous examples, when modifying a React app across files or updating code through a sequence of prompts, both Claude 3.7 Sonnet and ChatGPT-4o demonstrate strong contextual reasoning capabilities.&lt;/p&gt;

&lt;p&gt;When it comes to modifying an existing codebase, both tools can follow the project structure, reference variables across files, and update logic coherently. That said, ChatGPT occasionally stumbles on formatting when generating or appending code snippets. These formatting glitches can lead to small but critical typos or indentation errors that break execution unless caught by the developer.&lt;/p&gt;

&lt;p&gt;In multiturn interactions, performance is closely tied to context window size. Claude 3.7 Sonnet's 200K-token context window allows it to track and recall large amounts of historical information, spanning full files, architectural constraints, or extended conversation threads. ChatGPT-4o, while generally strong at maintaining continuity over shorter sessions, has a smaller effective window and may start to lose precision or forget earlier turns in long, detailed exchanges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration possibilities
&lt;/h2&gt;

&lt;p&gt;Any dev tool is most powerful when it integrates smoothly into a developer's workflow. Both Claude and ChatGPT offer robust integration options, but their approaches reflect their design philosophies. Claude leans into collaborative structure, while ChatGPT emphasizes breadth and extensibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  IDE integrations
&lt;/h3&gt;

&lt;p&gt;Claude offers official and community-built extensions for editors like VS Code, allowing developers to query and modify code directly from their editor. Its standout features (Artifacts and Projects) are uniquely designed for collaborative development. As you've seen above, Artifacts provide a live, editable canvas within the chat. Projects group conversations and assets into task-specific threads, making Claude feel more like a long-term pair programmer.&lt;/p&gt;

&lt;p&gt;ChatGPT, meanwhile, boasts broader IDE support, with integrations that plug seamlessly into GitHub Copilot, JetBrains, and VS Code ecosystems. These extensions are deeply embedded, offering inline completions, doc references, and context-aware suggestions that adapt as you code. However, the quality of responses in the context of coding remains somewhat inferior to Claude's.&lt;/p&gt;

&lt;h3&gt;
  
  
  API and external service connections
&lt;/h3&gt;

&lt;p&gt;Claude provides a stable and well-documented &lt;a href="https://docs.anthropic.com/claude/reference/getting-started-with-the-api" rel="noopener noreferrer"&gt;API&lt;/a&gt;, ideal for embedding AI into apps, chatbots, or backend services. It's especially helpful for generating docs or assisting with schema generation and code commenting.&lt;/p&gt;

&lt;p&gt;ChatGPT offers a more extensive &lt;a href="https://platform.openai.com/docs/api-reference" rel="noopener noreferrer"&gt;API and plugin ecosystem&lt;/a&gt;, with support for function calling, web browsing, and even real-time data retrieval through external tools. From scheduling meetings to querying databases, ChatGPT is designed to operate like a general-purpose AI layer across your entire stack.&lt;/p&gt;

&lt;p&gt;When it comes to integration, ChatGPT has a pretty strong ecosystem among all AI chatbots and APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business applications
&lt;/h2&gt;

&lt;p&gt;In startup environments where speed and agility are key, both Claude 3.7 Sonnet and ChatGPT-4o offer significant advantages. Claude is especially effective for rapid prototyping and MVP development, thanks to features like Artifacts for live previews and Projects for persistent context. These make Claude ideal for teams managing evolving requirements. Claude has proven to be useful for companies, like &lt;a href="https://www.anthropic.com/customers/lazy-ai" rel="noopener noreferrer"&gt;Lazy AI&lt;/a&gt;, that have used it to boost internal software development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpixd69x7vz6fa5vkstet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpixd69x7vz6fa5vkstet.png" alt="Fig: Image of Claude Artifacts in Sonnet 3.5 (Image credit: Anthropic)" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT, meanwhile, shines in tech exploration and early product development with its fast generation, wide range of integrations, and ability to spin up supporting content like documentation, APIs, and database schemas quickly. &lt;a href="https://openai.com/index/genmab/" rel="noopener noreferrer"&gt;Genmab&lt;/a&gt; is a great example of how ChatGPT and OpenAI models can help across a wide range of tasks across the operations of a company through features like custom GPTs and support for multimodal content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyvskqr70hzbovck894g1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyvskqr70hzbovck894g1.png" alt="Fig: The GPT Store for Custom GPTs (Image credit: OpenAI)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In enterprise settings, Claude's large context window and strong reasoning make it well suited for refactoring legacy codebases, standardizing documentation, and automating repetitive tasks with deep context awareness. ChatGPT could be used to complement this by offering real-time data access, plugin integrations, and a flexible API. Both tools serve distinct enterprise needs, depending on whether depth of analysis or breadth of integration is the priority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final insights
&lt;/h2&gt;

&lt;p&gt;Both Claude and ChatGPT are powerful AI coding assistants, but the best choice depends on your goals and project context. If you're working on large-scale, high-stakes applications (like refactoring legacy systems, analyzing long documents, or managing evolving architectural decisions), Claude offers the edge with its extended context window, structured reasoning, and collaboration-focused features like Artifacts and Projects. It's particularly well suited for technical leads, backend engineers, and teams that need clarity and consistency across complex codebases.&lt;/p&gt;

&lt;p&gt;For developers prioritizing speed, creative flexibility, and broad tool integration, ChatGPT is a more versatile companion. Its real-time data access, plugin ecosystem, and memory features make it ideal for prototyping, research, and generalist workflows. Whether you're experimenting with APIs, generating test data, or writing across disciplines, ChatGPT adapts quickly and helps get things done faster.&lt;/p&gt;

&lt;p&gt;Looking ahead, we can expect AI assistants like Claude and ChatGPT to fundamentally reshape software development, making coding more collaborative and turning natural language into a powerful interface for building software. As these tools continue to evolve, choosing the right assistant will become less about raw power and more about how well it fits your team's rhythm, priorities, and stack.&lt;/p&gt;

&lt;p&gt;That wraps up the first part of this series on conversational coding assistants. Stay tuned for the next installment, which explores how Google's Gemini fares against ChatGPT!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>claude</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What Is YubiKey Authentication &amp; How It Works</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 15 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/what-is-yubikey-authentication-how-it-works-4d0g</link>
      <guid>https://dev.to/descope/what-is-yubikey-authentication-how-it-works-4d0g</guid>
      <description>&lt;p&gt;This article was originally published on &lt;a href="https://www.descope.com/learn/post/yubikey-authentication" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;According to the 2024 Verizon Data Breach Investigation Report, 7 out of 10 cybercriminals prefer targeting users over attacking IT infrastructure. This preference isn't surprising: humans, not firewalls, are susceptible to phishing (social engineering attacks that steal login credentials). While &lt;a href="https://www.descope.com/learn/post/mfa" rel="noopener noreferrer"&gt;MFA (multi-factor authentication)&lt;/a&gt; is widely accepted as phishing-resistant using any combination of factors, leveraging possession-based authentication has emerged as the gold standard today. Unlike knowledge-based authentication, the possession factor establishes user presence. Thanks to the proliferation of smartphones, virtually everyone can leverage the possession factor for &lt;a href="https://www.descope.com/learn/post/2fa" rel="noopener noreferrer"&gt;2FA (two-factor authentication)&lt;/a&gt;. But for companies and customers who want to improve their possession-based authentication security even further, &lt;a href="https://www.yubico.com/products/" rel="noopener noreferrer"&gt;YubiKeys&lt;/a&gt; provide the perfect combination of resilience and convenience.&lt;/p&gt;

&lt;p&gt;This article explores how YubiKeys work, how they differ from other authentication methods, and common YubiKey use cases. Lastly, we'll cover some key benefits to help you determine if they're right for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a YubiKey?
&lt;/h2&gt;

&lt;p&gt;A YubiKey is a hardware security device that provides strong authentication when accessing computers, networks, and online services. Yubico developed these small USB or NFC-enabled keys as a physical component in two-factor or multi-factor authentication systems.&lt;/p&gt;

&lt;p&gt;Like a door key with ridges that turn physical tumblers, YubiKeys unlock digital assets by performing operations using cryptographic keys. When a user attempts to log in to a supported service, the cryptographic keys stored on the YubiKey prove the user's identity. Combined with another factor like a password or PIN, YubiKeys provide possession-based 2FA.&lt;/p&gt;

&lt;p&gt;YubiKeys support multiple authentication protocols and systems, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.descope.com/learn/post/fido2" rel="noopener noreferrer"&gt;FIDO2/U2F&lt;/a&gt;: Using the &lt;a href="https://www.descope.com/learn/post/webauthn" rel="noopener noreferrer"&gt;WebAuthn API&lt;/a&gt; and CTAP protocol, enables &lt;a href="https://www.descope.com/learn/post/passwordless-authentication" rel="noopener noreferrer"&gt;passwordless 2FA&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.descope.com/learn/post/otp" rel="noopener noreferrer"&gt;OTP&lt;/a&gt; (One-Time Passcode/Password): Generates single-use codes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.descope.com/learn/post/passkeys" rel="noopener noreferrer"&gt;Passkeys&lt;/a&gt;: A modern standard for passwordless authentication across devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although they're often regarded as a higher-friction authentication tool, YubiKeys are designed to be fast and user-friendly. They often require little more than a USB connection or a single tap followed by a PIN to authenticate. While some YubiKeys feature built-in biometric scanners (for &lt;a href="https://www.descope.com/learn/post/fingerprint-authentication" rel="noopener noreferrer"&gt;fingerprint authentication&lt;/a&gt;) and are more delicate, most models are extremely resistant to damage. Non-biometric YubiKeys are also built to be more durable than smartphones, with no moving parts or batteries. YubiKeys are resistant to water, crushing, and other forms of physical harm.&lt;/p&gt;

&lt;h2&gt;
  
  
  YubiKey vs. passkey vs. authenticator app
&lt;/h2&gt;

&lt;p&gt;To fully understand their place in the authentication ecosystem, it's helpful to compare YubiKeys with other popular methods. First, let's discuss their relationship with passkeys, which are a form of cross-device passwordless authentication built atop the FIDO2 protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  YubiKey vs. passkey
&lt;/h3&gt;

&lt;p&gt;The most obvious difference between YubiKeys and passkeys is that YubiKeys are physical devices, and passkeys are FIDO2-based credentials. However, YubiKeys and passkeys aren't mutually exclusive. As Yubico's &lt;a href="https://www.yubico.com/blog/a-yubico-faq-about-passkeys/" rel="noopener noreferrer"&gt;FAQ on passkeys&lt;/a&gt; phrases it, "They're the same, and they're different." They're similar because passkeys are built on the same PKI (public-key infrastructure) that YubiKeys used since 2018. YubiKeys can currently store up to 25 different passkeys, though Yubico intends to expand this as the market for passwordless implementation grows.&lt;/p&gt;

&lt;p&gt;They're different because YubiKey passkeys and standard passkeys follow different rules for cross-device duplication. Passkeys on most devices can be copied using the associated cloud account's credentials. Passkeys on YubiKeys are bound to the device and can't be copied.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of how YubiKeys and passkeys compare with one another:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;YubiKey passkey&lt;/th&gt;
&lt;th&gt;Standard passkey&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Device-bound in the YubiKey's hardware, making them impossible to copy.&lt;/td&gt;
&lt;td&gt;Stored on cloud services and within each associated device's TPM (Trusted Platform Module), a dedicated component for protecting authentication secrets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability&lt;/td&gt;
&lt;td&gt;Tied to the physical device, supporting possession-based 2FA by requiring the key's presence.&lt;/td&gt;
&lt;td&gt;Can be synced across trusted devices, offering more convenience but potentially increasing the attack surface.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Designed with hardware-level protection against extraction or duplication.&lt;/td&gt;
&lt;td&gt;Rely on the security of the device or cloud service provider.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  YubiKey vs. authenticator app
&lt;/h3&gt;

&lt;p&gt;Both YubiKeys and &lt;a href="https://www.descope.com/learn/post/authenticator-app" rel="noopener noreferrer"&gt;authenticator apps&lt;/a&gt; (Google Authenticator, Authy, etc.) provide two-factor authentication, but they differ in several key ways. Let's start with hardware security.&lt;/p&gt;

&lt;p&gt;While modern smartphones contain dedicated TPMs, that doesn't make them impervious to malware or remote attacks. Most cybercriminals won't target an authenticator app, but a phone's operating system is comparatively easy to infect—especially when unassuming users install risky apps and allow unfettered access, options that simply don't exist for YubiKeys.&lt;/p&gt;

&lt;p&gt;It's worth noting that most cryptographic operations take place in a Trusted Execution Environment (Android) or Secure Enclave (Apple), a portion of the processor that runs its own operating system. However, an authenticator app's secrets needn't be breached to steal an OTP, and the risk of malware is still present. On the other hand, YubiKeys are built to resist attack and live in isolation from other software.&lt;/p&gt;

&lt;p&gt;Next is form factor and dependency. The YubiKey is an unpowered, flash drive-sized device dedicated to authentication, and a smartphone is much larger and requires power to function. An authenticator app can add login friction due to its need for electricity, forcing users with dead phones to find a charger and wait.&lt;/p&gt;

&lt;p&gt;Last but not least are the OTPs themselves. While authenticator apps and YubiKeys support time-based (TOTP) and counter-based (HOTP) one-time passcodes, their delivery and security features are completely different. YubiKeys produce 44-character OTPs that require minimal user action. The authentication secrets (seeds) a YubiKey uses to generate OTPs are backed by AES-128 encryption, shielding them from direct attack. Conversely, authenticator apps generate six-digit codes that must be entered manually, allowing a scammer to phish them. The security surrounding underlying authentication secrets can vary, with most relying on a combination of dedicated hardware and operating system-specific key storage.&lt;/p&gt;

&lt;p&gt;Below is a breakdown of how YubiKeys and authenticator apps stack up with each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;YubiKey&lt;/th&gt;
&lt;th&gt;Authenticator app (smartphone)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Isolated from vulnerable operating systems and apps.&lt;/td&gt;
&lt;td&gt;Although resilient against direct attack, it exists alongside exploitable software.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency&lt;/td&gt;
&lt;td&gt;Compact, never needs to be updated, and doesn't require batteries to operate.&lt;/td&gt;
&lt;td&gt;Can be bulky, may require software updates, and needs a charged battery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durability&lt;/td&gt;
&lt;td&gt;Extremely tough against physical force. Difficult to break or crush, and water-resistant.&lt;/td&gt;
&lt;td&gt;Able to withstand minor damage, but still fragile and susceptible to water damage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-Time Passcodes&lt;/td&gt;
&lt;td&gt;Produce 44-character OTPs backed by 128-bit encryption, and automatically enter codes when prompted.&lt;/td&gt;
&lt;td&gt;Generate six-digit passcodes that require manual user entry. Encryption of OTP seeds varies based on OS and device.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How YubiKey authentication works
&lt;/h2&gt;

&lt;p&gt;YubiKey authentication leverages the principle of possession-based, two-factor authentication (2FA). It combines something you have (a YubiKey) with something you know (a PIN or password), or in the case of biometric-enabled YubiKeys, something you are (your fingerprint scan). The YubiKey stores authentication credentials and performs cryptographic operations, never exposing the secret keys. This closed-loop process ensures that even if a user's password is compromised, an attacker can't gain access without physical possession of the key and its associated PIN.&lt;/p&gt;

&lt;p&gt;Below, we've outlined the steps required to authenticate using YubiKeys with passkeys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Passkey authentication with YubiKey
&lt;/h3&gt;

&lt;p&gt;Passkeys use public-key cryptography or PKI (public-key infrastructure) to provide a phishing-resistant authentication process. Here's how it works with a YubiKey:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initiation&lt;/strong&gt;: The user starts the authentication ceremony by attempting to log in to an app or service that supports passkey authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge creation&lt;/strong&gt;: The app generates a cryptographic authentication challenge and sends it to the client (the browser or device).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge transmission&lt;/strong&gt;: The client passes this challenge to the authenticator (in this case, the YubiKey).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification request&lt;/strong&gt;: The YubiKey requests user verification and presence from the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIN entry or biometric scan&lt;/strong&gt;: When prompted, the user enters their PIN or scans their fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge signing&lt;/strong&gt;: Upon successful verification, the YubiKey (the authenticator) uses its stored private key to sign the challenge. It then sends the signed challenge to the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response submission&lt;/strong&gt;: The client provides the app with the signed challenge from the YubiKey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification&lt;/strong&gt;: The service verifies the signed challenge using the corresponding public key associated with the user's account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication complete&lt;/strong&gt;: If the verification is successful, the app confirms the authentication, granting the user access.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  OTP authentication with YubiKey
&lt;/h3&gt;

&lt;p&gt;YubiKeys can also authenticate using OTPs (One-Time Passwords/Passcodes), but the process is slightly different. Like all OTPs, YubiKeys generate one-time passcodes based on two elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A seed, which is a static secret key shared between the YubiKey and the server.&lt;/li&gt;
&lt;li&gt;A moving factor, which can be time-based or counter-based, depending on the OTP type (time-based or counter-based, TOTP or HOTP).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a more extensive exploration of OTPs check out our guide, &lt;a href="https://www.descope.com/learn/post/otp" rel="noopener noreferrer"&gt;OTP Authentication Explained: Definition, Uses &amp;amp; Benefits&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Notably, YubiKey OTPs differ from the standard six-digit codes an authenticator app provides. Instead, they are highly complex, 44-character strings with 128-bit encryption, making them nearly impossible to spoof. Here's how it works:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initiation&lt;/strong&gt;: The user attempts to log in to an app or service that supports YubiKey OTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTP generation&lt;/strong&gt;: When prompted, the user connects their YubiKey and activates it. In most cases, this is simply touching a sensor on the YubiKey. The YubiKey generates a unique OTP based on the seed and moving factor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTP submission&lt;/strong&gt;: The client (browser or app) sends this OTP to the service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification&lt;/strong&gt;: The service validates the OTP, either using Yubico's validation servers or (in the case of an enterprise setup) the organization's validation server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication complete&lt;/strong&gt;: After receiving a successful validation result from Yubico or the private server, the app or service grants the user access.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common YubiKey use cases
&lt;/h2&gt;

&lt;p&gt;Because YubiKeys support both OTP and passkey authentication, they support use cases across a wide range of industries and activities. Below are just a few examples of how phishing-resistant YubiKeys can uplevel security in various scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workforce MFA for remote workers
&lt;/h3&gt;

&lt;p&gt;With remote work becoming increasingly common, organizations can use YubiKeys to ensure secure access to company resources from any location. Remote employees can use YubiKeys for stronger authentication, logging in to corporate networks, cloud services, and sensitive applications. Companies with older, legacy frameworks can opt for OTP-based authentication, while more modern systems can benefit from passkey-enabled passwordless login.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading individual security
&lt;/h3&gt;

&lt;p&gt;Some individuals prefer YubiKeys when logging in to consumer applications, and offering YubiKey support on your app or service can help satisfy these security-conscious customers. Because one YubiKey supports up to 25 different passkeys, these users can benefit from possession-based security across multiple services without adding another link to their real-world keychain. Even if your app isn't ready to support passkeys, you can still work with YubiKeys using OTPs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Government officials and sensitive industries
&lt;/h3&gt;

&lt;p&gt;YubiKeys can be pivotal in high-security scenarios where protecting privileged data is an operational requirement. Government agencies, defense contractors, and critical infrastructure operators use YubiKeys to secure classified information and sensitive systems. In environments where regulatory compliance demands traditional smart card functionality, YubiKeys can double as a digital and physical access device.&lt;/p&gt;

&lt;h2&gt;
  
  
  YubiKey benefits
&lt;/h2&gt;

&lt;p&gt;While YubiKeys are certainly multi-faceted security devices, we've distilled their key benefits into three concise categories: security, user experience, and reliability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enhanced security
&lt;/h3&gt;

&lt;p&gt;YubiKeys offer robust, resilient protection against numerous cyber threats. They provide phishing-resistant authentication through hardware-bound passkeys, significantly reducing the risk of ATO (&lt;a href="https://www.descope.com/learn/post/account-takeover" rel="noopener noreferrer"&gt;account takeover&lt;/a&gt;). Unlike smartphones or traditional OTP methods, YubiKeys require physical presence for authentication, effectively eliminating risks associated with remote attacks and credential theft. In short, YubiKeys can't be duplicated, hijacked, monitored, or interfered with. Case in point: &lt;a href="https://blog.cloudflare.com/2022-07-sms-phishing-attacks/" rel="noopener noreferrer"&gt;Cloudflare stopped a 2022 SMS phishing attack&lt;/a&gt; targeting its workforce using FIDO2-compliant Yubikeys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improved user experience
&lt;/h3&gt;

&lt;p&gt;While they may appear more cumbersome, YubiKeys can quickly surpass a smartphone's speed and accessibility. Authentication with a YubiKey is often as simple as inserting the key and tapping it or using NFC, then entering a short PIN. This is typically faster and more convenient than a long, hard-to-remember password, and it's much speedier and more secure than manually submitting an OTP. Additionally, YubiKeys don't require a power adapter, their form factor can be extremely compact, and they don't require constant software updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reliability and durability
&lt;/h3&gt;

&lt;p&gt;YubiKeys are designed to withstand much more than daily wear and tear, offering greater protection compared to smartphones. They're resistant to water and crushing, and they have no moving parts or battery to short-circuit. YubiKeys' physical ruggedness makes them ideally suited for a wide range of environments, from field operations to heavy industry. Because of their long lifespan, YubiKeys are a highly cost-effective alternative to issuing company smartphones and are much more secure than a BYOD (Bring Your Own Device) policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Easily support YubiKey authentication with Descope
&lt;/h2&gt;

&lt;p&gt;YubiKeys offer a powerful solution for reinforcing authentication security across countless industries and use cases. They're physically tough, phishing-resistant, and built on dedicated hardware proven to defend against cyber threats. With strong authentication options for both passkeys and OTPs, YubiKeys address many of the security obstacles organizations and individuals face daily.&lt;/p&gt;

&lt;p&gt;While YubiKey authentication can significantly boost your security posture, integrating it with your systems, service, or app can be difficult and complex. At Descope, we make development easier regardless of the authentication method. Adding passkeys with &lt;a href="https://www.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt; is as easy as selecting an authentication type, picking a login screen, and deploying.&lt;/p&gt;

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

&lt;p&gt;Descope's flexible platform empowers developers to quickly and effortlessly implement YubiKey authentication, combining the security benefits of possession-based authentication with the accessibility of our drag-and-drop interface.&lt;/p&gt;

&lt;p&gt;To get started integrating YubiKeys with passkeys or OTPs using Descope, &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;sign up for our "Free Forever" plan&lt;/a&gt; today. Have questions? We're waiting to connect with you at &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;, our open developer community.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
    </item>
    <item>
      <title>Next.js vs Remix: What's the Difference?</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Thu, 14 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/nextjs-vs-remix-whats-the-difference-8ga</link>
      <guid>https://dev.to/descope/nextjs-vs-remix-whats-the-difference-8ga</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/nextjs-vs-remix" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Anyone who's worked with &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt; knows it's easy to get started with, and you can quickly become quite productive. However, once you move beyond the basics and need full-stack capabilities, like server-side rendering (SSR), selecting a React framework becomes the next step. Two of the most popular frameworks are &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; and &lt;a href="https://remix.run/" rel="noopener noreferrer"&gt;Remix&lt;/a&gt;. Both provide powerful tools to build high-performance web applications, but their philosophies and approaches differ.&lt;/p&gt;

&lt;p&gt;This article compares Next.js and Remix, helping you get to know both frameworks, including their key concepts, features, architecture, performance capabilities, learning curves, and use cases. By the end, you'll have a solid understanding of how these frameworks stack up against each other and, hopefully, a firm base for choosing the right framework for your specific needs and ways of solving problems.&lt;/p&gt;

&lt;p&gt;This comparison reflects the state of Next.js at version 14 and Remix at version 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js overview
&lt;/h2&gt;

&lt;p&gt;Next.js was developed by &lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt; and was launched in 2016. It has since become one of the most widely used frameworks in the React ecosystem. Next.js is known for its flexibility and extensive feature set. It's built to solve the complexities of SSR and static site generation (SSG), as well as easily handle routing, expose API endpoints, and optimize performance for the end user.&lt;/p&gt;

&lt;p&gt;Here are some key features of Next.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/routing" rel="noopener noreferrer"&gt;File-based routing&lt;/a&gt; automatically generates routes based on the filesystem structure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/rendering" rel="noopener noreferrer"&gt;SSR and SSG support&lt;/a&gt; gives developers flexibility in choosing between static and dynamic rendering methods.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers" rel="noopener noreferrer"&gt;API routes&lt;/a&gt;, which are built-in API endpoints that don't need a separate backend server.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating" rel="noopener noreferrer"&gt;Incremental Static Regeneration (ISR)&lt;/a&gt; enables static content updates without rebuilding the entire site.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes" rel="noopener noreferrer"&gt;Edge rendering support&lt;/a&gt; allows for even faster responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large companies frequently use Next.js for e-commerce, content-heavy websites, and sometimes enterprise-grade applications. Notable users include Twitch, Hulu, TikTok, Spotify, and Sonos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remix overview
&lt;/h2&gt;

&lt;p&gt;Remix was developed by the creators of &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;React Router&lt;/a&gt; and was released in 2021. It's built on top of the React Router. The framework is a newer entry to the React ecosystem. It focuses on web standards, performance, and a &lt;a href="https://remix.run/docs/main/discussion/data-flow" rel="noopener noreferrer"&gt;full-stack data flow&lt;/a&gt;, giving the developer a very high level of control. While Remix also supports SSR, it takes a different approach from Next.js, prioritizing progressive enhancement and fine-grained control over data loading and mutations.&lt;/p&gt;

&lt;p&gt;Remix offers the following key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://remix.run/docs/main/guides/data-loading" rel="noopener noreferrer"&gt;Loader&lt;/a&gt; and &lt;a href="https://remix.run/docs/main/route/action" rel="noopener noreferrer"&gt;action&lt;/a&gt; functions efficiently handle server-side data fetching and mutations, avoiding unnecessary JavaScript on the client.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://remix.run/docs/main/file-conventions/routes#nested-routes" rel="noopener noreferrer"&gt;Nested routes&lt;/a&gt; are optimized for both layout and data loading, making it easy to build complex user interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://remix.run/docs/main/guides/streaming" rel="noopener noreferrer"&gt;Streaming support&lt;/a&gt; enables fast content delivery through streaming HTML directly to the browser.&lt;/li&gt;
&lt;li&gt;Web standards focus means Remix emphasizes built-in browser APIs over proprietary abstractions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remix is ideal for projects where you want to squeeze out the maximum possible performance from your server-side React solution and want to have a very high level of control in your React application. Some companies that have adopted Remix include &lt;a href="https://thenewstack.io/why-chatgpt-shifted-from-next-js-to-remix-some-theories/" rel="noopener noreferrer"&gt;ChatGPT&lt;/a&gt;, &lt;a href="https://hydrogen.shopify.dev/" rel="noopener noreferrer"&gt;Shopify Hydrogen&lt;/a&gt;, and &lt;a href="https://www.docker.com/products/docker-scout/" rel="noopener noreferrer"&gt;Docker Scout&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js core concepts
&lt;/h2&gt;

&lt;p&gt;At its core, Next.js is designed to simplify SSR and SSG through a &lt;a href="https://nextjs.org/docs/app/building-your-application/routing" rel="noopener noreferrer"&gt;file-based routing system&lt;/a&gt;. The framework allows developers to build pages by creating files in the &lt;code&gt;app&lt;/code&gt; directory. Placing a &lt;code&gt;page.ts|js&lt;/code&gt; file in a folder named &lt;code&gt;about&lt;/code&gt; enables the Next.js app to show a page when you navigate to the &lt;code&gt;/about&lt;/code&gt; URL. This routing method allows you to easily view the project's file structure to see which pages and APIs are routed within the app. It's a straightforward approach that feels familiar to React developers.&lt;/p&gt;

&lt;p&gt;Pre-rendering is one of the standout Next.js features, giving developers a choice between SSG, which generates HTML at build time, and SSR, which renders HTML on each request. With ISR, Next.js can also regenerate specific pages after a certain interval, allowing static pages to be updated, behind the scenes, without a full rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remix core concepts
&lt;/h2&gt;

&lt;p&gt;Remix uses file-based routing as its conventional route configuration. But Remix also allows &lt;a href="https://remix.run/docs/en/main/discussion/routes#manual-route-configuration" rel="noopener noreferrer"&gt;manual route configuration&lt;/a&gt; for scenarios that the file-based routing isn't flexible enough to cover. On top of this, Remix has a nested routing architecture, which enables seamless data fetching and UI composition.&lt;/p&gt;

&lt;p&gt;The framework introduces loader and action functions for each route, where loaders handle data fetching and actions manage form submissions and mutations. This approach decouples the frontend and backend logic, ensuring that the right data is always available without duplicating code across the stack.&lt;/p&gt;

&lt;p&gt;Remix excels in delivering optimized and performant user experiences, particularly through its streaming capabilities, which allow pages to load incrementally as data becomes available, significantly improving perceived performance for users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boilerplate Next.js
&lt;/h2&gt;

&lt;p&gt;To get started with Next.js, all you have to do is run &lt;code&gt;npx create-next-app@latest&lt;/code&gt; and answer some configuration questions for your setup.&lt;/p&gt;

&lt;p&gt;The layout file, which defines the shared layout and content for your entire website, is located at &lt;code&gt;/app/layout.tsx&lt;/code&gt;. In its simplest form, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this file, the standard React &lt;code&gt;children&lt;/code&gt; prop is used to define where the page content will be rendered within the layout. To display a page, you need to add a file at &lt;code&gt;/app/page.tsx&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This is the page content&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following the routing convention in Next.js, if you want an about page on your website under the URL &lt;code&gt;https://example.com/about&lt;/code&gt;, you need to add a page file under &lt;code&gt;/app/about/page.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boilerplate Remix
&lt;/h2&gt;

&lt;p&gt;To get started with Remix, simply run &lt;code&gt;npx create-remix@latest&lt;/code&gt; to set up a new app.&lt;/p&gt;

&lt;p&gt;This generates several files, including &lt;code&gt;/app/root.tsx&lt;/code&gt;, which serves as the site's layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike Next.js, it uses an explicit export to follow React standards rather than relying on conventions.&lt;/p&gt;

&lt;p&gt;The root page route is located at &lt;code&gt;/app/routes/_index.tsx&lt;/code&gt; and contains the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This is the page content&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To add an about page in Remix, you add a new file under the path &lt;code&gt;/app/routes/about.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As you can see, the basic functionality is very similar across the frameworks. It's when you get into more detailed functionality that they start diverging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js performance features
&lt;/h2&gt;

&lt;p&gt;Next.js offers a range of performance optimization features that are intended to help the developer get a high level of performance by default. The features include SSG, SSR, and ISR, which provide flexibility in how content is rendered. SSG lets you pre-render the whole site into a static web application, resulting in faster loading times. SSR allows custom content to be dynamically served from the server, which provides a seamless experience when navigating the site quickly as a user. This is the same functionality that ensures that search engines get the expected content directly for good search engine optimization (SEO). ISR enables cached content to be served instantly while simultaneously being updated in the background for the following users.&lt;/p&gt;

&lt;p&gt;Its image optimization feature automatically adjusts images for the device and screen size, improving load times. Additionally, automatic code-splitting and script optimization make sure that only the necessary JavaScript is loaded on the page.&lt;/p&gt;

&lt;p&gt;Edge rendering was also introduced a few versions back as an experimental feature in version 12.2. This allows specific (or all) pages or API routes to be served from geographically distributed edge locations closer to the end user. This also further boosts speed and reduces latency.&lt;/p&gt;

&lt;p&gt;From a developer experience perspective, it's worth noting that Next.js was built using &lt;a href="https://webpack.js.org/" rel="noopener noreferrer"&gt;webpack&lt;/a&gt; for bundling, which has struggled to maintain performance. Therefore, when changing something in the code, reload times can be very slow. For this reason, the Next.js team has been working on getting full compatibility on &lt;a href="https://nextjs.org/docs/architecture/turbopack" rel="noopener noreferrer"&gt;its own bundler, Turbopack&lt;/a&gt;. As of Next.js 14, Turbopack is still considered beta but is much faster than the default experience with webpack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remix performance features
&lt;/h2&gt;

&lt;p&gt;Remix is focused on data efficiency and page load speed. The use of loader functions helps to load only the data required for a specific page or API endpoint. This ensures that the data is fetched server-side and eliminates the need for client-side fetching once the page has been rendered. Remix also offers streaming HTML, meaning users can start interacting with content while individual parts of the page continue loading as fast as possible in the background.&lt;/p&gt;

&lt;p&gt;Remix avoids client-side JavaScript for form submissions, relying instead on server-side logic. This reduces the overall JavaScript bundle size and improves performance. It also means that the basic functionality of posting a form, with fields and a submit button, can even work on a browser with JavaScript disabled.&lt;/p&gt;

&lt;p&gt;Remix also supports &lt;a href="https://remix.run/docs/main/guides/performance" rel="noopener noreferrer"&gt;serving content from the edge&lt;/a&gt;. Since the Remix documentation site is hosted on Fly.io, it achieves time to first byte (TTFB) under one hundred milliseconds, and updates to the site take only a couple of minutes.&lt;/p&gt;

&lt;p&gt;Remix supports the bundler Vite, which gives a remarkably fast developer experience. Remix reported that its &lt;a href="https://remix.run/blog/remix-heart-vite" rel="noopener noreferrer"&gt;Hot Module Replacement (HMR) became ten times faster using Vite&lt;/a&gt;, for example. Vite will become the &lt;a href="https://remix.run/docs/main/guides/vite" rel="noopener noreferrer"&gt;default compiler of Remix in the future&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js learning curve
&lt;/h2&gt;

&lt;p&gt;Next.js provides a fairly gentle learning curve, especially for developers already familiar with React. The Next.js teams make it a point to be transparent in that they try to work closely with the React team.&lt;/p&gt;

&lt;p&gt;Starting a project with &lt;code&gt;create-next-app&lt;/code&gt; is straightforward, and the framework's file-based routing system feels intuitive to most. Simply run &lt;code&gt;npx create-next-app@latest&lt;/code&gt; and follow the wizard to have a working project up in no time.&lt;/p&gt;

&lt;p&gt;With extensive documentation and a large community, developers have plenty of resources to get started quickly. However, advanced features like ISR and edge rendering can introduce complexity.&lt;/p&gt;

&lt;p&gt;Next.js aims to provide good defaults for most choices—such as routing, SSR, data fetching, CSS handling, and much more—that have to be made when starting a project in just vanilla React. However, it has a history of changing defaults, and the community has expressed some frustration—particularly on Reddit—about more fine-grained feature control being unintuitive or even impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remix learning curve
&lt;/h2&gt;

&lt;p&gt;Remix architecture requires developers to learn new patterns, particularly involving nested routing and &lt;a href="https://remix.run/docs/main/guides/data-loading" rel="noopener noreferrer"&gt;loader/action functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Remix also has an intuitive CLI called &lt;a href="https://remix.run/docs/main/other-api/create-remix" rel="noopener noreferrer"&gt;create-remix&lt;/a&gt; that allows you to quickly create a new Remix app. You simply run &lt;code&gt;npx create-remix@latest&lt;/code&gt; to get started.&lt;/p&gt;

&lt;p&gt;Unlike typical React applications that rely heavily on client-side JavaScript to handle forms and data mutations, Remix uses its routing and loader/action functions to handle as much as possible on the server side. While this approach can result in more efficient apps, it introduces a steeper learning curve.&lt;/p&gt;

&lt;p&gt;Given that Remix is a newer framework, it has a smaller community and newer documentation, resulting in fewer resources online for learning Remix compared to Next.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js ecosystem
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vercel.com/frameworks/nextjs" rel="noopener noreferrer"&gt;Next.js is developed by Vercel&lt;/a&gt;, which also provides deployment and hosting for multiple JavaScript frameworks. Given that Next.js is one of Vercel's main frameworks, it's no surprise that the Next.js experience on Vercel is seamless and easy. Vercel detects that the repository contains a Next.js app. Then Vercel takes care of all configurations for the deployment and makes sure the app just works and is available on the internet.&lt;/p&gt;

&lt;p&gt;The framework has strong support for integrations with content management systems (CMS), headless e-commerce, and popular third-party services. The wide range of Next.js libraries means you can easily extend its functionality. There is also a whole &lt;a href="https://vercel.com/marketplace" rel="noopener noreferrer"&gt;Vercel Marketplace for integrations&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remix ecosystem
&lt;/h2&gt;

&lt;p&gt;Remix takes a more minimalist approach, emphasizing web fundamentals over proprietary tools. It encourages using built-in browser APIs and traditional React libraries, allowing developers to build apps without needing Remix-specific integrations. This means that anyone who builds libraries or parts of their solution in plain React can easily plug it into Remix, without worrying about framework-specific quirks or special considerations based on &lt;a href="https://remix.run/docs/main/guides/performance#this-website--flyio" rel="noopener noreferrer"&gt;where the Remix application is hosted&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Although the Remix ecosystem is smaller, its focus on standard web technologies ensures long-term compatibility and flexibility as the ecosystem grows. &lt;a href="https://remix.run/resources" rel="noopener noreferrer"&gt;Remix relies on the community&lt;/a&gt; to build the resources in the ecosystem over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;p&gt;As mentioned, Next.js is used by many large organizations, such as Twitch, Hulu, TikTok, Spotify, and Sonos. It handles everything from marketing sites to large, composable commerce platforms for e-commerce and even web applications running server-side functionality, single-page application (SPA) functionality, or a combination of both. It's a good fit for projects that require scalability, SEO optimization, and real-time content updates with ISR.&lt;/p&gt;

&lt;p&gt;Remix also has some strong brands using its framework, such as Shopify, Docker, and even the &lt;a href="https://gcn.nasa.gov/" rel="noopener noreferrer"&gt;NASA General Coordinates Network (GCN)&lt;/a&gt; site. The Remix framework really shines in scenarios where performance and the following fast user experience are critical. Its approach to data loading and form handling is especially useful for projects that need fast, interactive user interfaces and seamless transitions. Similar to Next.js, it also supports a mix of server-side and SPA functionality, as well as robust SEO optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right framework
&lt;/h2&gt;

&lt;p&gt;There are many things to consider when deciding whether you should use Next.js or Remix for your project. As is often the case with technology comparisons, there is no clear winner. The decision depends on your specific scenario and needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs#main-features" rel="noopener noreferrer"&gt;Comprehensive feature set&lt;/a&gt; includes file-based routing, rendering on both server and client-side, data fetching, and TypeScript support built-in.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/optimizing" rel="noopener noreferrer"&gt;Lots of performance features out of the box&lt;/a&gt; includes optimizations of images, fonts, bundles, and lazy loading.&lt;/li&gt;
&lt;li&gt;Large &lt;a href="https://nextjs.org/docs/community" rel="noopener noreferrer"&gt;community&lt;/a&gt; and ecosystem with extensive libraries, integrations, and resources available for developers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next.js weaknesses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complexity: Next.js offers many advanced features related to updating data displayed in its apps, like ISR, data revalidation, and automatic caching on data fetching. However, understanding and using these features may require significant time and effort. For example, implementing ISR effectively can be difficult for dynamic content-heavy sites.&lt;/li&gt;
&lt;li&gt;Performance in larger applications: As the amount of data and number of pages in your solution grow, so do the build time and performance requirements for hosting. Prebuilding many pages takes more build time. If you revalidate a lot of data on a lot of pages, this requires significant resources on your server, potentially driving up costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Remix strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://remix.run/docs/main/discussion/data-flow" rel="noopener noreferrer"&gt;Performance-focused&lt;/a&gt;: Fine-tuned control over data loading and streaming improves both speed and user experience.&lt;/li&gt;
&lt;li&gt;Simplified form handling: Built-in form submissions without JavaScript reduce the need for complex state management and unnecessary client-side scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Remix weaknesses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller ecosystem: Remix has fewer integrations and community resources compared to Next.js.&lt;/li&gt;
&lt;li&gt;Steeper learning curve: Developers familiar with standard React patterns may find the unique architecture of Remix harder to grasp initially.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Other considerations
&lt;/h2&gt;

&lt;p&gt;There are some more points to take into consideration for an informed decision on which framework to choose. These points relate more to the long-term success of a project than the previously mentioned strengths and weaknesses, which are more related to how quickly you can pick up the framework and be productive.&lt;/p&gt;

&lt;p&gt;SEO is critical for the organic growth and spread of any public website today. When it comes to SEO, both frameworks support robust SEO by allowing you to set metadata related to SEO and render the pages' content server-side. This means that Google and other search engines get the pages' full content and don't have to wait for content to appear step-by-step, which is usually the experience in an SPA.&lt;/p&gt;

&lt;p&gt;Performance is an important factor to SEO since Google, and other search engines, use how fast a site shows its pages as a factor to score the website. When it comes to this topic, Next.js probably has a slight edge with SSG and ISR, which allow fast delivery and timely updates of heavily cached content.&lt;/p&gt;

&lt;p&gt;For complex routing scenarios, Remix has an edge over Next.js, given that Remix has file-based routing, which is very similar to Next.js. Remix also supports manual route configurations to cover more complex scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison overview
&lt;/h2&gt;

&lt;p&gt;For an overview of the comparison between the frameworks, we can look at the features in table format. This shows that there are only slight differences between these two very competent frameworks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Next.js&lt;/th&gt;
&lt;th&gt;Remix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power of routing&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size of ecosystem&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ease of learning&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One of the most important factors to consider when choosing any technology is the skill level and experience of your team relative to the complexity and learning curve of the tool. In this case, it's important to gauge what your team's prior experience of SSR is of React components, which type of routing they've used previously if they're used to working with web standards, and how much they've kept up with the bleeding-edge features of modern versions of React.&lt;/p&gt;

&lt;p&gt;If your team's experience with SSR is limited, then both frameworks will require a shift in mindset. If they're used to the React Router, then the Remix routing approach might be more intuitive. If they've been keeping up with the latest patterns, such as React.Suspense, then the patterns in Next.js might be more intuitive.&lt;/p&gt;

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

&lt;p&gt;This article explored the core features, performance features, learning curves, and use cases for both Next.js and Remix. While Next.js provides a robust and flexible toolkit for developers to get started quickly, Remix offers a performance-first approach with a unique take on routing and data management. The decision between the two frameworks ultimately depends on the needs of your project and the skills and experience of your development team.&lt;/p&gt;

&lt;p&gt;No matter which framework you choose, any modern web application needs to give the user a unique experience based on who they are. This can be a complex and quite time-consuming problem to handle on your own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.descope.com/" rel="noopener noreferrer"&gt;Descope&lt;/a&gt; is a service that offers a unique solution through its no-code/low-code visual workflows, allowing developers to quickly create and customize authentication flows without writing extensive code or disrupting the app's architecture. With a strong focus on passwordless authentication—including methods like magic links, one-time passwords (OTPs), and passkeys—Descope not only simplifies the implementation of secure user management but also enhances user experience. Check out our &lt;a href="https://docs.descope.com/getting-started/nextjs" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; and &lt;a href="https://github.com/descope-sample-apps/remix-oauth2-sample-app" rel="noopener noreferrer"&gt;Remix&lt;/a&gt; resources to get started.&lt;/p&gt;

&lt;p&gt;For more framework comparisons and authentication trends, subscribe to our blog or follow us on &lt;a href="https://www.linkedin.com/company/descope/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; and &lt;a href="https://bsky.app/profile/descope.com" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add Authentication in Flask</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Tue, 12 May 2026 14:52:03 +0000</pubDate>
      <link>https://dev.to/descope/how-to-add-authentication-in-flask-5496</link>
      <guid>https://dev.to/descope/how-to-add-authentication-in-flask-5496</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-flask-app" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Adding &lt;a href="https://www.descope.com/learn/post/authentication" rel="noopener noreferrer"&gt;authentication&lt;/a&gt; in Flask is a key step in building secure web apps that users can trust. In this hands-on tutorial, you'll see how to create a complete Flask authentication flow using Python and simple HTML templates. We'll walk through signup, login, logout, and profile features with clear examples that you can use in your own projects. Whether you're just getting started with Flask or want to level up your app's security, this guide will help you build a solid foundation for user authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and setup
&lt;/h2&gt;

&lt;p&gt;Before diving into Flask authentication, make sure you have the basics ready. All the code for this tutorial is available in our &lt;a href="https://github.com/descope-sample-apps/flask-sample-app" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;, with installation instructions in the README.&lt;/p&gt;

&lt;p&gt;If you're new to Flask or Descope, no problem. You can follow along as you build authentication in Flask from scratch. To get started, &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; for a free account on Descope. You may also want to check out the Descope &lt;a href="https://docs.descope.com/tutorials/" rel="noopener noreferrer"&gt;Get Started docs&lt;/a&gt; and &lt;a href="https://flask.palletsprojects.com/en/2.3.x/quickstart/" rel="noopener noreferrer"&gt;Flask Quick Start guide&lt;/a&gt; for additional background.&lt;/p&gt;

&lt;p&gt;Read more: &lt;a href="https://www.descope.com/blog/post/auth-react-flask-app" rel="noopener noreferrer"&gt;Adding Descope Authentication to a React+Flask App&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Flask authentication works in this example
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we'll build authentication in Flask using Descope for handling user sessions. The app will let users sign up, sign in, log out, and view a profile page. We'll manage authentication in Flask by combining Python, HTML templates, and a simple decorator that protects routes from unauthorized access.&lt;/p&gt;

&lt;p&gt;You'll see how to set up the Descope SDK, refresh and &lt;a href="https://docs.descope.com/authorization/session-management/session-validation" rel="noopener noreferrer"&gt;validate session tokens&lt;/a&gt;, and connect your Flask routes with secure &lt;a href="https://www.descope.com/learn/post/authorization" rel="noopener noreferrer"&gt;authorization&lt;/a&gt; logic. Let's start by creating the Flask app and building the authentication decorator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Flask app and auth decorator
&lt;/h2&gt;

&lt;p&gt;To manage authentication in Flask, we'll create an authentication decorator that protects sensitive routes. This decorator checks for a valid session token and ensures only authorized users can access certain endpoints.&lt;/p&gt;

&lt;p&gt;Here's how we define the &lt;code&gt;token_required&lt;/code&gt; decorator in our Flask app:&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;token_required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# auth decorator
&lt;/span&gt;   &lt;span class="nd"&gt;@wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="n"&gt;session_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# check if token in request
&lt;/span&gt;           &lt;span class="n"&gt;auth_request&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="n"&gt;session_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# throw error
&lt;/span&gt;           &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jsonify&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;❌ invalid session token!&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# validate token
&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_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jsonify&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;❌ invalid session token!&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Code snippet: app.py file, token_required decorator&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the foundation of our Flask authentication logic. It checks for an authorization header, validates the session token using Descope, and either allows or blocks access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Build the HTML templates
&lt;/h2&gt;

&lt;p&gt;Our Flask authentication app uses simple &lt;a href="https://docs.descope.com/getting-started/html" rel="noopener noreferrer"&gt;HTML templates&lt;/a&gt; for the user interface. These templates handle login, profile display, and navigation.&lt;/p&gt;

&lt;p&gt;Start with a base HTML file that loads the Descope SDK. This makes Descope authentication features available across all pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;{% block title %} {% endblock %}&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@descope/web-component@latest/dist/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@descope/web-js-sdk@latest/dist/index.umd.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Code snippet: base.html file head&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This base template provides a clean starting point for building the authentication pages in Flask. You'll extend this file for login, profile, and other views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add Descope authentication logic
&lt;/h2&gt;

&lt;p&gt;Now let's add the authentication logic in Flask that connects our HTML templates to Descope. First, we create a &lt;code&gt;descope.js&lt;/code&gt; file to define global variables and initialize the Descope SDK.&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;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Descope&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;persistTokens&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;autoRefresh&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSessionToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Code snippet: descope.js file&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These variables make it easy to manage authentication in Flask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;projectId&lt;/code&gt; links to your Descope project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sdk&lt;/code&gt; initializes Descope and manages tokens.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionToken&lt;/code&gt; stores the user's session token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll use these in the login, profile, and other pages to check authentication status and control access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Protect routes with the auth decorator
&lt;/h2&gt;

&lt;p&gt;Once your &lt;code&gt;token_required&lt;/code&gt; decorator is in place, you can use it to secure any Flask route that requires authentication. This helps prevent unauthorized access to sensitive parts of your app.&lt;/p&gt;

&lt;p&gt;For example, here's how we protect the &lt;code&gt;/get_secret_message&lt;/code&gt; 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="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/get_secret_message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@token_required&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_secret_message&lt;/span&gt;&lt;span class="p"&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&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="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;secret_msg&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;This is the secret message. Congrats!&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;em&gt;Code snippet: app.py file, get_secret_message endpoint&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When a request hits this route, the authentication decorator checks the session token first. If the token is valid, the route logic runs. If not, the user gets a 401 error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Log in with Descope in HTML
&lt;/h2&gt;

&lt;p&gt;The login logic can be found in the &lt;code&gt;login.html&lt;/code&gt; file. We import &lt;code&gt;descope.js&lt;/code&gt; so we can access the global variables that help manage Flask authentication.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends 'base.html' %}
{% block content %}
&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{% block title %} Login {% endblock %}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{url_for('static', filename='descope.js')}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;descope-wc project-id="&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;" flow-id="sign-up-or-in"&amp;gt;&amp;lt;/descope-wc&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wcElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descope-wc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile&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;onError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onError&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how the profile logic works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{url_for('static', filename='descope.js')}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userName&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;userEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userEmail&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;secretMsg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secretMsg&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;profileContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProfileData&lt;/span&gt;&lt;span class="p"&gt;(){&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;/get_secret_message&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;// call the api endpoint from the flask server&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="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sessionToken&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// error&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonData&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;secretMsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_msg&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// error&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setNameEmail&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;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;me&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profile&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;name&lt;/span&gt;
    &lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profile&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;email&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getProfileData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setNameEmail&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sdk&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Code snippet: profile.html file scripts&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are three key takeaways from the JavaScript code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the top of every HTML page, we call &lt;code&gt;sdk.refresh()&lt;/code&gt; to jumpstart the autorefresh process and use the refresh token to get a new valid session token.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;if&lt;/code&gt; statement checks if the session token is invalid and displays the Descope login widget. If the session token is valid, the user is redirected to the profile page.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;onSuccess&lt;/code&gt; arrow function captures the login event, calls &lt;code&gt;sdk.refresh()&lt;/code&gt; to start the autorefresh process, and redirects the user to the profile page, where we validate the session token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's see how we display our profile information in the &lt;code&gt;profile.html&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Display the profile page
&lt;/h2&gt;

&lt;p&gt;After logging in, users can access the profile page. This page displays user details and a secret message retrieved securely through authentication in Flask.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;       &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sdk&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Code snippet: profile.html file scripts&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This ensures the user session ends cleanly. You can connect this function to a logout button in your profile template. Logging out removes the session token, preventing unauthorized access if someone tries to revisit a protected page.&lt;/p&gt;

&lt;p&gt;Notice how &lt;code&gt;sdk.refresh()&lt;/code&gt; is called at the top of the script. This jumpstarts the auto-refresh process, using the refresh token to get a new valid session token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Run and test Flask authentication in your app
&lt;/h2&gt;

&lt;p&gt;With all the pieces in place, you can run your Flask app and test the authentication flow. Start your server and visit 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%2Fnc0aou2or14i0mie37lb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnc0aou2or14i0mie37lb.png" alt="Fig: Home page" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After login, users are redirected to the profile page where their name, email, and secret message appear.&lt;/p&gt;

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

&lt;p&gt;The profile page showcases our name, email, logout button, and a link back to the home page. The logout button ends the session and returns users to the login screen.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Flask authentication in action
&lt;/h2&gt;

&lt;p&gt;This example shows how authentication in Flask can be simple and secure using Python, HTML, and Descope.&lt;/p&gt;

&lt;p&gt;With just Python, HTML, and Descope, you've created a working Flask authentication system that handles signup, login, logout, and profile access. This simple approach to authentication in Flask helps you build secure apps without adding unnecessary complexity.&lt;/p&gt;

&lt;p&gt;If you're ready to explore more or want to scale your Flask authentication setup, &lt;a href="https://www.descope.com" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; for Descope or &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt; with our auth experts to learn more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Flask use JWT?
&lt;/h2&gt;

&lt;p&gt;Flask itself doesn't use &lt;a href="https://www.descope.com/learn/post/jwt" rel="noopener noreferrer"&gt;JWTs (JSON Web Tokens)&lt;/a&gt; by default. It's a lightweight web framework that lets you choose how to handle authentication and &lt;a href="https://www.descope.com/learn/post/session-management" rel="noopener noreferrer"&gt;session management&lt;/a&gt;. If you want to use JWTs in Flask, you can easily integrate it with libraries like PyJWT, Flask-JWT-Extended, or third-party services like &lt;a href="https://www.descope.com/product" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial, we showed how to handle authentication in Flask using Descope, which manages session tokens (including JWTs) for you securely behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Flask authentication secure?
&lt;/h2&gt;

&lt;p&gt;Flask authentication can be very secure, but it depends on how you implement it. Since Flask is a flexible framework, you're responsible for choosing secure methods for handling login, tokens, and session management.&lt;/p&gt;

&lt;p&gt;In this tutorial, we showed how to add authentication in Flask using Descope, which helps you securely manage tokens and user sessions. If you follow best practices like protecting routes, validating tokens, and using HTTPS, Flask apps can provide &lt;a href="https://www.descope.com/learn/post/strong-authentication" rel="noopener noreferrer"&gt;strong authentication&lt;/a&gt; for your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Flask as secure as Django?
&lt;/h2&gt;

&lt;p&gt;Flask and Django can both be secure, but they take different approaches. Django comes with more built-in security features like CSRF protection, form validation, and user authentication out of the box. Flask is more lightweight and gives you the flexibility to choose and implement your own security tools.&lt;/p&gt;

&lt;p&gt;This means Flask can be just as secure as Django if you follow best practices.&lt;/p&gt;

&lt;p&gt;Read more: &lt;a href="https://www.descope.com/blog/post/auth-django-app" rel="noopener noreferrer"&gt;Setting Up Django Auth With Descope&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build Secure Multi-Agent Systems With CrewAI and Descope</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 06 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/build-secure-multi-agent-systems-with-crewai-and-descope-51e3</link>
      <guid>https://dev.to/descope/build-secure-multi-agent-systems-with-crewai-and-descope-51e3</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/crewai-multi-agent" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A single AI agent can summarize, analyze, or plan, but it struggles to scale across domains, maintain context, or specialize deeply enough for complex enterprise use cases. Multi-agent systems address these gaps by distributing responsibility across many specialized agents. Instead of asking one model to do everything, individual agents receive a defined role and scope. Each agent executes its part before results are stitched together. This avoids overload, reduces errors, and produces better outcomes than a single agent working alone.&lt;/p&gt;

&lt;p&gt;Case in point: The &lt;a href="https://www.crewai.com/" rel="noopener noreferrer"&gt;CrewAI&lt;/a&gt; multi-agent platform structures multi-agent systems much like real-world teams. This design keeps workflows clear, predictable, and scalable while exposing the practical challenge of managing identity and access. But once you've got agents interacting, you need to think about safe and reliable orchestration: authentication, identity, permissions, and secure communication.&lt;/p&gt;

&lt;p&gt;Enter the Descope &lt;a href="https://www.descope.com/use-cases/ai" rel="noopener noreferrer"&gt;Agentic Identity Hub&lt;/a&gt;, which helps you manage identity and access control for all agents in such a multi-agent system. Each agent can be managed and assigned the relevant permissions to perform exactly the tasks they are designed for and nothing more. Let's put this in practice!&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Descope for a CrewAI Application
&lt;/h2&gt;

&lt;p&gt;To make this concrete, there's a working example that ties everything together. You can follow the steps in this tutorial, or check out the finished &lt;a href="https://github.com/descope-sample-apps/crewai-app" rel="noopener noreferrer"&gt;sample app&lt;/a&gt; to see the complete implementation in action.&lt;/p&gt;

&lt;p&gt;Here's what we'll be implementing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CrewAI crew with two different agents corresponding to two independent tasks: Calendar and Contacts&lt;/li&gt;
&lt;li&gt;Descope Outbound apps for each API with defined OAuth scopes. Each agent will have its own scope and will only be able to perform actions based on these scopes.&lt;/li&gt;
&lt;li&gt;User consent flows for granular permissioning&lt;/li&gt;
&lt;li&gt;Backend session validation and secure token exchange&lt;/li&gt;
&lt;li&gt;A unified output from the crew that combines results from both agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch the video below or carry on reading!&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You'll need the following tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Descope account: If you don't already have one, you can sign up for a Free Forever account.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://console.cloud.google.com/apis/library" rel="noopener noreferrer"&gt;Google Cloud APIs&lt;/a&gt;: Calendar and People/Contacts APIs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.crewai.com/en/installation" rel="noopener noreferrer"&gt;CrewAI&lt;/a&gt;: Python framework for multi-agent workflows&lt;/li&gt;
&lt;li&gt;Python HTTP library: For API requests (&lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;A simple React frontend to connect the backend to&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure Google OAuth credentials
&lt;/h2&gt;

&lt;p&gt;For using google contact and calendar APIs, you need to &lt;a href="https://developers.google.com/identity/protocols/oauth2" rel="noopener noreferrer"&gt;set up google OAuth credentials&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create OAuth credentials in the Google Cloud Console for the APIs your agents will access.&lt;/li&gt;
&lt;li&gt;Go to your Google Cloud Console &amp;gt; APIs &amp;amp; Services &amp;gt; Credentials.&lt;/li&gt;
&lt;li&gt;Create a new OAuth Client ID for each service: one for Calendar, one for Contacts.&lt;/li&gt;
&lt;li&gt;Copy the Client ID and Client Secret for each app.&lt;/li&gt;
&lt;li&gt;Add Authorized redirect URIs pointing back to your Descope Outbound app configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Add Outbound Apps in Descope
&lt;/h2&gt;

&lt;p&gt;Next, configure the outbound apps inside Descope so that each agent can request access tokens for its task. In the Descope Console, go to Outbound Apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add Google Calendar and Google Contacts as new apps.&lt;/li&gt;
&lt;li&gt;Paste the Client ID and Client Secret from Google for each app.&lt;/li&gt;
&lt;li&gt;Set the redirect URIs to match the Google configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://docs.descope.com/identity-federation/outbound-apps" rel="noopener noreferrer"&gt;Descope docs&lt;/a&gt; provide more detailed instructions on how to set up outbound applications. While setting up outbound applications, pay attention to scopes. Scopes determine exactly what each agent can access. Configuring scopes in Descope rather than in code makes it easy to audit and update them later.&lt;/p&gt;

&lt;p&gt;For this tutorial, we need two sets of scopes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calendar app&lt;/strong&gt;: &lt;code&gt;https://www.googleapis.com/auth/calendar.readonly&lt;/code&gt; and &lt;code&gt;https://www.googleapis.com/auth/calendar&lt;/code&gt; (so the calendar agent can read existing events and create new ones)&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Contacts app&lt;/strong&gt;: &lt;code&gt;https://www.googleapis.com/auth/contacts.readonly&lt;/code&gt; (so the contacts agent can look up user contacts but not modify them)&lt;/p&gt;

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

&lt;p&gt;Keeping scopes granular ensures each task is limited to its responsibilities and prevents over-permissioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Descope Inbound Application
&lt;/h2&gt;

&lt;p&gt;Configure an &lt;a href="https://docs.descope.com/identity-federation/inbound-apps" rel="noopener noreferrer"&gt;Inbound App&lt;/a&gt; in your Descope console. The inbound app protects the front end and also uses the consent flow so that the user can grant consent. The token granted by inbound applications is used by the application backend to securely exchange and fetch outbound app tokens:&lt;/p&gt;

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

&lt;p&gt;Descope already has a template flow that can be used for inbound applications. Start with that and customize as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insert a consent screen showing requested scopes for Calendar and Contacts.&lt;/li&gt;
&lt;li&gt;Consent is stored for auditability and future requests.&lt;/li&gt;
&lt;li&gt;Add two Outbound App Connection actions, one for each app. These are required to connect google calendar and contacts during the user login process.&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%2Fm0a0z4qxjwkb0ye7siix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0a0z4qxjwkb0ye7siix.png" alt="Fig: Snippet of Descope Flow with consent and Outbound App actions" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend authentication
&lt;/h2&gt;

&lt;p&gt;When a user signs in through the inbound app, they're guided through the customized Descope Flow. This flow not only authenticates the user but also presents a consent screen showing the requested Google Calendar and Contacts scopes. Once the user approves, Descope connects the outbound apps behind the scenes, tying the user's identity to the exact API permissions required.&lt;/p&gt;

&lt;p&gt;The result is a session token returned to the frontend. This token represents a verified user identity and their granted scopes. Every request from the frontend to the backend should include this session token, ensuring that the backend can validate the user and securely exchange it for outbound access tokens later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure CrewAI tasks
&lt;/h2&gt;

&lt;p&gt;With authentication and token management in place, the CrewAI application can now operate as a coordinated multi-agent system. In this setup, we've defined a crew that manages two independent agents, each with a distinct role and scope.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Calendar Task&lt;/strong&gt; is handled by the &lt;code&gt;calendar_manager&lt;/code&gt; agent, which uses the Google Calendar access token to read or create events. This agent specializes in interpreting user intent around time and scheduling, ensuring that calendar operations are executed precisely.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Contacts Task&lt;/strong&gt; is handled by the &lt;code&gt;contacts_finder&lt;/code&gt; agent, which uses the Google Contacts access token to search and retrieve detailed contact information. This agent is optimized for matching names, emails, or partial queries, and always returns structured, well-formatted results.&lt;/p&gt;

&lt;p&gt;Each task runs in isolation, bound by the specific OAuth scopes granted through Descope. This means the calendar agent cannot access contacts, and the contacts agent cannot alter calendar events. The crew oversees the process, coordinating the agents' outputs and merging them into a unified result for the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend session validation
&lt;/h2&gt;

&lt;p&gt;On the backend, validate the Descope session token using the Descope SDK. Verify the token and extract the trusted &lt;code&gt;userId&lt;/code&gt; and session information. Only validated sessions can request Outbound App access tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_token&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;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_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VITE_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&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;Successfully validated user session:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwt_response&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;jwt_response&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&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="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;Could not validate user session. Error:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&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;h2&gt;
  
  
  Exchange session for access tokens
&lt;/h2&gt;

&lt;p&gt;Once the session is validated, the backend exchanges it for scoped tokens for each Outbound App:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calendar access token scoped to Calendar app permissions.&lt;/li&gt;
&lt;li&gt;Contacts access token scoped to Contacts app permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Descope handles OAuth flows and refresh logic, so the backend receives short-lived, scoped tokens without needing to store refresh tokens directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_outbound_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch Google Calendar access token from Descope outbound token API.&lt;/span&gt;&lt;span class="sh"&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VITE_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;management_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_MANAGEMENT_KEY&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.descope.com/v1/mgmt/outbound/app/user/token/latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payload&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;appId&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_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&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_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&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;response&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="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to fetch token: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="n"&gt;token_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_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;accessToken&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;access_token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Orchestrating agent security with Descope
&lt;/h2&gt;

&lt;p&gt;With this setup, you have a secure, multi-agent CrewAI workflow. Each agent operates with least-privilege access, user consent is auditable, and the crew manager produces a unified output efficiently. To explore further or start experimenting right away, the &lt;a href="https://github.com/descope-sample-apps/crewai-app" rel="noopener noreferrer"&gt;sample app repo&lt;/a&gt; contains the complete codebase and configuration.&lt;/p&gt;

&lt;p&gt;From here, you can start applying the same pattern to real use cases—for example, a sales workflow where one agent books meetings on Google Calendar while another enriches leads from your CRM, or a support workflow where an agent pulls customer details and another schedules follow-ups automatically. You might also explore adding new agents tied to other APIs, integrating project management or finance systems, or experimenting with CrewAI's orchestration features for more complex collaborations.&lt;/p&gt;

&lt;p&gt;In short, this foundation gives you a practical way to scale multi-agent systems securely, while Descope handles the identity and access challenges behind the scenes.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>Secure API Calling With Custom GPTs and Descope</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 04 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/secure-api-calling-with-custom-gpts-and-descope-iig</link>
      <guid>https://dev.to/descope/secure-api-calling-with-custom-gpts-and-descope-iig</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/api-calling-custom-gpts" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;OpenAI's Custom GPTs offer a powerful way to create AI agents that can interact directly with your APIs through natural language conversations. Imagine you have a deployed FastAPI application that implements DevOps tools such as triggering CI/CD workflows, getting deployment logs, usage analytics, and other operational tasks. While integrating your API with an LLM sounds like a perfect task for a &lt;a href="https://www.descope.com/learn/post/mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; server, MCP servers can be complex to implement and maintain, often posing security challenges that necessitate external middleware. Even FastAPI MCP wrappers introduce routing changes, CORS policies, and deployment overhead that quickly becomes its own project.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://docs.descope.com/identity-federation/inbound-apps" rel="noopener noreferrer"&gt;Descope Inbound Apps&lt;/a&gt;, you're given a much simpler approach to establishing an OAuth compliant connection between your API and a custom GPT. In this article, we'll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Securing your FastAPI backend with Descope JWT validation&lt;/li&gt;
&lt;li&gt;Configuring a custom GPT to authenticate and interact with your protected APIs using scope-based authorization&lt;/li&gt;
&lt;li&gt;Setting up your FastAPI application to act as a proxy for the Inbound App, so you maintain the authorization and the resource server under the same domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also read: &lt;a href="https://docs.descope.com/mcp" rel="noopener noreferrer"&gt;Descope MCP documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build: a secure GPT assistant
&lt;/h2&gt;

&lt;p&gt;By the end of this article, you will have a custom GPT that can securely interact with your prebuilt FastAPI endpoints through natural language conversations. The first time you submit a prompt in the current chat, you will be asked to authenticate and taken through the Descope inbound apps user consent flow. Note that if you configure a custom domain for your Descope project, your Descope-powered authorization server would run on your own custom domain, eliminating the need for this proxy setup. However, for this example, we will implement the proxy approach to understand the OAuth flow mechanics and provide an immediate out-of-the-box solution.&lt;/p&gt;

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

&lt;p&gt;Once you are authenticated, you will be brought to the consent screen, which will display a list of scopes that you can consent to.&lt;/p&gt;

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

&lt;p&gt;After you give consent, you will be seamlessly redirected back to your chat along with access and refresh tokens, which will be used when you call your API routes. The GPT will then ask for authorization to interface with the specific tool(s) which will be called to answer your prompt. Select Allow or Allow Always based on your personal preference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql5ri4lr91fa8rcy77mx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql5ri4lr91fa8rcy77mx.png" alt="Fig: GPT asks for consent to interface with your API" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you make a request, your custom GPT sends it directly to your API with the access and refresh tokens it has. Your backend server then performs the scope validation and responds accordingly: if the token is invalid or expired, you'll get a 401 Unauthorized error; if the token is valid but lacks the necessary scopes for that specific endpoint, you'll receive a 403 Forbidden error. This approach keeps all the authorization decisions on the server side. In either case, the GPT will explain in natural language which error occurred when trying to generate a response. After you complete authentication and authorization, OpenAI will automatically manage your session. You can view and manually manage the connection and authorization status under Privacy Settings, which you can find in the drop-down menu next to the name of your GPT in the top left corner.&lt;/p&gt;

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

&lt;p&gt;You can continue to submit prompts without reauthenticating until the authorization token issued to you by the Inbound App expires. The default session timeout is set to 10 minutes, but you can learn to adjust this later in the article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;Python installed&lt;/li&gt;
&lt;li&gt;A deployed FastAPI application; you can find a sample starter app &lt;a href="https://github.com/descope-sample-apps/descope-fastapi-sample-app" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ChatGPT Plus account&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;To create a new Descope project, &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; or &lt;a href="https://app.descope.com/" rel="noopener noreferrer"&gt;sign in&lt;/a&gt; to the Descope console. If you just signed up for a new account, a new Descope project will be automatically created for you and you will find yourself on the &lt;a href="https://app.descope.com/gettingStarted" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; page of that project. To create a new project, click on your project's name in the top left corner of your Descope console, and select + Project from the dropdown. This is also where you can switch between projects. If you navigate to the &lt;a href="https://app.descope.com/flows" rel="noopener noreferrer"&gt;Flows&lt;/a&gt; section, you will see that a handful of flows have automatically been created for you. &lt;a href="https://docs.descope.com/flows" rel="noopener noreferrer"&gt;Descope Flows&lt;/a&gt; are a no-code tool to build user authentication journeys. While you do not need to modify the default flows, you can choose to do so if you want to design a custom user journey.&lt;/p&gt;

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

&lt;p&gt;You can access your project ID at any time by navigating to the &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;Project&lt;/a&gt; section of the Descope console. You can copy the project ID from the General tab.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Configuring your application environment
&lt;/h2&gt;

&lt;p&gt;In the root folder of your FastAPI application's project, create a &lt;code&gt;.env&lt;/code&gt; file if you do not already have one, and add the following variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;Your Descope project ID&amp;gt; 
&lt;span class="nv"&gt;DESCOPE_INBOUND_APP_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;Inbound App client ID&amp;gt;
&lt;span class="nv"&gt;DESCOPE_INBOUND_APP_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;Inbound App client secret&amp;gt;
&lt;span class="nv"&gt;DESCOPE_API_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com"&lt;/span&gt; &lt;span class="c"&gt;# or your custom domain if one is configured in your project settings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specify your Descope project ID. If you do not know where to find it, reference the previous section of this article. You will fill in the Inbound App client ID and secret later in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing your app through token validation
&lt;/h2&gt;

&lt;p&gt;Descope implements its &lt;a href="https://docs.descope.com/authorization/session-management" rel="noopener noreferrer"&gt;session and refresh tokens&lt;/a&gt; as JSON web tokens (JWT). After your Custom GPT obtains a token from your Inbound App through the OAuth flow, your FastAPI backend needs to validate and accept these scoped tokens for authentication and authorization. The advantage of using this OAuth approach with Descope is that you get a complete consent flow and hosted login pages out of the box - no need to build your own authentication UI. You will implement a &lt;a href="https://docs.descope.com/authorization/session-management/session-validation/oidc-jwt-authorizers" rel="noopener noreferrer"&gt;custom JWT authorizer&lt;/a&gt; into your FastAPI app that validates JWTs to make sure its signature is valid, it is not expired, and the audience and issuer claims match the expected resource server. Finally, the authorizer will enforce the scopes embedded in the token to control access to your API endpoints.&lt;/p&gt;

&lt;p&gt;First, you will implement a simple exception handler. In your FastAPI application, create a new file and name it &lt;code&gt;exceptions.py&lt;/code&gt;. Add these two exception definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnauthenticatedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authentication required&lt;/span&gt;&lt;span class="sh"&gt;"&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;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not authorized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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_403_FORBIDDEN&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="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's implement the JWT authorizer. To validate the JWT, you will need to obtain a public key from the Descope JSON web key set (JWKS) endpoint. Your JWKS endpoint will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.descope.com/&amp;lt;Your Descope Project ID&amp;gt;/.well-known/jwks.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an &lt;code&gt;auth.py&lt;/code&gt; file in your FastAPI app and add a &lt;code&gt;TokenVerifier&lt;/code&gt; class. This class is used as a FastAPI dependency to validate incoming JWTs and, if requested by a route, enforce OAuth scopes.&lt;/p&gt;

&lt;p&gt;At a high level, the &lt;code&gt;TokenVerifier&lt;/code&gt; class extracts the bearer token from the authorization header, fetches the public key from your JWKS endpoint, and decodes and validates the JWT. It also uses the &lt;code&gt;SecurityScopes&lt;/code&gt; library to validate and enforce the scopes embedded in the 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PyJWKClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SecurityScopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPAuthorizationCredentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPBearer&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UnauthenticatedException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UnauthorizedException&lt;/span&gt;

&lt;span class="n"&gt;jwks_url&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;https://api.descope.com/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_PROJECT_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/.well-known/jwks.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenVerifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_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;jwks_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PyJWKClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwks_url&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;allowed_algorithms&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;RS256&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;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;security_scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SecurityScopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# token injected by FastAPI Security, specified in the FastAPI route definition
&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;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HTTPAuthorizationCredentials&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HTTPBearer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;UnauthenticatedException&lt;/span&gt;

        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;

        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get_signing_key&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_decode_token&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;key&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;security_scopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_enforce_scopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;security_scopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&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;payload&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get_signing_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&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;try&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;jwks_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_signing_key_from_jwt&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;key&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&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;Failed to fetch signing key: &lt;/span&gt;&lt;span class="si"&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;e&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="c1"&gt;# helper which calls jwt.decode()
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_decode_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;token&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;key&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;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;issuer_candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.descope.com/v1/apps/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&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;project_id&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;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;algorithms&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;allowed_algorithms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;issuer_candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token decoding failed: &lt;/span&gt;&lt;span class="si"&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;e&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To restrict access to specific API routes based on the scopes carried by the incoming JWT token, add one more method to the &lt;code&gt;TokenVerifier&lt;/code&gt; class that checks the token's scope claim. After successful validation, it reads the token's &lt;code&gt;scope&lt;/code&gt; claim, compares it to the route's required scopes, and rejects the request if any are missing.&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;_enforce_scopes&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;payload&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="n"&gt;required_scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&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;scope_claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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;scope&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;scope_claim&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Missing required claim: &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;scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope_claim&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="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;scope_claim&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;else&lt;/span&gt; &lt;span class="n"&gt;scope_claim&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required_scopes&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scope&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;scopes&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;missing&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;UnauthorizedException&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;Missing required scopes: &lt;/span&gt;&lt;span class="si"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's hook up our JWT authorizer to the main file where you define your APIs. You can protect your API routes with token validation and/or scoping, restricting specific routes to tokens which have specific scopes. While scoping isn't mandatory for basic authentication, it's highly recommended when working with custom GPTs. This is because they handle 403 error responses fairly seamlessly, and they can gracefully inform users when they lack permissions. The scopes embedded in your access token will be set when creating your Descope Inbound App later in this article. For more insights on implementing robust security controls in enterprise applications, check out our &lt;a href="https://www.descope.com/blog/post/enterprise-mcp#challenge-#3:-scopes-and-permissions" rel="noopener noreferrer"&gt;enterprise MCP security challenges blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You may already have route protection configured, but if you do not, set it up 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;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TokenVerifier&lt;/span&gt;

&lt;span class="c1"&gt;# Set a custom User-Agent to avoid being blocked by security filters or rate limiters.
&lt;/span&gt;&lt;span class="n"&gt;opener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nf"&gt;build_opener&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;opener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addheaders&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;User-agent&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;Mozilla/5.0 (DescopeFastAPISampleApp)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;urllib&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="nf"&gt;install_opener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TokenVerifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 

&lt;span class="nd"&gt;@app.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;/api/private&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;private&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth_result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Security&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="c1"&gt;# This API is now protected by our TokenVerifier object `auth`
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;auth_result&lt;/span&gt;

&lt;span class="nd"&gt;@app.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;/api/private-scoped/read&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;private_scoped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth_result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Security&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;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;read:messages&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;
    This is a protected route with scope-based access control.

    Access to this endpoint requires:
    - A valid access token (authentication), and
    - The presence of the `read:messages` scope in the token.
    &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;auth_result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now have a working JWT authorizer implemented with your FastAPI app! For more detailed information on the functionality of this implementation, you can visit our &lt;a href="https://docs.descope.com/authorization/session-management/session-validation/oidc-jwt-authorizers/python-fastapi-jwt-authorizer" rel="noopener noreferrer"&gt;Validating JWTs in FastAPI doc.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring your FastAPI app to be an authorization server
&lt;/h2&gt;

&lt;p&gt;Because the GPT requires the OAuth endpoints and the APIs to be hosted under the same domain name, you will use your FastAPI app as a proxy or middleware between the client application (Custom GPT) and the identity provider (Descope). If you are using a custom domain that's the same root domain as your FastAPI backend, you can skip this step.&lt;/p&gt;

&lt;p&gt;You will expose three endpoints that your custom GPT will treat as its OAuth server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /authorize&lt;/code&gt; – forwards the browser to Descope's &lt;code&gt;/authorize&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/oauth/callback&lt;/code&gt; – receives the authorization response from Descope and transforms it into the format expected by the custom GPT, including proper state parameter preservation and error handling for a seamless user experience.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /token&lt;/code&gt; – forwards the GPT's back-channel token exchange to Descope's &lt;code&gt;/token&lt;/code&gt; endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authorization endpoint
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/authorize&lt;/code&gt; endpoint acts as the first step in the OAuth authorization code flow. When you initiate authentication through your custom GPT, this endpoint receives the OAuth parameters including &lt;code&gt;response_type&lt;/code&gt;, &lt;code&gt;redirect_uri&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, and &lt;code&gt;state&lt;/code&gt;. The endpoint performs several critical functions to ensure a secure OAuth flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validates the incoming request parameters to ensure OAuth compliance and prevents malformed or malicious requests.&lt;/li&gt;
&lt;li&gt;Dynamically constructs the callback URL based on the request's host, providing flexibility for different deployment environments.&lt;/li&gt;
&lt;li&gt;Transforms the request into Descope's expected format by adding the necessary client credentials and redirect URI.&lt;/li&gt;
&lt;li&gt;Redirects you to Descope's authorization server, where you can authenticate and authorize the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This proxy architecture provides several key benefits: centralized control over the OAuth flow, enhanced security through request validation and sanitization, and the flexibility to add custom logging and error handling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.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;/authorize&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;authorize&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;response_type&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    OAuth 2.0 Authorization Endpoint - Proxies to Descope

    This endpoint forwards OAuth authorization requests to Descope&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s Inbound Apps.
    &lt;/span&gt;&lt;span class="sh"&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;# Validate required parameters
&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;redirect_uri&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response_type&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing required parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="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="mi"&gt;400&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Validate response_type
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported response_type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response_type&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;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="mi"&gt;400&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Get client ID from environment or use the provided one
&lt;/span&gt;        &lt;span class="n"&gt;descope_client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_INBOUND_APP_CLIENT_ID&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;descope_client_id&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;OAuth client credentials not configured&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="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="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Get the base URL from the request
&lt;/span&gt;        &lt;span class="n"&gt;base_url&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="n"&gt;callback_url&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/oauth/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="c1"&gt;# Construct query parameters
&lt;/span&gt;        &lt;span class="n"&gt;params&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;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;descope_client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirect_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_type&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;code&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;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;scope&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openid&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;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;  &lt;span class="c1"&gt;# Just pass through the state parameter
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Build the full URL with query parameters
&lt;/span&gt;        &lt;span class="n"&gt;query_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
        &lt;span class="n"&gt;full_url&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;https://api.descope.com/oauth2/v1/apps/authorize?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query_string&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;Redirecting to Descope: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;full_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Redirect to Descope's authorization endpoint
&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;full_url&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="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&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="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;Authorization endpoint error: &lt;/span&gt;&lt;span class="si"&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;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="mi"&gt;500&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Token exchange endpoint
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/token&lt;/code&gt; endpoint performs the OAuth 2.0 token exchange process, handling the conversion of authorization codes into access tokens. When your custom GPT sends a token exchange request, this endpoint manages the complete flow from validation to token retrieval. The endpoint handles several key operations during the token exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses the incoming request body containing the authorization code, client credentials, and grant type from your custom GPT.&lt;/li&gt;
&lt;li&gt;Validates all required parameters including the grant type and client credentials to ensure the request meets OAuth 2.0 standards.&lt;/li&gt;
&lt;li&gt;Dynamically constructs the redirect URI to match the original authorization request, maintaining consistency across the flow.&lt;/li&gt;
&lt;li&gt;Forwards the validated request to Descope's token server with proper client credentials and authorization code, acting as a trusted intermediary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This centralized architecture enables detailed request/response logging for debugging, allows for custom validation and transformation of the OAuth flow, and maintains full compatibility with standard OAuth implementations while giving you complete control over the token exchange process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/token&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;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;Request&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    OAuth 2.0 Token Endpoint - Proxies to Descope

    This endpoint forwards token exchange requests to Descope&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s Inbound Apps.
    &lt;/span&gt;&lt;span class="sh"&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;Token exchange request received&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Parse the request body based on content type
&lt;/span&gt;        &lt;span class="n"&gt;content_type&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;headers&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;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="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;Request content-type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;content_type&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;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&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;content_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/x-www-form-urlencoded&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;content_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;form_data&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;body&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;form_data&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;# Try to parse as JSON first, then as form data
&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;body&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;form_data&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;body&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;form_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;grant_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&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;grant_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&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;code&lt;/span&gt;&lt;span class="sh"&gt;"&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_INBOUND_APP_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;client_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DESCOPE_INBOUND_APP_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Validate required parameters
&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;grant_type&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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="mi"&gt;400&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Only support authorization_code grant type
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;grant_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="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="mi"&gt;400&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Get the base URL from the request
&lt;/span&gt;        &lt;span class="n"&gt;base_url&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="n"&gt;callback_url&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/oauth/callback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="c1"&gt;# Forward the request to Descope's token endpoint
&lt;/span&gt;        &lt;span class="n"&gt;token_request_body&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;grant_type&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;authorization_code&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;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;client_secret&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="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirect_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;callback_url&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;descope_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;https://api.descope.com/oauth2/v1/apps/token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&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;client&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;Sending request to Descope token endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;response&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;client&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="n"&gt;descope_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_request_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/x-www-form-urlencoded&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;descope_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="c1"&gt;# If Descope returned an error, log it
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&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;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Descope token exchange failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;descope_data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Return the response from Descope
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;descope_data&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&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="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;Token endpoint error: &lt;/span&gt;&lt;span class="si"&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;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="mi"&gt;500&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Callback endpoint
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/api/oauth/callback&lt;/code&gt; endpoint serves as the bridge in the OAuth 2.0 authorization code flow, receiving the authorization response from Descope and transforming it into the format expected by your custom GPT. When Descope redirects the user's browser back to this endpoint, it performs several critical operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extracts the authorization code and state parameter from Descope's callback response.&lt;/li&gt;
&lt;li&gt;Validates the received parameters and handles any OAuth errors returned by Descope during the authorization process.&lt;/li&gt;
&lt;li&gt;Constructs a properly formatted redirect URL that includes the authorization code and preserved state parameter in the format your custom GPT expects.&lt;/li&gt;
&lt;li&gt;Issues a 307 Temporary Redirect response to the user's browser, which automatically follows the redirect to your custom GPT's callback URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 307 redirection mechanism is crucial because it preserves the HTTP method and request body while ensuring that your custom GPT receives the authorization data in exactly the format it needs to complete the token exchange.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.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;/api/oauth/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;oauth_callback&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;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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;error_description&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    OAuth 2.0 Callback Endpoint

    Handles the callback from Descope and redirects back to Custom GPT.
    &lt;/span&gt;&lt;span class="sh"&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;# Handle errors from Descope
&lt;/span&gt;        &lt;span class="k"&gt;if&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="mi"&gt;400&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="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="n"&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;error_description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error_description&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;code&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="mi"&gt;400&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="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;invalid_request&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;error_description&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;No authorization code received&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;# Custom GPT callback URL - obtained after creating your GPT
&lt;/span&gt;        &lt;span class="n"&gt;custom_gpt_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;Your GPT callback URL&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="c1"&gt;# Build redirect URL back to Custom GPT
&lt;/span&gt;        &lt;span class="n"&gt;redirect_url&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;custom_gpt_callback&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?code=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;redirect_url&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;state=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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_url&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="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&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="mi"&gt;500&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="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;server_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;error_description&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;Internal server error&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;h2&gt;
  
  
  Setting up an Inbound App
&lt;/h2&gt;

&lt;p&gt;Now we will set up the Descope part of this backend by creating an Inbound App. Inbound Apps enable you to turn your application into an identity provider (IdP). The Inbound App will handle the OAuth flow, endpoints, and consent mechanisms for you, allowing you to easily manage authentication and define granular permission scopes at the user or tenant level. When you configure your custom GPT, it will use a scoped OAuth token generated by your Inbound App to access the APIs that it is authorized to access.&lt;/p&gt;

&lt;p&gt;To create a new Descope Inbound App, navigate to the &lt;a href="https://app.descope.com/apps/inbound" rel="noopener noreferrer"&gt;Inbound Apps&lt;/a&gt; page of your Descope console. Select the blue + Inbound App button on the right side of the screen, and give your new Inbound App a name and optional description.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Scoping your Inbound App
&lt;/h3&gt;

&lt;p&gt;After you create your Inbound App, you will be able to see all the settings and details. If you scroll down to the Scopes section, this is where you will define what scopes will be embedded in the JWT token provided to your application. The scope names you set here should exactly match the scopes that are required by the API routes that you wish to give your token access to.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Client ID and secret
&lt;/h3&gt;

&lt;p&gt;Next, scroll down to the Connection Information section, and here you will see the configuration data needed to integrate your backend app with the Inbound App. For example, notice the Authorization URL and Token URL are the same URLs that we routed to in the &lt;code&gt;/authorize&lt;/code&gt; and &lt;code&gt;/token&lt;/code&gt; routes of our FastAPI app. The most important values here are the Client ID and Client Secret. Copy and paste those values into your &lt;code&gt;.env&lt;/code&gt; file for your &lt;code&gt;DESCOPE_INBOUND_APP_CLIENT_ID&lt;/code&gt; and &lt;code&gt;DESCOPE_INBOUND_APP_CLIENT_SECRET&lt;/code&gt; variables, respectively.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  User consent flow
&lt;/h3&gt;

&lt;p&gt;Notice also the Flow Hosting URL. This URL points to the exact Descope flow that will be used to define the authentication and consent user journey. If you navigate over to the Flows section of the Descope console, you will see that once you created your first Inbound App, Inbound App consent flows were also automatically created for you. The default user consent flow was the one demonstrated at the beginning of this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom session token expiration
&lt;/h3&gt;

&lt;p&gt;By default, the session token issued by your Inbound App will expire 10 minutes after the time of issuance. If you want to adjust this, scroll down to the last section of the Inbound App settings, called Session Management. Select the Custom option, and then modify the Session Token Timeout.&lt;/p&gt;

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

&lt;p&gt;Your Inbound App is now fully configured and ready to use!&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring your custom GPT
&lt;/h2&gt;

&lt;p&gt;GPTs enable you to create a tailored version of ChatGPT based on custom instructions, actions, or context. For this article, we are going to be configuring a GPT which will act as your personal DevOps assistant. We will essentially be building an AI agent, but one that knows how to interact with your specific APIs. To create a new GPT, open the ChatGPT homepage and select the &lt;a href="https://chatgpt.com/gpts" rel="noopener noreferrer"&gt;GPTs tab&lt;/a&gt; in the left sidebar. This will bring you to the homepage of existing publicly available GPTs. Select the + Create button in the upper right corner.&lt;/p&gt;

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

&lt;p&gt;Select the Configure tab on your GPT. Enter a name for your GPT, and optionally provide an icon, description, instructions, prompt starters, and knowledge (extra context) on this 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%2Fxr3e6cmcyuvf497fc5oz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr3e6cmcyuvf497fc5oz.png" alt="Fig: GPT configure page" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring OAuth for your GPT
&lt;/h3&gt;

&lt;p&gt;Scroll all the way down to the bottom of the Configure page, and under Actions, select the Create new action button. This action will be the mechanism through which your GPT will interface with your application. Once on the action definition page, click on the Authentication dropdown menu and select OAuth as your authentication type. This will open up a new window, where you will configure the OAuth connection between this GPT and your API. You will need to specify the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authorization URL&lt;/strong&gt;: this is your API route that handles the initial call to the Descope authorize endpoint (e.g. &lt;code&gt;app/authorize&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token URL&lt;/strong&gt;: this is the API route that handles the code and token exchange at the Descope token endpoint (e.g. &lt;code&gt;app/token&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Exchange Method&lt;/strong&gt;: Make sure Default (POST request) is selected. Leave the Client ID, Client Secret, and Scope fields blank. Your backend application will take care of these parts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click Save once you have filled out the appropriate fields.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Creating an OpenAPI spec
&lt;/h3&gt;

&lt;p&gt;Once you have configured the OAuth credentials, you will need to provide an OpenAPI 3.1.0 spec in the textbox titled Schema. An OpenAPI spec is a document written in a machine readable format like YAML or JSON that describes your API endpoints and how to call them with the appropriate parameters, request/response formats, authentication methods, and other relevant information. For an example of an OpenAPI spec in either a JSON or YAML format, visit &lt;a href="https://github.com/descope-sample-apps/descope-fastapi-sample-app" rel="noopener noreferrer"&gt;this Descope sample app&lt;/a&gt;. FastAPI easily generates a JSON file for you, simply enter the URL where your application is hosted followed by &lt;code&gt;/openapi.json&lt;/code&gt;. If you utilize this option, you will have to add a &lt;code&gt;servers&lt;/code&gt; object, where you will tell the GPT the URL where it can find your API server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;Your base URL&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To complete the process of creating your GPT, click the black Create button in the top right corner. You will then have the option of keeping your GPT private, or sharing it with others. This is up to your personal preference, but sharing it will require you to add a privacy policy. After you create your GPT, navigate to GPTs &amp;gt; My GPTs then click the pencil icon on your GPT and scroll down to the same place where you created your action earlier. Now you should see an action with your base URL listed, and under it you will see a Callback URL. Copy this callback URL and paste it into the &lt;code&gt;custom_gpt_callback&lt;/code&gt; variable in the &lt;code&gt;/api/oauth/callback&lt;/code&gt; route.&lt;/p&gt;

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

&lt;p&gt;Now you can securely interact with your FastAPI routes through an LLM – all without having to spin up an MCP server. This approach provides a robust, secure, and maintainable way to connect custom GPTs to your protected APIs. By leveraging Descope's Inbound Apps and OAuth capabilities, you can create sophisticated AI agents without the complexity of MCP servers.&lt;/p&gt;

&lt;p&gt;To explore Descope further, &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; for a free account, &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;join our developer Slack community&lt;/a&gt;, or &lt;a href="https://www.linkedin.com/company/descope/" rel="noopener noreferrer"&gt;follow us on LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>openai</category>
      <category>security</category>
    </item>
    <item>
      <title>Authenticating CLI Tools With Descope</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 01 May 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/authenticating-cli-tools-with-descope-401f</link>
      <guid>https://dev.to/descope/authenticating-cli-tools-with-descope-401f</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/cli-tool-auth" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Need to add authentication to your command-line tool? Most command-line applications rely on API keys, manual token management, and other methods that create friction for users who just want to get authenticated.&lt;/p&gt;

&lt;p&gt;In this blog, we'll walk through the challenges of CLI authentication and how to implement it seamlessly with Descope Inbound Apps. We'll also show how to bring &lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; directly to your CLI app with code examples in Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CLI authentication?
&lt;/h2&gt;

&lt;p&gt;In the context of command-line applications, authentication presents a unique challenge: how do you securely authenticate users in an environment that traditionally lacks the interactive elements of web applications?&lt;/p&gt;

&lt;p&gt;Most CLI tools handle this via the tedious storage of API keys in configuration files or environment variables. This approach creates friction for users and introduces security risks as a byproduct of storing credentials in plain text or across systems.&lt;/p&gt;

&lt;p&gt;Consider this scenario: You switch to a new laptop and need to set up various CLI applications, such as deployment tools, database clients, cloud service CLIs, and internal company tools. For each of these tools, you will need to generate API keys from different dashboards, make new cryptic environment variables to store these values, and save them in obscure configuration files. Now, you have API keys scattered across config files and tokens with overly broad permissions because of mishandled scoping. And eventually when you leave your company, IT will have no way to revoke access to these tools with these keys. This is the tax of API key storage for authentication and the potential security nightmare.&lt;/p&gt;

&lt;p&gt;CLI authentication using OAuth 2.0 solves this by bringing the familiar browser-based authentication flow directly to terminal applications. Users are able to authenticate through the same OAuth providers and flows they use in web applications.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How authentication looks with Descope Inbound Apps
&lt;/h2&gt;

&lt;p&gt;Inbound apps in Descope turn your application into an identity provider, making it compliant with OAuth standards and allowing third-party applications, APIs, and tools to authenticate and access authorized user data through a user consent flow with scope-based access control. They also handle OAuth flows and contain the necessary credentials for OAuth parameters, redirect URIs, and authentication settings. If you would like to learn more, check out our &lt;a href="https://www.descope.com/blog/post/inbound-apps" rel="noopener noreferrer"&gt;blog on Inbound Apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When a user runs an authentication command, or any command that requires authentication to use, the CLI tool generates an OAuth authorization URL corresponding to your Inbound App and opens the link in the user's default browser. The user then proceeds to complete the standard OAuth flow in their browser, signing in and authorizing the inbound app. Finally, the CLI app will receive the authorization callback with the necessary tokens to act on the user's behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: CLI authentication in Golang
&lt;/h2&gt;

&lt;p&gt;Now that we understand the purpose of CLI authentication and what our authentication flow looks like with Descope Inbound Apps, we can implement our own sample application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites and set up
&lt;/h3&gt;

&lt;p&gt;Before we dive into our CLI application, make sure you have Go 1.19 or later and a configured Descope Inbound App. If you're new to Descope, no problem. To get started, &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; for a free account on Descope &lt;a href="https://docs.descope.com/tutorials" rel="noopener noreferrer"&gt;Get started docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create your Descope Inbound App
&lt;/h3&gt;

&lt;p&gt;Navigate to your &lt;a href="https://app.descope.com/home" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt; and click on &lt;a href="https://app.descope.com/apps/inbound" rel="noopener noreferrer"&gt;Inbound Apps&lt;/a&gt; in the left 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%2Frw0u5j8wvqwmywx4r87u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frw0u5j8wvqwmywx4r87u.png" alt="Fig: Descope Inbound Apps page" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the + Inbound App button and give your app a name. Once created, you can configure scopes and view your connection information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Set up your Go command-line app with Cobra
&lt;/h3&gt;

&lt;p&gt;To initialize your new Go module and install the Cobra generator, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go mod init &amp;lt;your-module-name&amp;gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/spf13/cobra-cli@latest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
go get github.com/spf13/cobra &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
cobra-cli init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you will see your basic project structure including your main application file (&lt;code&gt;main.go&lt;/code&gt;) and a &lt;code&gt;cmd&lt;/code&gt; folder containing your &lt;code&gt;root.go&lt;/code&gt; file. You should see the following in the &lt;code&gt;root.go&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/spf13/cobra"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rootCmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"clidemo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"A brief description of your application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;`A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;[]&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rootCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rootCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoolP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"toggle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Help message for toggle"&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;For the purposes of the demonstration, we will be using the &lt;code&gt;Run&lt;/code&gt; in &lt;code&gt;rootCmd&lt;/code&gt; in order to run our authentication flow upon program execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configuring your Inbound App
&lt;/h3&gt;

&lt;p&gt;Connecting to your Inbound App requires setting up the relevant credentials. The form of the configuration will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;InboundAppConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Issuer&lt;/span&gt;                            &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;JwksURI&lt;/span&gt;                          &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;AuthorizationEndpoint&lt;/span&gt;            &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ResponseTypesSupported&lt;/span&gt;           &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;SubjectTypesSupported&lt;/span&gt;            &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;IdTokenSigningAlgValuesSupported&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;CodeChallengeMethodsSupported&lt;/span&gt;    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;TokenEndpoint&lt;/span&gt;                    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;UserInfoEndpoint&lt;/span&gt;                 &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ScopesSupported&lt;/span&gt;                  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ClaimsSupported&lt;/span&gt;                  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RevocationEndpoint&lt;/span&gt;               &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RegistrationEndpoint&lt;/span&gt;             &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// then, in your Run function, define your configuration&lt;/span&gt;
&lt;span class="c"&gt;// the base URL and project ID should be defined as environmental variables&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;InboundAppConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Issuer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                            &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/v1/apps/"&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;JwksURI&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                          &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/"&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="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/.well-known/jwks.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AuthorizationEndpoint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/oauth2/v1/apps/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResponseTypesSupported&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;SubjectTypesSupported&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;IdTokenSigningAlgValuesSupported&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;CodeChallengeMethodsSupported&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;TokenEndpoint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                    &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/oauth2/v1/apps/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UserInfoEndpoint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                 &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/oauth2/v1/apps/userinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ScopesSupported&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;ClaimsSupported&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"email_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"phone_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"phone_number_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"picture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"family_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"given_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;RevocationEndpoint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/oauth2/v1/apps/revoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RegistrationEndpoint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;DESCOPE_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/v1/mgmt/inboundapp/app/"&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="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a blueprint for the Inbound App, identifying it by the aforementioned connection information.&lt;/p&gt;

&lt;p&gt;Next, we create a handler for OpenID Connect discovery requests through the &lt;code&gt;.well-known/openid-configuration&lt;/code&gt; endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/.well-known/openid-configuration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Origin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Methods"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"GET, OPTIONS"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Headers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Content-Type, mcp-protocol-version"&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;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNoContent&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="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// using our Inbound App config&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Generating an OAuth URL
&lt;/h3&gt;

&lt;p&gt;Creating the parameters for the OAuth URL requires creating an OAuth2 CSRF state, a PKCE code verifier, and a code challenge. The state prevents Cross-Site Forgery attacks, verifying that the state remains the same when starting the OAuth2 flow and when redirecting back to the app. The Proof Key for Code Exchange code verifier is hashed in the code challenge, and the server verifies the two values are corresponding when exchanging the authorization code for tokens.&lt;/p&gt;

&lt;p&gt;To create these parameters, we will make a &lt;code&gt;util.go&lt;/code&gt; class for the associated functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// used to generate the state &amp;amp; verifier&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&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;base64URLEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;sha256Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// for hashing the verifier&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;base64URLEncode&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="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// for encoding the string &amp;amp; hash&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLEncoding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithPadding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoPadding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&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="s"&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;Then, in &lt;code&gt;root.go&lt;/code&gt;, we create the authorization URL using the authorization endpoint and params:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;codeVerifier&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;codeChallenge&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;base64URLEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256Hash&lt;/span&gt;&lt;span class="p"&gt;(&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;params&lt;/span&gt; &lt;span class="o"&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;Values&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"response_type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="o"&gt;:&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="s"&gt;"scope"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"code_challenge"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;codeChallenge&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"code_challenge_method"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;authURL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthorizationEndpoint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"?"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Authorization callback and retrieving tokens
&lt;/h3&gt;

&lt;p&gt;Now, we will set up a channel to receive our authorization code and exchange it for tokens. Go makes this elegant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;codeChan&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// channel receiving strings&lt;/span&gt;
&lt;span class="n"&gt;srv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c"&gt;// server to listen to&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the redirect URI is configured to &lt;code&gt;http://localhost:8080/callback&lt;/code&gt; (can be configured to a custom redirect URI), we set up an HTTP server listening on port 8080 to handle the OAuth callback response. We then handle the OAuth callback through a couple of steps:&lt;/p&gt;

&lt;h4&gt;
  
  
  Error handling
&lt;/h4&gt;

&lt;p&gt;Verify that the OAuth provider did not send back any error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errorCode&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&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;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;errorCode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;errorDesc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&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;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error_description"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OAuth Error: %s - %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorDesc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&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;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OAuth error: %s - %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorDesc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  State validation
&lt;/h4&gt;

&lt;p&gt;Compare the state parameter of the callback URL to the originally generated CSRF state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&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;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&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;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Invalid state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Authorization code extraction
&lt;/h4&gt;

&lt;p&gt;Look for the code parameter of the callback URL provided after successful login.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&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;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&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;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&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;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Missing code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Success!
&lt;/h4&gt;

&lt;p&gt;Receive the authorization code and output a success on the page for visual representation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Login successful! You may close this browser window."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;codeChan&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Opening the browser, posting, and fetching the token
&lt;/h4&gt;

&lt;p&gt;We want the user to interact with the authorization URL to proceed with the callback. In order to open the user browser, we add the following to our &lt;code&gt;util.go&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;openBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOS&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"windows"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"rundll32"&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"url.dll,FileProtocolHandler"&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="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"darwin"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"open"&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&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="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"xdg-open"&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&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;Start&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 function will call the corresponding shell command to open the user's browser for their respective operating system. Our next steps to retrieve the token response will be to post a request to the token endpoint of the following form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="s"&gt;"grant_type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c"&gt;// grant by auth code&lt;/span&gt;
&lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;    &lt;span class="c"&gt;// your inbound app client id&lt;/span&gt;
&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="o"&gt;:&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="c"&gt;// the code fetched from the channel&lt;/span&gt;
&lt;span class="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="o"&gt;:&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="c"&gt;// your redirect URI&lt;/span&gt;
&lt;span class="s"&gt;"code_verifier"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;codeVerifier&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;         &lt;span class="c"&gt;// previously calculated verifier&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And, putting it all together with a final authentication message on the CLI app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;openBrowser&lt;/span&gt;&lt;span class="p"&gt;(&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;code&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;codeChan&lt;/span&gt;
&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenEndpoint&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;Values&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"grant_type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DESCOPE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="o"&gt;:&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="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="o"&gt;:&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="s"&gt;"code_verifier"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;codeVerifier&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Token exchange error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Token exchange failed with status: %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tokenResp&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c"&gt;// response format in Go&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&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;tokenResp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid token response: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome! You have successfully authenticated with Descope"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Visualizing the Flow
&lt;/h3&gt;

&lt;p&gt;Run your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, your browser will open a new 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%2Fbzl52djpclw1t6suh2ia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzl52djpclw1t6suh2ia.png" alt="Fig: Sign in prompt opened via the authorization URL" width="356" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sign in with your preferred method. You will then be prompted to authorize:&lt;/p&gt;

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

&lt;p&gt;Next, you will see the browser output after authenticating the user and successfully authorizing the Inbound App:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login successful! You may close this browser window.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the CLI app will print a success message after receiving the authorization callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Welcome! You have successfully authenticated with Descope Inbound Apps.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you would like to see the full sample code, it is available in our &lt;a href="https://github.com/descope/descope-cli-auth" rel="noopener noreferrer"&gt;Github repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI authentication made simpler with Descope
&lt;/h2&gt;

&lt;p&gt;CLI auth with Descope Inbound Apps makes your command-line authentication flow smoother, safer, and free of user friction. Users will be able to authenticate with the same OAuth providers and flows they use in web applications, providing a seamless and familiar experience. &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; for a Free Forever Descope account now to start creating frictionless CLI authentication flows. You can also dive into more auth best practices by joining &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;, our community of developers building better identity experiences. Got questions about Descope? &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;Book time&lt;/a&gt; with our auth experts.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>go</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Adding Authentication Middleware With Descope</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 29 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/adding-authentication-middleware-with-descope-1jo2</link>
      <guid>https://dev.to/descope/adding-authentication-middleware-with-descope-1jo2</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/authentication-middleware" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

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

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

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

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

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

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

&lt;p&gt;Let's look at how to build simple authentication middleware using Express in a Node.js app. This middleware intercepts requests, checks for an authorization header, and validates the session token before passing the request to the next handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;validateJwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// validation logic&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValidSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;responseText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&amp;lt;br&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;responseText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;small&amp;gt;Session Valid:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValidSession&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

&lt;p&gt;&lt;strong&gt;authentication.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DescopeClient&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@descope/node-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DescopeMiddleware&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descopeClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DescopeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;descopeSdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Successfully validated user session:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Could not validate user session &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;app.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DescopeMiddleware&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./authentication.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DescopeMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__PROJECT_ID__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Express Running port 3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern helps you manage authentication in one place. It also improves maintainability by offloading session validation to Descope's SDK, allowing you to focus on application logic.&lt;/p&gt;

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

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

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

&lt;p&gt;Here's a basic example of how decorators work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Something is happening before the function is called.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Something is happening after the function is called.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;

&lt;span class="nd"&gt;@my_decorator&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;say_hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;Here's an example of using Descope's &lt;code&gt;@descope_validate_auth&lt;/code&gt; decorator to protect a route that requires authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This needs authentication
&lt;/span&gt;&lt;span class="nd"&gt;@APP.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@descope_validate_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;descope_client&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Can add permissions=["Perm 1"], roles=["Role 1"], tenant="t1" conditions
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;private&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Restricted page, authentication needed.&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decorator automatically checks the user's &lt;a href="https://www.descope.com/learn/post/jwt" rel="noopener noreferrer"&gt;JWT&lt;/a&gt;. If the token doesn't meet the conditions—such as having the correct roles or permissions—Descope will return a 401 Unauthorized response without running the route handler.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@APP.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@descope_full_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flow_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sign-up-or-in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;success_redirect_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://dev.localhost:9010/private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Nothing to do! this is the MAGIC!
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need to handle logout? The &lt;code&gt;@descope_logout&lt;/code&gt; decorator clears the user's session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@APP.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/logout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@descope_logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descope_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Goodbye, logged out.&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With just a few lines of code, you can use decorators to handle user authentication and session flow without repeating logic across your app.&lt;/p&gt;

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

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

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

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

</description>
      <category>node</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Shiny App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-shiny-app-15mb</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-shiny-app-15mb</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-sso-shiny" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;p&gt;&lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Sign up for a free Descope account&lt;/a&gt; if you don't already have one, and follow the &lt;a href="https://app.descope.com/gettingStarted" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; wizard to create a new flow. In the wizard, first, choose Consumers as the target audience for your app:&lt;/p&gt;

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

&lt;p&gt;Next, choose the authentication methods you want to use. In this tutorial, you'll use Magic Link and SSO as the authentication method. You can change these settings in the future if you like:&lt;/p&gt;

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

&lt;p&gt;Skip the next step that prompts you to choose an MFA method by clicking Go ahead without MFA. In the next step, review the selected settings and click Next to generate the authentication flow:&lt;/p&gt;

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

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;personal-finance-shiny
git checkout starter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the application using RStudio and attempt to run the starter app. When you run it for the first time, it will prompt you to install the required packages. After you install the packages and rerun the app, you'll notice that you're directly taken to the dashboard page without authentication:&lt;/p&gt;

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

&lt;p&gt;Next, click on the Publish button in RStudio to deploy the app on &lt;a href="https://docs.posit.co/pct/" rel="noopener noreferrer"&gt;Posit Connect&lt;/a&gt;.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;In your R project, create a &lt;code&gt;www/descope-auth.js&lt;/code&gt; file, add the following content to create a container for the Descope element, and set success and failure listeners:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// www/descope-auth.js&lt;/span&gt;
&lt;span class="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCustomMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initDescopeAuth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wcElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descope-wc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;project-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flow-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flowId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;successInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authentication error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Clear any previous instances&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, you need to update the UI definition to include the &lt;code&gt;@descope/web-component&lt;/code&gt; and &lt;code&gt;descope-auth.js&lt;/code&gt; JS scripts. Update the &lt;code&gt;app.R&lt;/code&gt; file by replacing it with the following code snippet:&lt;br&gt;
&lt;/p&gt;

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

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

  &lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appContent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;sidebarLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;sidebarPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload Expense Data (CSV)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".csv"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;selectInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Select Category:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;mainPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;tabsetPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;tabPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Overview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;plotOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"expenseTrend"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tableOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"summaryTable"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;tabPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Category Analysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;plotOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"categoryTrend"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tableOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"categoryTable"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you need to update the server definition to initialize the Descope web component and implement success and failure callbacks. Add the following code snippet at the start of the server section:&lt;br&gt;
&lt;/p&gt;

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

  &lt;/span&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is.null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Login successful!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"appContent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is.null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;auth_error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Authentication failed. Please try again."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;# existing server definition&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_DESCOPE_PROJECT_ID&lt;/code&gt; with your &lt;a href="https://app.descope.com/settings/project" rel="noopener noreferrer"&gt;Descope project ID&lt;/a&gt;, save the changes, and republish the app to Posit Connect using RStudio.&lt;/p&gt;

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

&lt;p&gt;When a user clicks the magic link received in their email for authentication, Descope will validate the auth and redirect the user back to the Shiny application. For redirection to work, you need to configure the auth redirect URL in Descope. Head over to &lt;a href="https://docs.descope.com/auth-methods/magic-link#magic-link" rel="noopener noreferrer"&gt;Magic link&lt;/a&gt; under Authentication Methods and set the &lt;a href="https://docs.descope.com/auth-methods/magic-link#redirect-url" rel="noopener noreferrer"&gt;Redirect URL&lt;/a&gt; to the published URL of your Shiny app.&lt;/p&gt;

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

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

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

&lt;p&gt;When you run the app, you'll be redirected to the Descope auth screen. You can test the integration by entering your email ID on this screen and clicking the Continue button to log in with a magic link. You'll receive an email with a login link, and upon clicking it, you'll be redirected to the dashboard page of your Shiny app.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;First, use the Descope portal to create a new project to host the OIDC application. This tutorial sets the project name as &lt;code&gt;descope-shiny-auth-sso&lt;/code&gt;.&lt;/p&gt;

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

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

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

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

&lt;p&gt;To configure OAuth with Posit Connect, you'll also need to create a new client secret. Navigate to the &lt;a href="https://app.descope.com/m2m/accessKeys" rel="noopener noreferrer"&gt;Access Keys&lt;/a&gt; page and click +Access key to create a new access key.&lt;/p&gt;

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

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

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

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

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

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


&lt;/li&gt;

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

&lt;/ul&gt;

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

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

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

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

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

&lt;p&gt;Finally, to test the integration, open the application in the browser, enter your email ID, and click on Continue with SSO to initiate the SSO flow. Note that the email ID should use &lt;code&gt;example.com&lt;/code&gt; for SSO to work since you didn't configure any other domains in Descope.&lt;/p&gt;

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

&lt;p&gt;You'll be redirected to the IdP login screen, which, in this case, is hosted by the &lt;code&gt;descope-shiny-auth-sso&lt;/code&gt; Descope project. You can verify this by comparing the project ID in the address bar with the Descope project ID.&lt;/p&gt;

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

&lt;p&gt;For testing, complete the login using one of the social login options or the magic link, and upon authentication, you'll be redirected back to the Shiny dashboard.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;To test the integration, open the application in the browser, enter your email ID, and click on Continue with SSO to initiate the SSO flow. Note that the email ID should use &lt;code&gt;example.com&lt;/code&gt; for SSO to work since you didn't configure any other domains in Descope. If you used a mock URL, you'll be redirected to a MockSAML login page where you can confirm your email ID and click Sign in.&lt;/p&gt;

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

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

&lt;p&gt;After logging in, you'll be redirected to the dashboard page.&lt;/p&gt;

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

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

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

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

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

  &lt;/span&gt;&lt;span class="c1"&gt;# Existing inputs&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload Expense Data (CSV)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".csv"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;selectInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Select Category:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;user_authenticated&lt;/code&gt; observer under the server definition to set the welcome message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;observeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;user_authenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

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

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

  &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;welcomeText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;renderText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Welcome,"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Login successful!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"appContent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the above code snippet tries to retrieve the user's name from the &lt;code&gt;user_info&lt;/code&gt; object and falls back to email if name is not present.&lt;/p&gt;

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

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

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

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

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

  &lt;/span&gt;&lt;span class="c1"&gt;# Existing inputs&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload Expense Data (CSV)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".csv"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;selectInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Select Category:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;# Logout button&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;actionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"logout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Logout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sign-out-alt"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add a &lt;code&gt;descopeLogout&lt;/code&gt; message handler in the &lt;code&gt;descope-auth.js&lt;/code&gt; file that will be invoked when the logout button is clicked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Shiny&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCustomMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descopeLogout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wcElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descope-wc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;wcElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, add a logout observer under the server definition to invoke the &lt;code&gt;descopeLogout&lt;/code&gt; and &lt;code&gt;initDescopeAuth&lt;/code&gt; message handlers.&lt;br&gt;
&lt;/p&gt;

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

  &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;sendCustomMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"initDescopeAuth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P2pBl7sYVGg1RWtv4jC7zfO9TnUN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;flowId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sign-up-or-in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;containerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"authContainer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;successInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_authenticated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;errorInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth_error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and republish the changes to test them. Once you log in to the dashboard, you'll notice a Logout button being displayed next to the Analyze button.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Descope is a drag-and-drop customer authentication and identity management platform. Our low- or no-code CIAM solution helps hundreds of organizations easily create and customize their entire user journey using visual workflows—from authentication and authorization to MFA and federated SSO. Customers like &lt;a href="https://www.descope.com/customers/navan" rel="noopener noreferrer"&gt;Navan&lt;/a&gt;, GoFundMe, &lt;a href="http://you.com/" rel="noopener noreferrer"&gt;You.com&lt;/a&gt;, and Branch Insurance use Descope to reduce user friction, prevent account takeover, and get a unified view of their customer journey. To learn more, &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt; with our auth experts or join our dev community, &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add Authentication and SSO to Your Flet App</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Fri, 24 Apr 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/descope/add-authentication-and-sso-to-your-flet-app-543b</link>
      <guid>https://dev.to/descope/add-authentication-and-sso-to-your-flet-app-543b</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/auth-sso-flet" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

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

&lt;p&gt;In this guide, you will learn how to integrate Descope authentication and &lt;a href="https://www.descope.com/learn/post/sso" rel="noopener noreferrer"&gt;single sign-on (SSO)&lt;/a&gt; into a Flet application. You'll learn how to add basic authentication to your Flet app using Descope &lt;a href="https://www.descope.com/learn/post/magic-links" rel="noopener noreferrer"&gt;magic links&lt;/a&gt; and &lt;a href="https://www.descope.com/learn/post/social-login" rel="noopener noreferrer"&gt;social login&lt;/a&gt;. You'll also configure Descope to use &lt;a href="https://www.okta.com/" rel="noopener noreferrer"&gt;Okta&lt;/a&gt; as the &lt;a href="https://docs.descope.com/sso-integrations/idp-vs-sp" rel="noopener noreferrer"&gt;identity provider (IdP)&lt;/a&gt;, enabling SSO for your Flet app.&lt;/p&gt;

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

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

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

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

&lt;p&gt;For businesses handling sensitive data, SSO ensures centralized &lt;a href="https://www.descope.com/learn/post/authorization" rel="noopener noreferrer"&gt;access control&lt;/a&gt;, which makes it easier to enforce security policies while maintaining a smooth login process.&lt;/p&gt;

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

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

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

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

&lt;p&gt;This tutorial uses a prebuilt starter template so you can focus on implementing authentication. To clone the starter template to your local machine, execute the following command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;cd into the project folder, set up a virtual environment, activate it, and install the project dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;descope-flet-auth-sso

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



&lt;p&gt;This command opens the web version of the app. To view it, navigate to &lt;a href="http://localhost:8550" rel="noopener noreferrer"&gt;http://localhost:8550&lt;/a&gt; on your browser, and you should see the demo application homepage with a Login button:&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;To implement authentication in your Flet app, you first need to create a project and design a &lt;a href="https://docs.descope.com/flows" rel="noopener noreferrer"&gt;flow&lt;/a&gt; on your &lt;a href="https://app.descope.com/" rel="noopener noreferrer"&gt;Descope console&lt;/a&gt;. To get started, launch your Descope console, click the project dropdown, and select + Project to create a new project:&lt;/p&gt;

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

&lt;p&gt;On the Create project page, provide "descope-flet-auth-sso" as the project name and click Create to create the project:&lt;/p&gt;

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

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

&lt;p&gt;Typically, you would need to design this flow from scratch, but to make it easy for you to follow along, this tutorial uses a pre-prepared flow, which is included in the starter template. You can find it in the project root folder in a file named sign-up-or-in.json. You just need to import this flow into the Descope console. To do this, navigate open the default sign-up-or-in flow in the flow editor by navigating to Flows &amp;gt; sign-up-or-in:&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;p&gt;To integrate Descope as the authentication provider for your Flet app, you first need to configure an OIDC app in the Descope dashboard to obtain the necessary endpoints required for the authorization code flow. To do this, navigate to Applications &amp;gt; OIDC default application on the Descope console:&lt;/p&gt;

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

&lt;p&gt;Scroll down to the SP Configuration section and take note of the Client ID, Authorization URL, and Access Token URL. You will use these values in your Flet app.&lt;/p&gt;

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

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

&lt;p&gt;You also need to generate a client secret. You can do this by navigating to M2M &amp;gt; + Access Key. On the Generate Access Key form, provide the key name, select Generate Key, and copy the value of your key.&lt;/p&gt;

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

&lt;p&gt;Now that you have these values, you can go back to your Flet app and start implementing authentication. First, create a new file named .env in the project root folder and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR-DESCOPE-CLIENT-ID&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR-DESCOPE-CLIENT-SECRET&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com/oauth2/v1/authorize"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com/oauth2/v1/token"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.descope.com/oauth2/v1/userinfo"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8550/oauth_callback"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sub"&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_SCOPES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openid profile email"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;You also need a utility file to extract the environment variables and make them available for use in different parts of the application. Create a new file named config.py in the project root folder and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Environment variable &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is required but not set.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;

    &lt;span class="n"&gt;OAUTH_CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_REDIRECT_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_SCOPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_SCOPES&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code loads environment variables from a .env file and defines a Config class with a method to retrieve them, raising an error if a required variable is missing. It then uses this method to set various OAuth-related settings as class attributes.&lt;/p&gt;

&lt;p&gt;To define a custom OAuth provider, create a new file named auth_provider.py in the auth folder and add the code below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flet.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuthProvider&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;BaseOAuthProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseOAuthProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;authorization_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_AUTHORIZATION_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;token_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_USER_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;redirect_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user_id_fn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_USER_ID_FIELD&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code defines an &lt;code&gt;OAuthProvider&lt;/code&gt; class that inherits from the Flet &lt;code&gt;OAuthProvider&lt;/code&gt; class and initializes it with OAuth configuration settings from the &lt;code&gt;Config&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Open the auth/auth_helpers.py file. Here, you will modify the methods to log in and log out the user instead of just navigating them. First, replace the import statements with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flet&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoginEvent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, replace the &lt;code&gt;handle_login&lt;/code&gt; method with the following to trigger the login process using the OAuth provider configured for the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the &lt;code&gt;handle_logout&lt;/code&gt; method with the following to log the user out by calling the logout function on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the same class, add the following methods to configure methods that will be called when the user is successfully logged in or out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LoginEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These methods handle login and logout events: &lt;code&gt;on_login&lt;/code&gt; checks for errors during login and navigates to the &lt;code&gt;/profile&lt;/code&gt; route if successful, while &lt;code&gt;on_logout&lt;/code&gt; redirects the user to the home route (&lt;code&gt;/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Open the views/profile_view.py file and replace the &lt;code&gt;get_view&lt;/code&gt; method with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vertical_alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;horizontal_alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;controls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;horizontal_alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;controls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;foreground_image_src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;picture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="n"&gt;bgcolor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREY_400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOLD&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Logout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_logout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Open the main.py file and add the following import statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;auth.auth_provider&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuthProvider&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following line of code to the &lt;code&gt;__init__&lt;/code&gt; method just before &lt;code&gt;self.auth_manager = AuthManager(page=self.page)&lt;/code&gt; to create an instance of the OAuth provider and attach it to the current page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuthProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the event handler you created earlier to the page by adding the following code at the end of the &lt;code&gt;__init__&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_login&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_logout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_logout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you need to protect the &lt;code&gt;/profile&lt;/code&gt; page to make sure it cannot be accessed by unauthenticated users. To do this, in the &lt;code&gt;route_handler&lt;/code&gt; method, replace the first block of the if statement with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProfileView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_logout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_view&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code checks if the current route is &lt;code&gt;/profile&lt;/code&gt;; if the user isn't authenticated, it redirects them to the home page. Otherwise, it loads the &lt;code&gt;ProfileView&lt;/code&gt; and appends it to the page.&lt;/p&gt;

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

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

&lt;p&gt;&lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OIDC&lt;/a&gt; is a simple identity layer built on top of OAuth 2.0. It helps enable SSO by allowing users to authenticate through trusted IdPs, such as Azure or Okta. With Descope, you can easily integrate OIDC SSO into your app, letting users log in securely with their existing credentials, which streamlines access without the need for new accounts.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;On the app details page, select the Sign On tab and take note of the client ID and secret.&lt;/p&gt;

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

&lt;p&gt;You'll also need a &lt;a href="https://docs.descope.com/b2b/multi-tenancy#tenants" rel="noopener noreferrer"&gt;tenant&lt;/a&gt; to use SSO. Back on your Descope console, navigate to Tenants &amp;gt; + Tenant. On the Create Tenant form, provide the tenant name and click Create:&lt;/p&gt;

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

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

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

&lt;p&gt;Next, still on the tenant details page, select Authentication Methods &amp;gt; SSO &amp;gt; OIDC. Under Tenant Details &amp;gt; SSO Domains, make sure to provide your email domain. This domain is used to determine which SSO configuration to load once a user chooses to authenticate using SSO.&lt;/p&gt;

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

&lt;p&gt;Scroll down to the SSO configuration &amp;gt; Account Settings section and provide the required values as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider Name:&lt;/strong&gt; Okta&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; The Client ID you obtained from the Okta dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret:&lt;/strong&gt; The client secret you obtained from the Okta dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; &lt;code&gt;openid profile email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant Type:&lt;/strong&gt; Authorization code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also need to provide the required values for the SSO configuration &amp;gt; Connection Settings section. For this, you need to obtain OAuth endpoints from the &lt;a href="https://developer.okta.com/docs/concepts/auth-servers/#discovery-endpoints-org-authorization-servers" rel="noopener noreferrer"&gt;Okta "well-known" configuration&lt;/a&gt;, which provides standardized metadata for OAuth and OIDC integrations. To obtain this, navigate to &lt;code&gt;https://&amp;lt;YOUR-OKTA-INSTANCE&amp;gt;.okta.com/.well-known/openid-configuration&lt;/code&gt; on your browser and take note of the issuer and authorization, token, user info, and JWKs endpoints.&lt;/p&gt;

&lt;p&gt;Make sure to replace &lt;code&gt;&amp;lt;YOUR-OKTA-INSTANCE&amp;gt;&lt;/code&gt; with the correct value, which is your organization's Okta instance ID.&lt;/p&gt;

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

&lt;p&gt;Go back to the Descope console and provide the values you just obtained from the Okta well-known endpoint in the respective inputs under SSO configuration &amp;gt; Connection Settings and save the changes.&lt;/p&gt;

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

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

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

&lt;p&gt;On the Assign Descope Groups page, select the Assign button beside the Everyone group to allow all users in your organization to log in via SSO and click Done to save the changes.&lt;/p&gt;

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

&lt;p&gt;SSO with Okta as the IdP is now fully configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a persistent login feature
&lt;/h2&gt;

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

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

&lt;p&gt;Open the .env file in the project root folder and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cv3Gc9RG-9o8icwTBUEDb0HJC7M5A0b2FxT3WpWvg1I="&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"auth_token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;To load the environment variables you just created into the Flet application, open the config.py file and add the following attributes to the &lt;code&gt;Config&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the auth/auth_helpers.py file and add the following import statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flet.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decrypt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Replace the &lt;code&gt;on_login&lt;/code&gt; method with the following to modify the &lt;code&gt;on_login&lt;/code&gt; method to encrypt and save the token to client storage once a user is successfully authenticated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LoginEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Save token in client storage
&lt;/span&gt;    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;encrypted_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encrypted_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code retrieves the access token, encrypts it using the secret key you set earlier, and stores the encrypted token in client storage. It then redirects the user to the profile page.&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;handle_login&lt;/code&gt; method with the following to modify the &lt;code&gt;handle_login&lt;/code&gt; method to check if a valid token is stored in the client storage before redirecting the user to the sign-in page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;saved_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# Retrieve token from client storage
&lt;/span&gt;    &lt;span class="n"&gt;encrypted_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

    &lt;span class="c1"&gt;# Log in the user
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;saved_token&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saved_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;saved_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_SCOPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code retrieves the saved encrypted token from the client storage, decrypts it, and passes it to the &lt;code&gt;self.page.login&lt;/code&gt; method. This way, if the token is valid, the application will obtain the user's info without prompting the user to reauthenticate.&lt;/p&gt;

&lt;p&gt;Lastly, replace the &lt;code&gt;handle_logout&lt;/code&gt; method with the following to modify the &lt;code&gt;handle_logout&lt;/code&gt; method to make sure that it removes the token from storage when a user logs out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OAUTH_TOKEN_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;Run the application using the command &lt;code&gt;flet run -w --port 8550&lt;/code&gt;, navigate to &lt;a href="http://localhost:8550" rel="noopener noreferrer"&gt;http://localhost:8550&lt;/a&gt; on your browser, and click the Login button. You should be redirected to the sign-in page:&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;Implementing authentication and SSO into your Flet app using Descope is a straightforward process that significantly enhances your application's security and user experience. By integrating features like magic link authentication, social logins, and SSO with OIDC, you can offer your users seamless and secure access, whether they are individuals or part of an organization using their existing credentials.&lt;/p&gt;

&lt;p&gt;Descope is a drag-and-drop customer authentication and identity management platform. Our no- or low-code CIAM solution helps hundreds of organizations easily create and customize their entire user journey using visual workflows—from authentication and authorization to MFA and federated SSO. Customers such as GoFundMe, &lt;a href="https://www.descope.com/customers/navan" rel="noopener noreferrer"&gt;Navan&lt;/a&gt;, &lt;a href="https://www.descope.com/customers/you-com" rel="noopener noreferrer"&gt;You.com&lt;/a&gt;, and &lt;a href="https://www.descope.com/customers/branch" rel="noopener noreferrer"&gt;Branch&lt;/a&gt; use Descope to reduce user friction, prevent account takeover, and get a unified view of their customer journey. To learn more, join our dev community, &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown&lt;/a&gt;, and explore the &lt;a href="https://docs.descope.com/" rel="noopener noreferrer"&gt;Descope documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
