Skip to content
TDACorpIDENTITYSign in

Pushed authorization requests

Push the authorization request from your server first and receive a short-lived reference, which is the only thing the browser carries.

On this page

Identity does not accept authorization parameters in the browser address. A client first sends them directly to identity over an authenticated connection (RFC 9126), gets a reference back, and sends the browser to the authorization endpoint with only that reference. Discovery says so with require_pushed_authorization_requests: true.

The reason is that every parameter in a browser address is a parameter someone can put in a link and send to a person. Pushing them first means the browser carries an opaque reference to a request your client itself made.

Request#

POST to the pushed_authorization_request_endpoint from discovery, which is /par. The body is application/x-www-form-urlencoded; any other content type, or an oversized body, is invalid_request.

Client authentication#

client_id is required exactly once. When the oauth_cimd flag is on (it defaults OFF), it may be an https URL naming your client metadata document instead of a registered identifier. A confidential client also sends a signed assertion, client_assertion_type and client_assertion; see client authentication. A client that requires DPoP sends a DPoP proof header; see DPoP. Identity authenticates the client before it looks at the other parameters, so an unauthenticated caller learns nothing about which scopes or redirect addresses a client holds.

Parameters#

ParameterRequiredRule
response_typeYescode. Anything else is unsupported_response_type.
redirect_uriYesMust equal a registered redirect address byte for byte.
scopeYesSpace separated, no repeats, must include openid, and every value must be one the client may request.
stateYes16 to 512 printable ASCII characters. Returned unchanged.
nonceYesUp to 512 printable ASCII characters. Returned in the ID token.
code_challengeYesThe 43-character base64url SHA-256 of your code verifier.
code_challenge_methodYesS256. There is no default, and plain is refused.
response_modeNoquery or form_post. When absent, a web client gets form_post and every other client gets query.
promptNoSpace separated: none, login, consent, create. none cannot be combined with another value. select_account is treated as login. A value identity does not know is ignored.
max_ageNoWhole seconds. The person must have signed in within that time, and the ID token carries auth_time.
acr_valuesNoSpace separated from aal1, aal2, aal3. See acr and amr values.
tdaid_tenantNoAn organization identifier (not its slug) to sign the person in to. The person must be an active member, and the client's app must be installed in it.
resourceNoRFC 8707. Up to eight absolute addresses without a fragment, each registered as a resource and available to this client. Whether the resource's app is installed in the organization is checked at the authorization endpoint, once the organization is known. Each becomes the audience of its own access token.
authorization_detailsNoRFC 9396. A JSON array of at most ten objects whose type is tdaid:agent_action or tdaid:elevation.
login_hintNoA single-line prefill of the identify field, at most 254 characters. It never skips sign-in and never picks an account.
ui_localesNoOpenID Connect Core section 3.1.2.1. A space-separated list of at most eight language tags, best first, each a language and optional dash-separated subtags (fr-CA). It only chooses the language of the sign-in pages, and a signed-in person's saved language outranks it. See Languages.
dpop_jktNoThe RFC 7638 thumbprint of your DPoP key. Only a client that requires DPoP may send it; if a proof is also present the two must agree.

Identity refuses every parameter not in this table, and refuses a parameter that appears twice. This is deliberate: a parameter dropped silently leaves your client believing a safety check took effect.

Response#

A successful push is 201 Created with Cache-Control: no-store.

MemberMeaning
request_uriAn opaque reference starting urn:ietf:params:oauth:request_uri:. Treat it as a secret for its short life.
expires_in60, or less if the deployment shortens it. The reference lives that many seconds and works once.

Every response from this endpoint, success or failure, carries a DPoP-Nonce header. Use it in your next proof.

Errors#

Errors are JSON, {"error": "...", "error_description": "..."}, with Cache-Control: no-store. Status is 400, or 500 for server_error. Note that invalid_client is 400 here, where the token endpoint answers 401. Rely on the error code, not the status. The description is for your logs, not for a person.

errorWhen
invalid_requestA malformed, missing, repeated or unknown parameter, a wrong content type, an oversized body, or a bad redirect_uri, state, nonce, code_challenge, prompt, max_age, acr_values, tdaid_tenant, login_hint, ui_locales or dpop_jkt.
invalid_clientThe client_id is missing, malformed or unknown, the client is suspended, or authentication failed. Identity answers the same for an unknown client and a malformed identifier.
unauthorized_clientThe client does not hold the authorization_code grant.
unsupported_response_typeresponse_type is not code.
invalid_scopeopenid is missing or a requested scope is not allowed for the client.
invalid_targetA resource is malformed, repeated, unregistered or not available to this client.
invalid_authorization_detailsauthorization_details is not valid JSON, too large, or has an unsupported type.
invalid_dpop_proofThe proof is invalid, or dpop_jkt disagrees with the proof key.
use_dpop_nonceThe proof lacks a current nonce. Retry once with the DPoP-Nonce header value.
server_errorIdentity could not complete the request.

A request can also be refused with 429 and a Retry-After header. Wait that long.

See Error codes for the full list.

Worked example#

curl -s https://identity.tdacorp.in/par \  -H 'content-type: application/x-www-form-urlencoded' \  -H "DPoP: $DPOP_PROOF" \  --data-urlencode 'client_id=tdaid_c_example' \  --data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \  --data-urlencode "client_assertion=$ASSERTION" \  --data-urlencode 'response_type=code' \  --data-urlencode 'redirect_uri=https://app.example.com/api/auth/callback' \  --data-urlencode 'scope=openid profile offline_access' \  --data-urlencode "state=$STATE" \  --data-urlencode "nonce=$NONCE" \  --data-urlencode "code_challenge=$CHALLENGE" \  --data-urlencode 'code_challenge_method=S256'
{  "request_uri": "urn:ietf:params:oauth:request_uri:Qm9ndXNFeGFtcGxl",  "expires_in": 60}

The value is illustrative. The next step is to send the browser to the authorization endpoint within expires_in seconds.

Security notes#

  • PKCE is mandatory. Generate a fresh high-entropy code_verifier for each request, keep it server side, and send only its S256 challenge.
  • The reference is spent by being presented, not by succeeding. A second use fails with invalid_request_uri at the authorization endpoint whatever happened the first time.
  • A DPoP key binds the code. When the push carries a DPoP proof, the authorization code is bound to that key and the token request must be signed by the same key, or the code is refused with invalid_grant.
  • Keep state and nonce unguessable and per request. The SDK generates both for you.

Verified against commit 7ff8932