<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Descope</title>
    <description>The latest articles on DEV Community by Descope (@descope).</description>
    <link>https://dev.to/descope</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F12798%2Fc2ebf8e9-6ee5-4b7b-93fd-4e62def6c983.png</url>
      <title>DEV Community: Descope</title>
      <link>https://dev.to/descope</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/descope"/>
    <language>en</language>
    <item>
      <title>Best MCP Server Directories for Developers</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Wed, 27 May 2026 21:31:45 +0000</pubDate>
      <link>https://dev.to/descope/best-mcp-server-directories-for-developers-502e</link>
      <guid>https://dev.to/descope/best-mcp-server-directories-for-developers-502e</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/blog/post/mcp-directories" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&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; has quickly established itself as an open standard that easily connects AI applications to external data sources and tools. It acts as a universal interface that allows language models to access databases, APIs, or file systems in a standardized way. No custom integrations required. However, with thousands of MCP servers now available on dozens of MCP directories, it's hard for developers to find the right MCP server.&lt;/p&gt;

&lt;p&gt;To help you choose, this article compares several MCP directories based on six practical criteria: how easy it is to find the right server, how carefully the entries are compiled and documented, how up-to-date and maintained the catalog is, what security and trust signals are available, how smoothly installation and configuration go, and how active the community is.&lt;/p&gt;

&lt;p&gt;In our testing approach, we'll simulate realistic developer workflows that begin with identifying a need, continue with assessing the trustworthiness of a server, and culminate in the actual installation and execution in a standard MCP client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP registries
&lt;/h2&gt;

&lt;p&gt;Several directories have established themselves within the MCP ecosystem, each with a different focus. The most notable are:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mcp" rel="noopener noreferrer"&gt;GitHub MCP Registry&lt;/a&gt;: The focus of this directory is on standardization and authority, not quantitative breadth. It serves as a reference implementation for the entire ecosystem. The registry ensures adherence to metadata standards, but only includes a selective list of official servers (around 65 directories as of January 2026) and offers little insight into the maturity of third-party tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://glama.ai/mcp" rel="noopener noreferrer"&gt;Glama&lt;/a&gt;: Glama positions itself as a curated seal of approval for MCP servers. A team uses automated scans and manual reviews to ensure that projects have READMEs, valid licenses, and no known vulnerabilities. These stringent inclusion criteria make the catalog significantly more focused on quality than sheer quantity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pulsemcp.com/" rel="noopener noreferrer"&gt;PulseMCP&lt;/a&gt;: PulseMCP focuses on discoverability and popularity signals. It aggregates several thousand servers and marks entries as "official" or "community." Popularity metrics, such as estimated visitor numbers and rankings, make it a good choice for browsing and prototyping.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mcpmarket.com/" rel="noopener noreferrer"&gt;MCP Market&lt;/a&gt;: MCP Market is operated by a private provider and positions itself as a comprehensive marketplace with the largest number of listed servers. It ranks servers by popularity and recency and categorizes them into topics like "API development".&lt;/p&gt;

&lt;h2&gt;
  
  
  Discoverability and finding the right server
&lt;/h2&gt;

&lt;p&gt;To assess how well the various directories are suited for searching, we searched for an MCP server that allows access to a PostgreSQL database. On each platform, we entered the same search term and then examined how quickly a suitable solution could be found, what filtering options were available, and how detailed the respective descriptions were.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub MCP Registry
&lt;/h3&gt;

&lt;p&gt;The GitHub MCP Registry currently lists 87 servers as of March 2026 with a basic search bar, but no categories or filters. For instance, searching for "PostgreSQL" returns a single result: DBHub, a database server supporting PostgreSQL, MySQL, SQL Server, SQLite, and MariaDB. Each listing includes an install button, a short description, the provider, and the repository's GitHub star count.&lt;/p&gt;

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

&lt;p&gt;This directory is useful if you know what you are looking for, and offers limited discoverability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glama
&lt;/h3&gt;

&lt;p&gt;Glama offers a search field above the server list and enables an advanced search with a single click on "DeepSearch." For instance, entering "PostgreSQL" returns matching MCP tools (for example, pg_debug_database) and actual MCP servers, and selecting one opens a dedicated details page:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  PulseMCP
&lt;/h3&gt;

&lt;p&gt;PulseMCP has a central search field with auto-complete and directly displays how many servers are found for the search term. For example, searching for "PostgreSQL", it lists 100+ results, with the first entry being an Anthropic's reference server, in addition to many other community maintained servers.&lt;/p&gt;

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

&lt;p&gt;It is well-suited for broad discovery with popularity signals to guide shortlisting.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Market
&lt;/h3&gt;

&lt;p&gt;MCP Market is geared towards discovery through presentation and categorization. This makes the platform attractive to developers who want to see practical applications alongside functionality while browsing. For example, when we searched "PostgreSQL," multiple server maps were displayed, each with a star rating and a brief description. Clicking on a map opens a detailed page with tabs for "About," "README," and "FAQ."&lt;/p&gt;

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

&lt;p&gt;MCP Market works best when you're trying to figure out which server variant actually fits what you're doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Github MCP Registry&lt;/th&gt;
&lt;th&gt;Glama&lt;/th&gt;
&lt;th&gt;PulseMCP&lt;/th&gt;
&lt;th&gt;MCP Market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Basic search, no filters&lt;/td&gt;
&lt;td&gt;DeepSearch, category filters&lt;/td&gt;
&lt;td&gt;Auto-complete, classification filters, popularity sorting&lt;/td&gt;
&lt;td&gt;Use case categories, star ratings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL results&lt;/td&gt;
&lt;td&gt;1 server&lt;/td&gt;
&lt;td&gt;2 server &amp;amp; tools&lt;/td&gt;
&lt;td&gt;100+ servers&lt;/td&gt;
&lt;td&gt;2+ with use cases listed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detail page&lt;/td&gt;
&lt;td&gt;Install button, description, provider, Github stars&lt;/td&gt;
&lt;td&gt;Dedicated page with tool and server separation&lt;/td&gt;
&lt;td&gt;Provider, visitor metrics, release date and server.json&lt;/td&gt;
&lt;td&gt;About, Readme file, FAQ tabs, implementation guides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key strength&lt;/td&gt;
&lt;td&gt;Official reference&lt;/td&gt;
&lt;td&gt;Advanced search&lt;/td&gt;
&lt;td&gt;Community servers and ranking signals&lt;/td&gt;
&lt;td&gt;Rich contextual details about the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PulseMCP offers the strongest discovery experience here. It provides over a hundred results for PostgreSQL with popularity sorting and classification filters to give you a quick path from search to install. MCP Market is a solid runner-up: useful contextual details, but only once you're on a page. The other two are limited unless you already know what you're looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality standards: curation vs. comprehensiveness
&lt;/h2&gt;

&lt;p&gt;How a directory vets its servers matters as much as what it lists. Our goal is to assess the server's maturity, as well as its documentation, auditing, and user feedback while searching for "GitHub MCP server" on the presented platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub MCP Registry
&lt;/h3&gt;

&lt;p&gt;The GitHub MCP registry performs basic validation for server.json, limited to format and namespace uniqueness, but doesn't evaluate documentation quality or run tests. Users can share feedback by opening GitHub issues against the server's GitHub repository, but the feedback isn't directly visible on the server details page. For example, when you search for "GitHub MCP Server", the details page essentially renders the Readme.md file of the corresponding server repository.&lt;/p&gt;

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

&lt;p&gt;The registry positions itself as an "app store for MCP servers" for clients and deliberately leaves quality assessment to other levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glama
&lt;/h3&gt;

&lt;p&gt;Glama is significantly more curation-oriented than other directories in this test and attempts to answer the question "Is this server mature enough for deployment?" directly within the directory. For example, the GitHub MCP server receives a scorecard with separate ratings for security, license, and quality, including statements such as "no known vulnerabilities" and "Author verified." Its includes criteria in the scores, such as known vulnerabilities, license verification, operability, tool scope, and up-to-dateness.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  PulseMCP
&lt;/h3&gt;

&lt;p&gt;PulseMCP shows the provider, official classification, number of visitors, and release date at a glance. For example, with a GitHub MCP server, you immediately notice the provider as GitHub and classification as Official reassuring users about the server's reliability. PulseMCP also explicitly refers to the server.json file for servers as a "standardized, official file format" and positions it as the source for installation, configuration, and usage guidelines.&lt;/p&gt;

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

&lt;p&gt;The platform is particularly suitable for developers who want a wide selection but can quickly create a pre-selection based on "official and frequently used" options.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Market
&lt;/h3&gt;

&lt;p&gt;MCP Market focuses on explaining the fit, helping users quickly decide whether it's worth taking a look at the repository. Consequently, documentation requirements and audit procedures are less prominently displayed. Testing or validation in the sense of "guaranteed to work" is not communicated as a core feature. User feedback is conveyed more through marketplace mechanics like rankings and related recommendations than through a comprehensive review system.&lt;/p&gt;

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

&lt;p&gt;MCP Market primarily caters to the preference for breadth and comfortable browsing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Github MCP Registry&lt;/th&gt;
&lt;th&gt;Glama&lt;/th&gt;
&lt;th&gt;PulseMCP&lt;/th&gt;
&lt;th&gt;MCP Market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Curation approach&lt;/td&gt;
&lt;td&gt;Format validation only&lt;/td&gt;
&lt;td&gt;Comprehensive scorecard&lt;/td&gt;
&lt;td&gt;Classification &amp;amp; popularity&lt;/td&gt;
&lt;td&gt;Marketplace rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation requirements&lt;/td&gt;
&lt;td&gt;None (only format compliance)&lt;/td&gt;
&lt;td&gt;Readme needed&lt;/td&gt;
&lt;td&gt;server.json and repository link&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality and user feedback&lt;/td&gt;
&lt;td&gt;Github issues&lt;/td&gt;
&lt;td&gt;Quality indicators, issue reporting&lt;/td&gt;
&lt;td&gt;Shows visitor metrics, but no direct feedback&lt;/td&gt;
&lt;td&gt;Rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Glama is the only directory that appears to be taking a hands-on approach to curation. Scorecards, vulnerability checks, and license verification mean you can actually evaluate production readiness without leaving the platform to research yourself. Every other contender puts the quality eval on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and verification: GitHub server test case
&lt;/h2&gt;

&lt;p&gt;Because MCP servers allow deep access to production systems, you need to choose a trusted directory. Platforms differ significantly in how they inspect code, authenticate maintainers, and flag vulnerabilities. Developers should understand the security features offered and where they need to perform their own checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub MCP Registry
&lt;/h3&gt;

&lt;p&gt;The official registry provides metadata but no security assessments. It checks whether the server.json file conforms to the schema and the namespace is unique. If users discover abuse or suspicious servers, they can report them via a GitHub issue. Maintainers can then add entries to a deny list. Beyond that, there are no code reviews, vulnerability scans, or verified maintainer badges.&lt;/p&gt;

&lt;p&gt;The registry explicitly described itself as a "metaregistry" that delegates security and compliance to underlying package registries and downstream systems. For organizations, the registry recommends setting up private sub-registries with their own security policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glama
&lt;/h3&gt;

&lt;p&gt;Glama performs the most comprehensive check of MCP servers and displays the results as a scorecard. For instance, for the search of the GitHub MCP server, three categories are evaluated: security, licensing, and quality. The platform reported no known vulnerabilities and confirmed that the server functions as expected.&lt;/p&gt;

&lt;p&gt;Glama also shows whether a server is inspectable, when it was last updated, and whether a README and license are available. Maintainers can verify their projects via a GitHub login. Until they do so, the message "Author not verified" appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  PulseMCP
&lt;/h3&gt;

&lt;p&gt;PulseMCP prioritizes openness and popularity signals. The GitHub MCP server is marked as "official" and the listing clearly identifies the provider (GitHub) and the responsible maintainer as GitHub. Visitor numbers and a popularity ranking serve as trust indicators, but there are no automated code or vulnerability checks.&lt;/p&gt;

&lt;p&gt;The platform links to the server.json file, which contains installation and configuration instructions, and displays details such as authentication type (OAuth) and transport protocol (Streamable HTTP). Red flags, such as missing maintainer information or severely outdated servers, are not explicitly highlighted. Therefore, users must consult the GitHub repository and other sources themselves for a security assessment.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Market
&lt;/h3&gt;

&lt;p&gt;MCP Market is primarily aimed at developers who want to browse by use case, for example developer tools, API development, and workflow-oriented categories like browser automation, web scraping, and web app testing. The GitHub MCP server is tagged with categories, a short description, and GitHub stars. However, the directory provides no security checks or dependency information, and maintainer verifications are missing.&lt;/p&gt;

&lt;p&gt;In addition, multiple entries with similar names can coexist, which can be confusing without additional checks. Security-relevant information, such as CVE scans (checks against the Common Vulnerabilities and Exposures database for known security issues), dependency lists, or "Report Issue" links, is not included in the listing. Developers must therefore rely on the linked repository and external security tools.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GitHub MCP Registry&lt;/th&gt;
&lt;th&gt;Glama&lt;/th&gt;
&lt;th&gt;PulseMCP&lt;/th&gt;
&lt;th&gt;MCP Market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability check&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Automated, with scorecard&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;GitHub login verification&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust signals&lt;/td&gt;
&lt;td&gt;GitHub stars only&lt;/td&gt;
&lt;td&gt;Security / license / quality scores&lt;/td&gt;
&lt;td&gt;Official/community classification, visitor count&lt;/td&gt;
&lt;td&gt;GitHub stars, rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reporting&lt;/td&gt;
&lt;td&gt;GitHub issues, deny list&lt;/td&gt;
&lt;td&gt;Issue reporting&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency notation&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Flagged in scorecard&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Glama is the clear outlier on security: it's the only directory running vulnerability scans (automated, but still viable), verifying maintainers, and surfacing dependency info. The rest of our directories leave that assessment up to you. PulseMCP's official/community classification helps with broad, initial trust signals, but it's not a meaningful substitute for actual code eval. If you're looking at servers for production use, Glama's scorecard saves you a step (depending on your acceptance criteria) that every other directory forces you to do externally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance, up-to-dateness, and avoiding dead ends
&lt;/h2&gt;

&lt;p&gt;The MCP landscape is changing quickly. New servers appear daily, while others are decommissioned. Developers need to pay attention to how current the entries in the directories are and whether outdated or broken servers are flagged. The table below shows a quick comparison of MCP server catalog's freshness across the four registries.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Github MCP Registry&lt;/th&gt;
&lt;th&gt;Glama&lt;/th&gt;
&lt;th&gt;PulseMCP&lt;/th&gt;
&lt;th&gt;MCP Market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total servers&lt;/td&gt;
&lt;td&gt;&amp;lt;100&lt;/td&gt;
&lt;td&gt;14K+&lt;/td&gt;
&lt;td&gt;~8K&lt;/td&gt;
&lt;td&gt;~20k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last catalog update&lt;/td&gt;
&lt;td&gt;Not displayed&lt;/td&gt;
&lt;td&gt;&amp;lt;24 hours ago&lt;/td&gt;
&lt;td&gt;&amp;lt;24 hours ago&lt;/td&gt;
&lt;td&gt;&amp;lt;1 hour ago&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorting options&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Date updated&lt;/td&gt;
&lt;td&gt;Last updated, recently released&lt;/td&gt;
&lt;td&gt;None, but offers latest MCP section&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Installation experience, from discovery to running code
&lt;/h2&gt;

&lt;p&gt;The following sections describe the installation and setup of the &lt;a href="https://dbhub.ai/" rel="noopener noreferrer"&gt;DBHub&lt;/a&gt; MCP server in a development environment. DBHub is a database MCP server that supports PostgreSQL, MySQL, SQL Server, SQLite, and MariaDB. DBHub also offers a demo mode that spins up a local service with sample data using a single npx command, making it easy to test without configuring databases manually. We tested its installation across the four directories using &lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; CLI on Mac to compare installation steps and time to first successful query.&lt;/p&gt;

&lt;p&gt;Let's start by going through the details page of the DBHub MCP server on each of the four directories to understand how developers can discover and install the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Github Registry
&lt;/h3&gt;

&lt;p&gt;The GitHub MCP Registry has a deep link that lets you install the MCP server directly in VS Code and use it with GitHub Copilot. But if you wanted to use it with a different agent (like Codex or Claude), the Registry simply displays the Readme.md file, and developers need to search through the file to understand the installation steps. The Readme.md file has quick start information to install dbhub, but it's not clear how one could use it as a MCP in an agent of your choice. After navigating through the Readme.md, I found a link to the &lt;a href="https://dbhub.ai/installation" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt; under the Installation section, which in turn contains a section for installation steps with different agents, including Claude Code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glama
&lt;/h3&gt;

&lt;p&gt;Similar to the GitHub MCP registry, Glama displays the Readme.md file of the MCP server. It also displays the schema for the MCP server, but apart from the server configuration section, DBHub's schema page mostly contained placeholder text for capabilities, tools, and resources.&lt;/p&gt;

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

&lt;p&gt;The info on the schema page wouldn't help the developer install the MCP, and as a result, the developers will need to rely on the Readme file for installation steps.&lt;/p&gt;

&lt;p&gt;Note, that the Glama details page for MCP server also has an Install Server button, but that was disabled for DBHub. Additionally, if enabled, the button will install the MCP to Glama's own web-based agent, not to the agent of your choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjqlde6mwxnufp3c0mh5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjqlde6mwxnufp3c0mh5.png" alt="Fig: Highlighting the inability to one-click install DBHub via Glama" width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Market
&lt;/h3&gt;

&lt;p&gt;Similar to other directories, MCP Market displays the server's Readme page. It also has an FAQ section that mentions that DBHub can be installed via Docker or npm, but it doesn't specify instructions for installing DBHub's MCP server. Developers who discover the server in this directory will still need to rely on the server's Readme for installation steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  PulseMCP
&lt;/h3&gt;

&lt;p&gt;PulseMCP displays the server.json file for the MCP server and recommends viewing the file to see installation instructions, configuration options, and usage guidelines. The server.json file contains the supported env variables, server identifier, and a link to the schema definition, but it's not immediately clear how to install the MCP server using this information. When I fed the entire JSON to Claude code, it tried installing the MCP server with Claude Desktop by editing the claude_desktop_config.json file, but when I ran the desktop app, it failed to load the MCP server. To install the MCP server, devs would need to click on Learn More from the directory's detail page and go through the docs to install it with an agent of their choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing DBHub with Claude Code
&lt;/h3&gt;

&lt;p&gt;To use the MCP, you first need to install DBHub with npm by executing the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @bytebase/dbhub@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the package, run the demo server by executing 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;dbhub &lt;span class="nt"&gt;--transport&lt;/span&gt; http &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can skip installation and use npx to start a demo server for testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @bytebase/dbhub@latest &lt;span class="nt"&gt;--transport&lt;/span&gt; http &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have a demo server running, you can navigate to the code repository where you want to install the DBHub MCP server and fire up Claude CLI:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Next, use the mcp add command to install the MCP server to the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http dbhub http://localhost:8080/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will add the MCP server to the project by creating or updating the project's .claude/mcp.json file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Added HTTP MCP server dbhub with URL: http://localhost:8080/mcp to &lt;span class="nb"&gt;local &lt;/span&gt;config
File modified: /Users/vivekmaskara/.claude.json &lt;span class="o"&gt;[&lt;/span&gt;project: /Users/vivekmaskara/Documents/Projects.nosync/mdh/mdedit.ai]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the MCP server is added, can run the /mcp command to verify the installation:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;When you execute the command, it should show dbhub as one of the installed tools, and you can press Enter to view more details about the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrrckxwnusae2xzpu94u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrrckxwnusae2xzpu94u.png" alt="Fig: Viewing the details about the installed DBHub MCP server" width="800" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To test the setup, we asked "Which tables are in the database?". Claude Desktop successfully connected to the DBHub MCP server and retrieved a complete list of all tables in the demo database. The response identified six tables: "department", "dept_emp", "dept_manager", "employee", "salary" and "title".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6huungupg8dqbmmqmx9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6huungupg8dqbmmqmx9u.png" alt="Fig: Successful test query of the installed DBHub MCP server using Claude Desktop" width="800" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire installation, from the initial search to the first successful query, took approximately an hour and 15 minutes, as we initially tried installing the MCP in Claude Desktop. Despite following the official docs, with Claude Desktop, DBHub's MCP server kept crashing due to npx or node compatibility issues. Additionally, after any MCP configuration, Claude Desktop needs to be fully restarted, not just the conversation window. This isn't immediately obvious and can be confusing if the server connection fails. Claude Desktop finally suggested trying DBHub with Claude Code, and when we tried following the installation steps for Claude Code, we were able to get everything working within 5-10 minutes.&lt;/p&gt;

&lt;p&gt;Finally, while the demo mode is good for testing, the documentation could provide more detailed information about which databases are included in the demo, how to switch to production databases, and which connection string formats are used for PostgreSQL, MySQL, SQL Server, and SQLite. If port 8080 is already in use by another application, the npx command will fail with a unique error message indicating which port is being used. In this case, users must manually select an alternative port using a parameter such as port 3000.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GitHub MCP Registry&lt;/th&gt;
&lt;th&gt;Glama&lt;/th&gt;
&lt;th&gt;PulseMCP&lt;/th&gt;
&lt;th&gt;MCP Market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install method&lt;/td&gt;
&lt;td&gt;VS Code deep link&lt;/td&gt;
&lt;td&gt;Install button (Glama agent only)&lt;/td&gt;
&lt;td&gt;server.json reference&lt;/td&gt;
&lt;td&gt;README display&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code support&lt;/td&gt;
&lt;td&gt;Not directly; requires README navigation&lt;/td&gt;
&lt;td&gt;Not directly; requires README navigation&lt;/td&gt;
&lt;td&gt;server.json misled Claude Code into wrong config in our test&lt;/td&gt;
&lt;td&gt;Not directly; requires README navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install guidance&lt;/td&gt;
&lt;td&gt;README with link to external install guide&lt;/td&gt;
&lt;td&gt;README plus schema page (mostly a placeholder)&lt;/td&gt;
&lt;td&gt;server.json with env variables and schema link&lt;/td&gt;
&lt;td&gt;README plus FAQ mentioning Docker/npm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our test was only able to complete a full end-to-end install from one directory (GitHub MCP Registry), and even that took 75 minutes before a failed Claude Desktop attempt led to a working Claude Code setup in 10. Glama's install button was disabled for DBHub and only targets its own agent, and PulseMCP's server.json appears to have misled Claude Code into a broken configuration.&lt;/p&gt;

&lt;p&gt;None of these directories function as installation tools for anything beyond their preferred client, if that. Expect to end up in the server's documentation regardless of where you discover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community and ecosystem
&lt;/h2&gt;

&lt;p&gt;The vitality of an MCP directory depends heavily on its community. How many people are actively involved? How easy is it to submit new servers? Are there support channels? And how vibrant is the overall environment?&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub MCP Registry
&lt;/h3&gt;

&lt;p&gt;The registry code is publicly developed on GitHub and now has over 6,200 stars, 73 watchers, and more than 550 forks. For discussions and questions, the registry documentation refers to the official MCP Discord server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glama
&lt;/h3&gt;

&lt;p&gt;This directory positions itself as a platform for "Search, compare, and connect to thousands of MCP servers." Currently, 14,274 servers are listed (as of January 9, 2026, last updated 11:08 AM). New servers can be submitted via "Add Server" in the directory. For discussion and support, Glama refers users to its own community channels, particularly Discord and Reddit.&lt;/p&gt;

&lt;h3&gt;
  
  
  PulseMCP
&lt;/h3&gt;

&lt;p&gt;This directory describes itself as "Everything MCP: servers, clients, use cases, tools, and newsletters." It lists over 7,600 servers and maintains a blog and a weekly newsletter. There is a submission page ("Submit") where new servers can be added via a form. An editorial curation process chooses "Top Picks." PulseMCP's guides refer to the MCP Discord server and advise users to contact the community with any problems. PulseMCP also publishes tutorials/articles on its website, so community interaction mainly takes place via website content, the newsletter, and Discord.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Market
&lt;/h3&gt;

&lt;p&gt;This portal aggregates more than 19,000 servers, presents star ratings and rankings, but makes little mention of community forums. Adding new servers appears to be done primarily via a submission form. The review process is opaque, and there's no clear public statement about its duration. There are no dedicated forums or Discord servers. It seems more like a website workflow plus any linked project pages (e.g., GitHub), if available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side comparison table
&lt;/h2&gt;

&lt;p&gt;The following matrix provides a quick-reference comparison of the four major MCP directories. Use this table to identify which platform best matches your specific needs and priorities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;GitHub MCP Registry&lt;/th&gt;
&lt;th&gt;Glama&lt;/th&gt;
&lt;th&gt;PulseMCP&lt;/th&gt;
&lt;th&gt;MCP Market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number of Servers (Jan 2026)&lt;/td&gt;
&lt;td&gt;~65&lt;/td&gt;
&lt;td&gt;14,000+&lt;/td&gt;
&lt;td&gt;7,640+&lt;/td&gt;
&lt;td&gt;19,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curation Approach&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Curated&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Verification&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Comprehensive&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search Quality&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-Click Install&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (with compatible clients)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE Integration&lt;/td&gt;
&lt;td&gt;Per-server basis&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Reviews/Ratings&lt;/td&gt;
&lt;td&gt;Yes (GitHub star ratings)&lt;/td&gt;
&lt;td&gt;Quality indicators&lt;/td&gt;
&lt;td&gt;Visitor metrics&lt;/td&gt;
&lt;td&gt;Yes (GitHub star ratings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update Frequency&lt;/td&gt;
&lt;td&gt;No recency signals&lt;/td&gt;
&lt;td&gt;High (timestamped)&lt;/td&gt;
&lt;td&gt;High (timestamped)&lt;/td&gt;
&lt;td&gt;High (timestamped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Community Size&lt;/td&gt;
&lt;td&gt;6,200+ stars, 73 watchers, 550+ forks&lt;/td&gt;
&lt;td&gt;Discord &amp;amp; Reddit community&lt;/td&gt;
&lt;td&gt;Discord, newsletter&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best For&lt;/td&gt;
&lt;td&gt;Official reference, standards compliance&lt;/td&gt;
&lt;td&gt;Production deployments, security-conscious teams&lt;/td&gt;
&lt;td&gt;Rapid prototyping, popular server discovery&lt;/td&gt;
&lt;td&gt;Use-case exploration, comprehensive browsing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fragmentation and security challenges
&lt;/h2&gt;

&lt;p&gt;After testing four directories across six criteria, the pattern is consistent: each platform optimizes for a different stage of the developer workflow, and none covers the full end-to-end lifecycle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The GitHub MCP Registry is the reference canonically&lt;/li&gt;
&lt;li&gt;Glama is the only directory doing meaningful security and quality assessments&lt;/li&gt;
&lt;li&gt;PulseMCP offers the broadest discovery surface&lt;/li&gt;
&lt;li&gt;MCP Market gives you contextual detail for evaluating fit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As far as best practices with what we have today, consider using two or three in combination: start with PulseMCP or MCP Market to find candidates, cross-reference against Glama's scorecards for production fit, and check the official registry (if it's on there) for standards adherence.&lt;/p&gt;

&lt;p&gt;But the bigger challenge isn't finding MCP servers. Just to go Reddit's &lt;a href="https://www.reddit.com/r/mcp/" rel="noopener noreferrer"&gt;/r/mcp&lt;/a&gt; sub, and you'll have a dozen dropped on your head before you can even ask. The real problem underlying all these directories is controlling what happens after you connect them.&lt;/p&gt;

&lt;p&gt;Every server in this comparison grants AI agents access to sensitive systems, like databases or code repositories. The directories we discussed may tell you whether a server is well-maintained or vulnerability-free, but it's not going to address what an agent is allowed to do once it's authenticated, which tools it can invoke, or whether a human needs to approve potentially risky actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplifying MCP security with Descope
&lt;/h2&gt;

&lt;p&gt;Finding a server from a directory is easy. Installing, based on our experience, maybe requires a little metaknowledge or a dedicated agent. But actually running it in production? For that, you need identity infrastructure that none of these MCP servers ship with.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.descope.com/use-cases/ai" rel="noopener noreferrer"&gt;Descope Agentic Identity Hub&lt;/a&gt; is a dedicated identity provider for AI agents and MCP servers that enables MCP developers to easily connect their internal and external-facing MCP servers with AI agents.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Securely expose MCP servers to MCP clients with OAuth 2.1 and PKCE.&lt;/li&gt;
&lt;li&gt;Use a dedicated &lt;a href="https://www.descope.com/blog/post/python-mcp-sdk" rel="noopener noreferrer"&gt;Python MCP SDK&lt;/a&gt; to add auth to any MCP server built in Python.&lt;/li&gt;
&lt;li&gt;Validate access to MCP servers with user auth and consent management.&lt;/li&gt;
&lt;li&gt;Support context-aware MCP client registration through DCR and CIMD with agent risk assessment flows.&lt;/li&gt;
&lt;li&gt;Assign granular per-agent and per-tool scopes to MCP clients.&lt;/li&gt;
&lt;li&gt;Manage, monitor, and delete MCP clients connected to MCP servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Descope, access is granted (or denied) at the function level, not API-wide, and policies adapt without touching your codebase. &lt;a href="https://www.descope.ai/" rel="noopener noreferrer"&gt;See how Descope secures agentic workflows&lt;/a&gt;, or start building the MCP server flow your project needs with a &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;Free Forever account&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
      <category>tooling</category>
    </item>
    <item>
      <title>What Is OAuth Token Exchange?</title>
      <dc:creator>Mrunank Pawar</dc:creator>
      <pubDate>Tue, 26 May 2026 17:35:21 +0000</pubDate>
      <link>https://dev.to/descope/what-is-oauth-token-exchange-1dd0</link>
      <guid>https://dev.to/descope/what-is-oauth-token-exchange-1dd0</guid>
      <description>&lt;p&gt;This blog was originally published on &lt;a href="https://www.descope.com/learn/post/oauth-token-exchange" rel="noopener noreferrer"&gt;Descope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;OAuth Token Exchange is a grant type defined in OAuth 2.0 (&lt;a href="https://datatracker.ietf.org/doc/html/rfc8693" rel="noopener noreferrer"&gt;RFC 8693&lt;/a&gt;) that allows a client to present an existing &lt;a href="https://www.descope.com/learn/post/token-based-authentication" rel="noopener noreferrer"&gt;security token&lt;/a&gt; to an &lt;a href="https://www.descope.com/learn/post/authorization-server" rel="noopener noreferrer"&gt;authorization server&lt;/a&gt; and receive a new token in return, scoped and addressed for the next destination in a request chain. It operates entirely between services and the authorization server, on the basis of a token already in hand. Most &lt;a href="https://www.descope.com/learn/post/oauth" rel="noopener noreferrer"&gt;OAuth&lt;/a&gt; grant types assume a contained interaction in which a client gets authorized, receives a token, and uses it. OAuth Token Exchange addresses the cases this common model doesn't cover, like when a token needs to travel across service boundaries, change its audience, shed unnecessary permissions, or carry an explicit record of who is acting on whose behalf. The growing relevance of token exchange is directly tied to the rise of agentic identity systems, where a single user authorization may need to propagate through an unpredictable sequence of services and tools.&lt;/p&gt;

&lt;p&gt;This post explains how OAuth Token Exchange works, the distinction between its two core patterns (impersonation and delegation), when to use it, and what to consider when implementing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How OAuth Token Exchange works
&lt;/h2&gt;

&lt;p&gt;A token exchange request is a POST to the authorization server's token endpoint, the same endpoint used by other OAuth grant types. The client presents a subject token, which is the existing credential representing the party on whose behalf the new token is being requested, along with a parameter identifying what type of token it is. The type identifier tells the authorization server how to parse and validate what it receives: registered types cover &lt;a href="https://www.descope.com/learn/post/access-token" rel="noopener noreferrer"&gt;access tokens&lt;/a&gt;, &lt;a href="https://www.descope.com/learn/post/id-token" rel="noopener noreferrer"&gt;ID tokens&lt;/a&gt;, &lt;a href="https://www.descope.com/learn/post/jwt" rel="noopener noreferrer"&gt;JWTs&lt;/a&gt; in a general sense, and &lt;a href="https://www.descope.com/learn/post/saml" rel="noopener noreferrer"&gt;SAML&lt;/a&gt; assertions, among others.&lt;/p&gt;

&lt;p&gt;Several optional parameters shape what comes back from this initial arc of the 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%2F77pm1frgwvu5d4xbpbkd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77pm1frgwvu5d4xbpbkd.png" alt="Fig: OAuth Token Exchange parameters" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.descope.com/blog/post/jwt-aud-claim" rel="noopener noreferrer"&gt;audience&lt;/a&gt; parameter specifies who the new token is for (the aud claim in a JWT), like which resource servers or services the token is valid for. Setting the audience claim in the exchange request pins the issued token to a specific target, preventing cross-server reuse. The scope parameter requests the &lt;a href="https://www.descope.com/learn/post/access-control" rel="noopener noreferrer"&gt;permissions&lt;/a&gt; the new token should carry (typically narrower than what the subject token holds).&lt;/p&gt;

&lt;p&gt;An actor token can be included when the requesting service needs to identify itself as a party distinct from the subject, which is the mechanism that enables delegation rather than impersonation (more on that in the next section).&lt;/p&gt;

&lt;p&gt;A requested token type parameter tells the authorization server what format to issue.&lt;/p&gt;

&lt;p&gt;The response follows the standard shape established by OAuth: the issued token is returned in the access token field, a field name reused for historical reasons since the returned credential is not necessarily an OAuth access token in the functional sense. Crucially, presenting a token for exchange does not invalidate it. The subject token remains valid unless separately revoked. One detail worth mentioning is that RFC 8693 leaves client authentication requirements up to each implementation. The spec permits unauthenticated exchanges, but that flexibility is not a recommendation. Allowing any caller to exchange tokens without proving its own identity means anyone who obtains a valid token can potentially trade it for another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth Token Exchange impersonation vs. delegation
&lt;/h2&gt;

&lt;p&gt;The most significant design decision in a token exchange implementation is whether the issued token uses impersonation or delegation patterns. The spec defines both for different contexts, and they each have meaningful considerations for &lt;a href="https://docs.descope.com/audit-trails-and-integrations" rel="noopener noreferrer"&gt;audit trail clarity&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impersonation:&lt;/strong&gt; When the requesting service takes on the complete identity of the subject. The resource server receiving the token sees only the original user, with no indication that an intermediate service was involved. This is appropriate when the downstream service has no need for that information, and the intermediate service is fully trusted to act within the bounds of the original authorization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delegation:&lt;/strong&gt; When the requesting service retains its own identity while acting on behalf of the subject. The issued token is a composite: it carries both the subject (the original user or entity, or sub) and the actor (the requesting service, or act). The actor claim is a JSON object embedded in the issued JWT containing claims about the acting party. A resource server processing a delegation token can determine both who authorized the action and which service executed it.&lt;/p&gt;

&lt;p&gt;The key difference between these patterns comes down to audit trail clarity. Impersonation produces logs that look more or less identical to direct user actions, which is what you want when the intermediate service should be invisible. It's the opposite of what you want when you need to reconstruct what happened across a request chain, however. For multi-agent systems in particular, where an orchestrator agent may dispatch several sub-agents each calling different downstream services, delegation is the appropriate pattern. RFC 8693 also defines an authorized actor claim (may_act) that subject tokens can carry. A token with this claim pre-authorizes specific parties to exchange it, and the authorization server can inspect it when evaluating whether to honor the request. This gives the original token issuer a degree of control over how and by whom the token can be forwarded.&lt;/p&gt;

&lt;p&gt;Whether impersonation or delegation applies is determined by the request itself: a subject token alone produces impersonation, since there is no actor identity to include. Adding an actor token enables delegation, and the authorization server can issue a composite token carrying both identities.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth Token Exchange use cases
&lt;/h2&gt;

&lt;p&gt;Token exchange applies broadly whenever authorization needs to cross a service boundary without re-prompting the user or forwarding a token that wasn't issued for its next destination. The examples below represent the most common patterns (but aren't an exhaustive list).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microservices and internal service calls:&lt;/strong&gt; When a request travels through several backend services before reaching its destination, forwarding the original token at each hop creates problems. Most critically, it was issued for a specific client with a specific audience, so downstream services may reject it, and it likely carries more scope than any individual downstream service actually needs. Exchanging tokens lets each service trade the token it holds for one correctly scoped and addressed for the next leg of its journey, while the original user's identity travels through intact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agent delegation:&lt;/strong&gt; When a user authorizes an agent to act on their behalf, that agent may need to call several downstream APIs or &lt;a href="https://www.descope.com/learn/post/mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; servers in sequence. Rather than forwarding the original user token or requiring the user to re-authorize at each step, the agent exchanges its delegated token for one scoped specifically to each destination. The delegation chain remains intact: each downstream service can verify who authorized the action and which agent executed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-domain federation:&lt;/strong&gt; When a token is issued by an external identity provider (a SAML assertion from an enterprise identity provider, or a JWT from a third-party service) it cannot be used directly with a resource server that only trusts tokens from its own authorization server. Exchanging tokens bridges the gap: the external token is presented as the subject token, the local authorization server validates it, maps the identity to a local principal (an entity that can be authenticated and granted access, like a user or service), and issues a token in the local trust domain. This is particularly relevant for enterprise federation scenarios where multiple identity providers are in play.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token exchange in agentic identity
&lt;/h2&gt;

&lt;p&gt;The delegation model maps directly onto how agentic authorization needs to work at scale. When a user grants an agent permission to act on their behalf, that authorization event produces a delegated token identifying both the user and the agent. What happens next is where token exchange becomes the operative mechanism.&lt;/p&gt;

&lt;p&gt;Agents rarely call a single service, especially for long-running or complex tasks. An orchestrating agent in a multi-agent system may invoke several tools and sub-agents in sequence, each requiring its own scoped credential. A sub-agent spawned mid-task may need to call a downstream API the user never directly interacted with. In each case, the agent exchanges the delegated token it holds for one scoped and addressed to the immediate destination, rather than forwarding the original credential or requesting a new authorization entirely. The user's identity and the agent's identity both travel through each exchange, satisfying the resource server's need to know who authorized the action and maintaining the full audit trail across every hop.&lt;/p&gt;

&lt;p&gt;This also handles the revocation case cleanly. Because each token in the chain is issued with a specific audience and scope, revoking the user's original authorization propagates through the system: the authorization server stops honoring exchanges against a revoked subject token, and downstream services that validate tokens against the issuer's key material will reject any that have expired or been revoked. The chain breaks at the point of revocation rather than requiring per-service cleanup.&lt;/p&gt;

&lt;p&gt;Multi-agent systems, where one agent delegates to another (which may delegate yet another sub-agent, and so on), require explicit policy at each hop to prevent scope from persisting unchanged through the chain. The authorized actor claim (may_act) gives each link in that chain a mechanism for pre-authorizing the next actor, but the enforcement logic is an implementation responsibility, not something the spec provides automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices and security considerations
&lt;/h2&gt;

&lt;p&gt;Token exchange introduces a few implementation decisions that affect security posture in ways other grants don't. The considerations below apply across most deployments, though the specific requirements will vary depending on the trust model and pattern (delegation vs. impersonation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope only to what is needed:&lt;/strong&gt; Exchanged tokens should carry equal or narrower scope than the subject token (and most authorization servers wouldn't allow scope elevation via token exchange by default). Each exchange is an opportunity to narrow scopes to what the next service actually needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticate the requesting client:&lt;/strong&gt; A confidential client performing a token exchange should authenticate at the token endpoint just as it would for the OAuth Client Credentials Flow. Anonymous token exchange means anyone in possession of a valid token (including an attacker who obtained it through other means) can potentially trade it for a new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design explicit policy for multi-hop delegation:&lt;/strong&gt; The authorized actor claim enables pre-authorization of which actors may exchange a given token, but does not automatically enforce scope reduction across hops. If Agent A delegates to Agent B, which delegates to Agent C, explicit policy at each step is required to prevent the original scope from persisting unchanged through the entire chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revoke input tokens deliberately:&lt;/strong&gt; A token exchange does not revoke the subject token automatically. After exchange, audit whether the subject token needs to continue being valid. If it doesn't, revoke it. Leaving it live creates a window where two tokens with overlapping authority are both valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer delegation over impersonation in agentic contexts:&lt;/strong&gt; When intermediate services are part of an observable, auditable workflow, delegation produces tokens that accurately reflect what happened: which user authorized the action and which agent executed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation example of OAuth Token Exchange
&lt;/h2&gt;

&lt;p&gt;We'll use Descope's &lt;a href="https://docs.descope.com/mcp/integrations/skyflow" rel="noopener noreferrer"&gt;integration with Skyflow for MCP servers&lt;/a&gt; in the following example. In this scenario, Descope's authorization server exposes a token exchange endpoint as part of its OAuth 2.1 infrastructure. When the MCP server is configured in Descope, the .well-known &lt;a href="https://www.descope.com/learn/post/oidc" rel="noopener noreferrer"&gt;OpenID&lt;/a&gt; configuration includes the authorization, token exchange, &lt;a href="https://www.descope.com/learn/post/jwks" rel="noopener noreferrer"&gt;JWKS&lt;/a&gt;, and &lt;a href="https://www.descope.com/learn/post/dynamic-client-registration" rel="noopener noreferrer"&gt;Dynamic Client Registration (DCR)&lt;/a&gt; endpoints. A Descope access token issued after user authentication and consent is exchanged for a Skyflow bearer token at runtime. That Skyflow token carries contextual information about the user's role and drives row-level security and privacy policy enforcement before any data is returned to the agent. The user identity travels through the exchange; the downstream service makes authorization decisions based on who the user is, not which service is calling.&lt;/p&gt;

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

&lt;p&gt;More broadly, Descope's &lt;a href="https://www.descope.ai/" rel="noopener noreferrer"&gt;Agentic Identity Hub&lt;/a&gt; features capabilities using the delegation model that token exchange enables. When a user authorizes an AI agent through Descope's consent flow, the agent receives scoped credentials tied back to that user. When the agent calls a downstream service, those credentials include the chain of custody (who authorized the action, which agent executed it, etc.). That traceability is what makes agent access auditable and revocable after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The value of OAuth Token Exchange in modern identity
&lt;/h2&gt;

&lt;p&gt;OAuth Token Exchange fills a crucial gap that other grant types don't: what happens when authorization has been established and a request needs to move through multiple services before it reaches its destination? By providing a clear answer to and standard for that question, OAuth Token Exchange makes it possible to preserve identity context, enforce least privilege at each hop, and maintain an audit trail across complex request chains without re-prompting the user.&lt;/p&gt;

&lt;p&gt;Its relevance has grown exponentially with the rise of agentic identity. The delegation patterns defined in this spec, specifically the actor and authorized actor claims, give agentic systems a principled way to represent the relationship between a user's authorization and the agent acting on their behalf, at every leg of a multi-service (or multi-agent) workflow.&lt;/p&gt;

&lt;p&gt;Descope's Agentic Identity Hub is designed to put these patterns into practice, providing the delegation infrastructure, consent flows, and audit streaming integration that makes agent access governable and revocable. To get started securing your agentic project, &lt;a href="https://www.descope.com/sign-up" rel="noopener noreferrer"&gt;sign up for a Free Forever Descope Account&lt;/a&gt;, join the &lt;a href="https://www.descope.com/community" rel="noopener noreferrer"&gt;AuthTown dev community&lt;/a&gt;, and &lt;a href="https://www.descope.com/demo" rel="noopener noreferrer"&gt;reach out to our auth experts&lt;/a&gt; to learn more.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs about OAuth Token Exchange
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is OAuth Token Exchange?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OAuth Token Exchange is a grant type defined in RFC 8693 that allows a service to present an existing security token to an authorization server and receive a new one in return. The new token may have a different audience, narrower scope, different format, or carry delegation semantic identifying which service is acting on whose behalf. It operates entirely between services with no user-facing redirect or re-authorization, and is increasingly used in agentic systems where a single user authorization needs to propagate through multiple downstream services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is OAuth Token Exchange the same as refreshing a token?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Token refresh is for receiving a new access token for the same client, audience, and authorization context. Token exchange is a different mechanism: it takes an existing token and issues a new token that may have a different audience, scope, or format, or carry delegation patterns identifying a separate acting party. Refresh is about extending a session, while token exchange is about crossing a service boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does token exchange work with non-JWT token formats?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The spec defines type identifiers for several token formats, including opaque tokens and SAML assertions, in addition to JWTs. The type identifier parameter tells the authorization server what format it's receiving so it knows how to validate the subject token. What format comes back depends on what the authorization server supports and what the client requests via the requested token type parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can token exchange be used to elevate permissions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The spec doesn't prohibit it, but it isn't an intended use and most authorization servers won't allow it by default. Token exchange is designed to narrow authorization scope at each hop, not expand it. A deployment that permits scope elevation via exchange would need to be explicitly configured that way, and doing so would undermine the least-privilege intent of the mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to the original token after exchange?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It stays valid. Performing a token exchange does not revoke the subject token. If the original token shouldn't remain usable after the exchange, it needs to be revoked separately through whatever revocation mechanism the authorization server provides.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>security</category>
    </item>
    <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>
  </channel>
</rss>
