Skip to content
TDACorpIDENTITYSign in

Pushed authorization requests

Why every sign-in starts with a request sent directly to identity, what to send, and how the answer and its errors look.

On this page

A pushed authorization request (RFC 9126) moves the sign-in parameters out of the browser's address bar. Your server sends them straight to identity, authenticated as your client, and gets back a short reference. The browser is then redirected with only the reference. Identity requires this for every client: the discovery document says require_pushed_authorization_requests is true, and the authorization endpoint accepts nothing but client_id and request_uri.

Why it matters#

Parameters in a browser redirect can be read, replayed and tampered with by anything between your server and the person: an extension, a proxy, a log. A pushed request cannot. It is authenticated, so identity knows it came from your client. It is validated before the person sees any page, so a mistake in your request is an error for your developer, not a broken page for a user. And the redirect carries no scope, redirect_uri or code_challenge for anyone to change.

The request#

POST /parContent-Type: application/x-www-form-urlencodedDPoP: <proof>client_id=<client id>&response_type=code&redirect_uri=https%3A%2F%2Fapp.example.com%2Fapi%2Fauth%2Fcallback&scope=openid%20profile%20email&state=<random, 16 to 512 characters>&nonce=<random>&code_challenge=<43 characters>&code_challenge_method=S256&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=<assertion>
  • The body is a form, not JSON. Any other content type is refused with invalid_request, as is a body that is too large.
  • client_id appears exactly once. A missing or repeated one is refused with invalid_client. When the oauth_cimd flag is on (it defaults OFF), it may be an https URL naming your client metadata document.
  • The parameters are those listed in Authorization code with PKCE. An unknown or repeated parameter is refused. The description sometimes names the parameter and sometimes does not.
  • You authenticate as your client. For a web or service client that means a signed assertion, in the body. See Client authentication.
  • A client that requires DPoP sends a proof in the DPoP header. The proof has no ath claim here. See DPoP.

Binding the code to your key#

When your client requires DPoP, the key in the proof is recorded with the request, and the authorization code that results is bound to it. The token request must then be signed by the same key, or the code is refused with invalid_grant. You may also send a dpop_jkt parameter, the thumbprint of your key. It is accepted only from a client that requires DPoP, and when you send both it and a proof, they must name the same key, or the request fails with invalid_dpop_proof.

The answer#

A request that passes every check is answered 201 Created:

{ "request_uri": "urn:ietf:params:oauth:request_uri:...", "expires_in": 60 }

expires_in is 60, or less if the deployment shortens it.

Treat request_uri as an opaque string.

  • It is valid for expires_in seconds, at most 60. Start the redirect right away.
  • It can be used once. Opening the same address a second time fails.
  • It belongs to your client. Another client cannot use it.

Then redirect the browser:

https://identity.tdacorp.in/authorize?client_id=<client id>&request_uri=<request_uri>

What identity checks#

Before it stores anything, identity checks, in this order:

  1. The form, the body size and client_id.
  2. That the client exists.
  3. Your client authentication, which includes the DPoP proof when required. A suspended client fails here.
  4. That the client holds authorization_code and has a registered redirect address.
  5. Every authorization parameter: the redirect_uri against the registered addresses, then response_type, scope (a subset of the registered scopes, including openid), state, nonce, the PKCE pair, response_mode, prompt, max_age, acr_values, the organization, resource, login_hint and ui_locales.
  6. Every resource: it must be registered, and your client must be allowed to ask for it. See Resource indicators.

The request is checked again when the browser arrives at the authorization endpoint, against the client as it is at that moment. A client suspended in between, or a scope removed in between, is refused there. The organization is not known at the push, so whether each resource's app is installed in it is checked at the authorization endpoint.

Errors#

At this endpoint every refusal is a direct JSON answer with status 400, including invalid_client. Only server_error is 500.

{ "error": "invalid_request", "error_description": "redirect_uri does not exactly match a registered redirect URI" }

This differs from the token endpoint, which answers invalid_client with 401. Do not branch on the status alone. Read error.

errorUsual cause
use_dpop_nonceThe proof carries no current server nonce. The answer has a DPoP-Nonce header. Sign a new proof with it and a new client assertion, and send the request again. For a client that requires DPoP this is normal on the first request.
invalid_clientUnknown client id, a suspended client, or an assertion that does not verify.
invalid_requestA malformed, unknown or repeated parameter (other than client_id), a state that is too short, a redirect address that does not match, a missing PKCE parameter.
invalid_scopeA scope the client is not registered for, or no openid.
invalid_targetA resource that is malformed, not registered, or not allowed for your client.
invalid_dpop_proofThe proof is malformed or the wrong key.
unauthorized_clientThe client does not hold authorization_code.
server_errorStatus 500. Retry later.

A 429 answer, with a Retry-After header, means you have sent too many requests. See Rate limits and retries.

Every answer, success or failure, carries a fresh DPoP-Nonce header, so you can keep the latest one for the next request.

With the SDK#

rp.startAuthorization({ scope }) generates the PKCE pair, state and nonce, pushes the request, handles the nonce retry, and returns the URL to redirect to and the transaction to keep. The route helpers store that transaction in a sealed cookie for you.

Verified against commit 936c0b4