惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
Project Zero
Project Zero
K
Kaspersky official blog
G
Google Developers Blog
T
Threat Research - Cisco Blogs
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
Y
Y Combinator Blog
Recorded Future
Recorded Future
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
Microsoft Security Blog
Microsoft Security Blog
H
Help Net Security
S
Schneier on Security
P
Palo Alto Networks Blog
H
Hacker News: Front Page
N
News and Events Feed by Topic
N
Netflix TechBlog - Medium
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
SecWiki News
SecWiki News
Cloudbric
Cloudbric
TaoSecurity Blog
TaoSecurity Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Hacker News
The Hacker News
C
Check Point Blog
L
LangChain Blog
腾讯CDC
小众软件
小众软件
T
Tenable Blog
Google DeepMind News
Google DeepMind News
GbyAI
GbyAI
L
LINUX DO - 最新话题
A
About on SuperTechFans
Google Online Security Blog
Google Online Security Blog
C
Cisco Blogs
Recent Announcements
Recent Announcements
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
雷峰网
雷峰网
美团技术团队
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Full Disclosure
博客园_首页

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Secure API Calling With Custom GPTs and Descope
Mrunank Pawa · 2026-05-04 · via DEV Community

This blog was originally published on Descope.

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 Model Context Protocol (MCP) 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.

With Descope Inbound Apps, 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:

  • Securing your FastAPI backend with Descope JWT validation
  • Configuring a custom GPT to authenticate and interact with your protected APIs using scope-based authorization
  • 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.

Also read: Descope MCP documentation

What you will build: a secure GPT assistant

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.

Fig: Inbound app consent flow OAuth screen

Once you are authenticated, you will be brought to the consent screen, which will display a list of scopes that you can consent to.

Fig: Inbound app consent flow consent screen

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.

Fig: GPT asks for consent to interface with your API

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.

Fig: GPT privacy settings

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.

Prerequisites

To complete the tutorial, you will need the following:

  • Python installed
  • A deployed FastAPI application; you can find a sample starter app here
  • ChatGPT Plus account

Setting up a Descope project

To create a new Descope project, sign up or sign in 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 Getting Started 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 Flows section, you will see that a handful of flows have automatically been created for you. Descope Flows 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.

Fig: Descope console flows default homepage

You can access your project ID at any time by navigating to the Project section of the Descope console. You can copy the project ID from the General tab.

Fig: Accessing your Descope project ID

Configuring your application environment

In the root folder of your FastAPI application's project, create a .env file if you do not already have one, and add the following variables:

DESCOPE_PROJECT_ID=<Your Descope project ID> 
DESCOPE_INBOUND_APP_CLIENT_ID=<Inbound App client ID>
DESCOPE_INBOUND_APP_CLIENT_SECRET=<Inbound App client secret>
DESCOPE_API_BASE_URL="https://api.descope.com" # or your custom domain if one is configured in your project settings

Enter fullscreen mode Exit fullscreen mode

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.

Securing your app through token validation

Descope implements its session and refresh tokens 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 custom JWT authorizer 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.

First, you will implement a simple exception handler. In your FastAPI application, create a new file and name it exceptions.py. Add these two exception definitions:

from fastapi import HTTPException, status

class UnauthenticatedException(HTTPException):
    def __init__(self):
        super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")

class UnauthorizedException(HTTPException):
    def __init__(self, detail: str = "Not authorized"):
        super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail)

Enter fullscreen mode Exit fullscreen mode

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:

https://api.descope.com/<Your Descope Project ID>/.well-known/jwks.json

Enter fullscreen mode Exit fullscreen mode

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

At a high level, the TokenVerifier 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 SecurityScopes library to validate and enforce the scopes embedded in the token:

from typing import Optional, List
import os
import jwt
from jwt import PyJWKClient
from fastapi import Depends
from fastapi.security import SecurityScopes, HTTPAuthorizationCredentials, HTTPBearer

from app.exceptions import UnauthenticatedException, UnauthorizedException

jwks_url = f"https://api.descope.com/{os.getenv('DESCOPE_PROJECT_ID')}/.well-known/jwks.json"

class TokenVerifier:
    def __init__(self):
        self.config = get_settings()
        self.jwks_client = PyJWKClient(jwks_url)
        self.allowed_algorithms = ["RS256"]

    async def __call__(
        self,
        security_scopes: SecurityScopes,
        # token injected by FastAPI Security, specified in the FastAPI route definition
        token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer())
    ):
        if token is None:
            raise UnauthenticatedException

        token = token.credentials

        key = self._get_signing_key(token)
        payload = self._decode_token(token, key)

        if security_scopes.scopes:
            self._enforce_scopes(payload, security_scopes.scopes)

        return payload

    def _get_signing_key(self, token: str):
        try:
            return self.jwks_client.get_signing_key_from_jwt(token).key
        except Exception as e:
            raise UnauthorizedException(f"Failed to fetch signing key: {str(e)}")

    # helper which calls jwt.decode()
    def _decode_token(self, token: str, key):
        try:
            project_id = os.getenv("DESCOPE_PROJECT_ID")
            issuer_candidates = [
                f'https://api.descope.com/v1/apps/{project_id}', 
                project_id
            ]
            return jwt.decode(
                token,
                key,
                algorithms=self.allowed_algorithms,
                issuer=issuer_candidates,
                audience=project_id
            )
        except Exception as e:
            raise UnauthorizedException(f"Token decoding failed: {str(e)}")

Enter fullscreen mode Exit fullscreen mode

To restrict access to specific API routes based on the scopes carried by the incoming JWT token, add one more method to the TokenVerifier class that checks the token's scope claim. After successful validation, it reads the token's scope claim, compares it to the route's required scopes, and rejects the request if any are missing.

def _enforce_scopes(self, payload: dict, required_scopes: List[str]):
        scope_claim = payload.get("scope")
        if scope_claim is None:
            raise UnauthorizedException('Missing required claim: "scope"')

        scopes = scope_claim.split() if isinstance(scope_claim, str) else scope_claim
        missing = [scope for scope in required_scopes if scope not in scopes]

        if missing:
            raise UnauthorizedException(
                f'Missing required scopes: {", ".join(missing)}'
            )

Enter fullscreen mode Exit fullscreen mode

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 enterprise MCP security challenges blog.

You may already have route protection configured, but if you do not, set it up as follows:

import urllib.request
import os
from app.auth import TokenVerifier

# Set a custom User-Agent to avoid being blocked by security filters or rate limiters.
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0 (DescopeFastAPISampleApp)')]
urllib.request.install_opener(opener)

auth = TokenVerifier() 

@app.get("/api/private")
def private(auth_result: str = Security(auth)):
    # This API is now protected by our TokenVerifier object `auth`
    return auth_result

@app.get("/api/private-scoped/read")
def private_scoped(auth_result: str = Security(auth, scopes=['read:messages'])):
    """
    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.
    """
    return auth_result

Enter fullscreen mode Exit fullscreen mode

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 Validating JWTs in FastAPI doc.

Configuring your FastAPI app to be an authorization server

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.

You will expose three endpoints that your custom GPT will treat as its OAuth server:

  • GET /authorize – forwards the browser to Descope's /authorize endpoint
  • GET /api/oauth/callback – 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.
  • POST /token – forwards the GPT's back-channel token exchange to Descope's /token endpoint

Authorization endpoint

The /authorize 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 response_type, redirect_uri, scope, and state. The endpoint performs several critical functions to ensure a secure OAuth flow:

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

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.

@app.get("/authorize")
async def authorize(
    request: Request,
    response_type: Optional[str] = None,
    redirect_uri: Optional[str] = None,
    scope: Optional[str] = None,
    state: Optional[str] = None
):
    """
    OAuth 2.0 Authorization Endpoint - Proxies to Descope

    This endpoint forwards OAuth authorization requests to Descope's Inbound Apps.
    """
    try:
        # Validate required parameters
        if not redirect_uri or not response_type:
            print(f"Missing required parameters")
            raise HTTPException(
                status_code=400
            )

        # Validate response_type
        if response_type != "code":
            print(f"Unsupported response_type: {response_type}")
            raise HTTPException(
                status_code=400
            )

        # Get client ID from environment or use the provided one
        descope_client_id = os.getenv("DESCOPE_INBOUND_APP_CLIENT_ID")
        if not descope_client_id:
            print("OAuth client credentials not configured")
            raise HTTPException(
                status_code=500,
            )

        # Get the base URL from the request
        base_url = str(request.base_url).rstrip('/')
        callback_url = f"{base_url}/api/oauth/callback"

        # Construct query parameters
        params = {
            "client_id": descope_client_id,
            "redirect_uri": callback_url,
            "response_type": "code",
            "scope": scope or "openid",
            "state": state or ""  # Just pass through the state parameter
        }

        # Build the full URL with query parameters
        query_string = "&".join([f"{k}={v}" for k, v in params.items()])
        full_url = f"https://api.descope.com/oauth2/v1/apps/authorize?{query_string}"

        print(f"Redirecting to Descope: {full_url}")

        # Redirect to Descope's authorization endpoint
        return RedirectResponse(url=full_url)

    except HTTPException:
        raise
    except Exception as error:
        print(f"Authorization endpoint error: {error}")
        raise HTTPException(
            status_code=500
        )

Enter fullscreen mode Exit fullscreen mode

Token exchange endpoint

The /token 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:

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

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.

@app.post("/token")
async def token(
    request: Request
):
    """
    OAuth 2.0 Token Endpoint - Proxies to Descope

    This endpoint forwards token exchange requests to Descope's Inbound Apps.
    """
    print("Token exchange request received")

    try:
        # Parse the request body based on content type
        content_type = request.headers.get("content-type", "")
        print(f"Request content-type: {content_type}")

        if "application/json" in content_type:
            body = await request.json()
        elif "application/x-www-form-urlencoded" in content_type:
            form_data = await request.form()
            body = dict(form_data)
        else:
            # Try to parse as JSON first, then as form data
            try:
                body = await request.json()
            except:
                form_data = await request.form()
                body = dict(form_data)

        grant_type = body.get("grant_type")
        code = body.get("code")
        client_id = os.getenv("DESCOPE_INBOUND_APP_CLIENT_ID")
        client_secret = os.getenv("DESCOPE_INBOUND_APP_CLIENT_SECRET")

        # Validate required parameters
        if not grant_type or not code or len(client_id) == 0 or len(client_secret) == 0:
            raise HTTPException(
                status_code=400
            )

        # Only support authorization_code grant type
        if grant_type != "authorization_code":
            raise HTTPException(
                status_code=400
            )

        # Get the base URL from the request
        base_url = str(request.base_url).rstrip('/')
        callback_url = f"{base_url}/api/oauth/callback"

        # Forward the request to Descope's token endpoint
        token_request_body = {
            "grant_type": "authorization_code",
            "client_id": client_id,
            "client_secret": client_secret,
            "code": code,
            "redirect_uri": callback_url
        }

        descope_url = "https://api.descope.com/oauth2/v1/apps/token"

        async with httpx.AsyncClient() as client:
            print("Sending request to Descope token endpoint")
            response = await client.post(
                descope_url,
                data=token_request_body,
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )

            descope_data = response.json()

            # If Descope returned an error, log it
            if response.status_code >= 400:
                print(f"Descope token exchange failed: {descope_data}")

            # Return the response from Descope
            return descope_data

    except HTTPException:
        raise
    except Exception as error:
        print(f"Token endpoint error: {error}")
        raise HTTPException(
            status_code=500
        )

Enter fullscreen mode Exit fullscreen mode

Callback endpoint

The /api/oauth/callback 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:

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

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.

@app.get("/api/oauth/callback")
async def oauth_callback(
    code: Optional[str] = None,
    state: Optional[str] = None,
    error: Optional[str] = None,
    error_description: Optional[str] = None
):
    """
    OAuth 2.0 Callback Endpoint

    Handles the callback from Descope and redirects back to Custom GPT.
    """
    try:
        # Handle errors from Descope
        if error:
            raise HTTPException(
                status_code=400,
                detail={
                    "error": error,
                    "error_description": error_description
                }
            )

        if not code:
            raise HTTPException(
                status_code=400,
                detail={
                    "error": "invalid_request",
                    "error_description": "No authorization code received"
                }
            )

        # Custom GPT callback URL - obtained after creating your GPT
        custom_gpt_callback = "<Your GPT callback URL>"

        # Build redirect URL back to Custom GPT
        redirect_url = f"{custom_gpt_callback}?code={code}"
        if state:
            redirect_url += f"&state={state}"

        return RedirectResponse(url=redirect_url)

    except HTTPException:
        raise
    except Exception as error:
        raise HTTPException(
            status_code=500,
            detail={
                "error": "server_error",
                "error_description": "Internal server error"
            }
        )

Enter fullscreen mode Exit fullscreen mode

Setting up an Inbound App

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.

To create a new Descope Inbound App, navigate to the Inbound Apps 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.

Fig: Creating a Descope Inbound App

Scoping your Inbound App

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.

Fig: Configuring Inbound App scopes

Client ID and secret

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 /authorize and /token routes of our FastAPI app. The most important values here are the Client ID and Client Secret. Copy and paste those values into your .env file for your DESCOPE_INBOUND_APP_CLIENT_ID and DESCOPE_INBOUND_APP_CLIENT_SECRET variables, respectively.

Fig: Inbound App connection information

User consent flow

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.

Custom session token expiration

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.

Fig: Setting a custom token expiration time

Your Inbound App is now fully configured and ready to use!

Configuring your custom GPT

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 GPTs tab 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.

Fig: GPTs homepage

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.

Fig: GPT configure page

Configuring OAuth for your GPT

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:

  • Authorization URL: this is your API route that handles the initial call to the Descope authorize endpoint (e.g. app/authorize).
  • Token URL: this is the API route that handles the code and token exchange at the Descope token endpoint (e.g. app/token).
  • Token Exchange Method: 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.

Click Save once you have filled out the appropriate fields.

Fig: OAuth configuration for GPT

Creating an OpenAPI spec

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 this Descope sample app. FastAPI easily generates a JSON file for you, simply enter the URL where your application is hosted followed by /openapi.json. If you utilize this option, you will have to add a servers object, where you will tell the GPT the URL where it can find your API server.

"servers": [
    {
      "url": "https://<Your base URL>"
    }
]

Enter fullscreen mode Exit fullscreen mode

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 > 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 custom_gpt_callback variable in the /api/oauth/callback route.

GPTs with Descope

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.

To explore Descope further, sign up for a free account, join our developer Slack community, or follow us on LinkedIn.