Pushed Authorization Requests (PAR)
Pushed Authorization Requests (PAR) (RFC 9126) let your client send its authorization request parameters straight to Descope from your server, before it redirects the user. The redirect to /authorize then carries only a short request_uri that points to those parameters.
Descope supports PAR for Inbound Apps, where Descope is the authorization server for third-party apps and agents, and for Federated OIDC Apps, where Descope is your app's OpenID Connect provider. PAR works with the authorization code grant only.
Why Use PAR
In a standard authorization code flow, the client puts every authorization parameter in the browser redirect: scope, redirect_uri, state, the PKCE code_challenge, and more. The parameters travel through the user's browser as a query string, so browser history, referrer headers, proxies, and browser extensions can read them. Anything in that path can also change them before they reach Descope. Descope also can't reject a bad parameter until the user is already on the authorize page.
PAR moves those parameters to the back channel. Your server sends them to Descope over a direct TLS call, Descope validates them right away, and the browser only sees client_id and an opaque reference:
| Standard authorize request | With PAR | |
|---|---|---|
| Where the parameters travel | The browser URL, as a query string | A direct POST from your server to Descope |
| Who can read or change them | Anything that can see the browser URL | Only your server and Descope |
| When Descope validates them | When the browser reaches /authorize | When your server pushes them, before any redirect |
| Authorize URL | Long, with every parameter | Short: client_id and request_uri |
High-security profiles such as FAPI 2.0 require PAR. Even without a compliance requirement, PAR is a good default for confidential clients, because it keeps the request out of reach of the browser at no cost to the user.
How It Works
A PAR flow is an authorization code flow with one extra call at the start. Your server pushes the parameters and gets back a request_uri, and the rest of the flow continues as usual:
Select any step to see what happens. The authorization parameters go straight from your server to Descope, and the browser only ever carries a short reference to them.
The request_uri has the form urn:ietf:params:oauth:request_uri:<id>. Descope deletes it the moment /authorize uses it, so it works exactly once, and it expires after the number of seconds in expires_in, 60 by default. Push a new request for every sign-in attempt, right before you redirect.
PAR Endpoints
Which endpoint you push to depends on the kind of app. Every endpoint takes the same request body and returns the same response:
| App type | PAR endpoint | API reference |
|---|---|---|
| Inbound App | __BaseURL__/oauth2/v1/apps/par | |
| Inbound App, with the project ID in the URL | __BaseURL__/oauth2/v1/apps/{projectId}/par | |
| Federated OIDC App (project-level) | __BaseURL__/oauth2/v1/par | OIDC PAR |
| Federated OIDC App (specific SSO app) | __BaseURL__/{ssoAppId}/oauth2/v1/par | OIDC PAR (SSO app) |
Rather than hard-coding the path, read it from the pushed_authorization_request_endpoint field of the discovery document. For an Inbound App, that's the app's Discovery URL, described in Discovery and JWKs. For a Federated OIDC App, it's the Well-Known OIDC configuration. If you use a custom domain, the discovery document already uses it.
{
"pushed_authorization_request_endpoint": "__BaseURL__/oauth2/v1/apps/par",
"request_uri_parameter_supported": false,
"code_challenge_methods_supported": ["S256"]
}request_uri_parameter_supported is false on purpose. Descope accepts a request_uri only when its own PAR endpoint issued it. A client can't pass a request_uri that points to a request object it hosts somewhere else.
Pushing the Request
Your server sends a POST with a JSON body. The body holds the same parameters you would otherwise put on the authorize URL, plus client authentication:
client_id: The Inbound App's client ID. For a Federated OIDC App, this is your Project ID.- Client authentication: A
client_secretin the body, HTTP Basic authentication, or a signed JWT inclient_assertionwithclient_assertion_type(RFC 7523). For a Federated OIDC App, the secret is an Access Key, as described in Configuring your OIDC client. response_type: Must becode.scope,redirect_uri,state, andnonce: The same values you would send to/authorize. Descope validatesredirect_uriagainst the app's approved redirect URLs now, at push time.code_challengeandcode_challenge_method: The PKCE challenge. The only supported method isS256.dpop_jkt(optional): The thumbprint of a DPoP key, to bind the authorization code to that key.request(optional, Inbound Apps only): A signed request object (JAR, RFC 9101) that carries the parameters as a JWT. Descope merges its claims into the pushed request.
Don't put a request_uri in the body. Descope rejects a push that references another pushed request.
curl -X POST "__BaseURL__/oauth2/v1/apps/par" \
-H "Content-Type: application/json" \
-d '{
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"response_type": "code",
"scope": "openid profile email",
"redirect_uri": "https://yourapp.com/callback",
"state": "<RANDOM_STATE>",
"nonce": "<RANDOM_NONCE>",
"code_challenge": "<PKCE_CODE_CHALLENGE>",
"code_challenge_method": "S256"
}'When the parameters are valid, Descope responds with 201 Created:
{
"request_uri": "urn:ietf:params:oauth:request_uri:550e8400-e29b-41d4-a716-446655440000",
"expires_in": 60
}Redirecting to Authorize
Send the user's browser to the authorize endpoint with only client_id and the URL-encoded request_uri. For an Inbound App, that's /oauth2/v1/apps/authorize. For a Federated OIDC App, use the authorization_endpoint from its discovery document.
__BaseURL__/oauth2/v1/apps/authorize?\
client_id=<CLIENT_ID>&\
request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A550e8400-e29b-41d4-a716-446655440000Descope looks up the pushed request, deletes it, and validates that it belongs to the same client_id. The pushed values then replace any other parameters on the authorize URL, as RFC 9126 §4 requires. From there, the flow matches one without PAR. The user signs in and consents, and Descope redirects back with a code. Your server then exchanges the code at the token endpoint with the PKCE code_verifier.
Testing with oauth2c
oauth2c is an open-source command-line OAuth client that can run a full authorization code flow with PAR. With the --par flag, oauth2c pushes the parameters first, redirects with the returned request_uri, and then exchanges the code. It prints each request and response, so you can see the 201 from the PAR endpoint and the short authorize URL.
The example below runs against a Federated OIDC App, so the client ID is your Project ID. For an Inbound App, use the app's issuer and client ID instead.
oauth2c "https://api.descope.com/<PROJECT_ID>" \
--client-id "<PROJECT_ID>" \
--client-secret "<ACCESS_KEY>" \
--grant-type authorization_code \
--auth-method client_secret_basic \
--response-types code \
--response-mode query \
--redirect-url http://localhost:9876/callback \
--scopes openid \
--parRun the same command without --par to compare it with a standard authorize request.
Limitations
- Authorization code only:
response_typemust becode, and PKCE must useS256. - Optional, not enforced: PAR is available to every client, but an app can't yet require it. A client that sends a standard authorize request still works.
- One use, short-lived: A
request_uriworks once and expires afterexpires_inseconds, so a retried sign-in needs a new push.
Troubleshooting
| Symptom | Likely cause |
|---|---|
The push returns request_not_supported, or pushed_authorization_request_endpoint is missing from discovery | Your project doesn't have PAR turned on. Contact Descope Support. |
The push returns invalid_client for a Federated OIDC App | client_id isn't your Project ID, the secret isn't a valid Access Key, or someone disabled the app. |
The push returns invalid_request_object | The request JWT (JAR) failed validation. |
/authorize redirects back with invalid_request | The request_uri expired or was already used, or the client_id on the authorize URL doesn't match the one that pushed it. Push a new request. |
Related
- Authorization Code grant: the flow PAR starts
- Authorization Server Endpoints: every Inbound App endpoint
- OIDC Endpoints: the Federated OIDC App endpoints
Authorization Server Endpoints
Descope OAuth 2.0 authorization server endpoints for Inbound Apps (authorize, token, revoke, and userinfo) with links to the API reference.
Using Inbound Apps
Learn how to integrate inbound apps with Descope to streamline OAuth authentication, manage user consent, and securely connect third-party applications.