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 requestWith PAR
Where the parameters travelThe browser URL, as a query stringA direct POST from your server to Descope
Who can read or change themAnything that can see the browser URLOnly your server and Descope
When Descope validates themWhen the browser reaches /authorizeWhen your server pushes them, before any redirect
Authorize URLLong, with every parameterShort: 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:

Client appyour serverUserbrowserDescopeauthorization server1. POST /parclient auth, response_type=code, scope, redirect_uri, PKCEauthenticates client, validates params2. 201: request_uri, expires_in3. redirect to /authorizeGET /authorizeclient_id, request_uri4. consumes request_uri (one use)sign in with a Flow, then consent5. redirect with codeauthorization codePOST /tokencode, code_verifier

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 typePAR endpointAPI 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/parOIDC PAR
Federated OIDC App (specific SSO app)__BaseURL__/{ssoAppId}/oauth2/v1/parOIDC 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_secret in the body, HTTP Basic authentication, or a signed JWT in client_assertion with client_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 be code.
  • scope, redirect_uri, state, and nonce: The same values you would send to /authorize. Descope validates redirect_uri against the app's approved redirect URLs now, at push time.
  • code_challenge and code_challenge_method: The PKCE challenge. The only supported method is S256.
  • 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-446655440000

Descope 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 \
  --par

Run the same command without --par to compare it with a standard authorize request.

Limitations

  • Authorization code only: response_type must be code, and PKCE must use S256.
  • 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_uri works once and expires after expires_in seconds, so a retried sign-in needs a new push.

Troubleshooting

SymptomLikely cause
The push returns request_not_supported, or pushed_authorization_request_endpoint is missing from discoveryYour project doesn't have PAR turned on. Contact Descope Support.
The push returns invalid_client for a Federated OIDC Appclient_id isn't your Project ID, the secret isn't a valid Access Key, or someone disabled the app.
The push returns invalid_request_objectThe request JWT (JAR) failed validation.
/authorize redirects back with invalid_requestThe 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.
Was this helpful?

On this page