Pushed Authorization Requests
Send the authorization parameters to us server to server, then redirect with a short request_uri. The recommended start for a confidential client.
A pushed authorization request (PAR, RFC 9126) is the authorization request in two steps. Your server POSTs the parameters it would have put in the redirect to us first, authenticated as your client. We hand back a short request_uri. You then redirect the person to /oauth/oidc/authorize with only your client_id and that request_uri.
Everything after that - verification, consent, the code, the token exchange, userinfo - is exactly the same as the direct flow.
Why push
The request cannot be changed on the way through. In the direct flow the browser carries claims, email, phone and ruleset in the address bar. Anyone or anything between your redirect and our server can see them and, in principle, edit them: a proxy log, a shared screenshot, a browser extension. With a pushed request we only act on a set of parameters your server sent us with your secret attached. The browser never sees them, and the full request does not leak in a Referer header either.
The URL stays short, whatever you ask for. Our claims parameter is JSON and grows with every claim you request. Twelve claims make a direct authorize URL of around 850 bytes, and it keeps growing. Some email clients, SMS gateways, chat apps and proxies truncate or rewrite long URLs. A PAR authorize URL is under 200 bytes and does not change shape when your claims list does.
When to use it
Use it if your client is confidential. That is any client with a
client_secret, which means a server-side application.Do not use it from a public client. A single-page app or a mobile app has no secret, and the push has to be authenticated to mean anything. Public clients keep using the direct request, protected by PKCE as they are today.
Push when the person clicks, not when the page renders. A
request_uriis good for 10 minutes and can be used once. Make the push in the handler behind your "Continue with TrueVault" button, then redirect.
PAR is optional here: require_pushed_authorization_requests is false in the discovery document.
Step 1: push the parameters
POST the same parameters you would have put in the authorize redirect to the pushed_authorization_request_endpoint from the discovery document. PKCE is still mandatory, so code_challenge and code_challenge_method=S256 go in the push.
Authenticate the way your client authenticates at the token endpoint. Clients are set to one method, not both: send either the Authorization: Basic header (client_secret_basic) or client_id and client_secret in the form body (client_secret_post). A push that carries both is turned away.
With client_secret_basic:
curl -X POST https://sandbox.truevault.com.au/oauth/oidc/par \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d response_type=code \
-d "redirect_uri=https://your-app.com/callback" \
-d "scope=openid" \
-d "claims={\"true\":[\"trueidentity.valid\"]}" \
-d state=RANDOM_STATE \
-d nonce=RANDOM_NONCE \
-d code_challenge=YOUR_CODE_CHALLENGE \
-d code_challenge_method=S256
With client_secret_post:
curl -X POST https://sandbox.truevault.com.au/oauth/oidc/par \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d response_type=code \
-d "redirect_uri=https://your-app.com/callback" \
-d "scope=openid" \
-d "claims={\"true\":[\"trueidentity.valid\"]}" \
-d state=RANDOM_STATE \
-d nonce=RANDOM_NONCE \
-d code_challenge=YOUR_CODE_CHALLENGE \
-d code_challenge_method=S256
The push accepts every parameter the authorize endpoint does: response_type, redirect_uri, scope, claims, state, nonce, code_challenge, code_challenge_method, and the optional ruleset, aobo, phone, phone_country_code and email.
Response (HTTP 201):
{
"request_uri": "urn:ietf:params:oauth:request_uri:cPkc0bdMi3tYpeEYAPR57ZizsansgCuf",
"expires_in": 600
}
expires_in is in seconds. Treat the request_uri as opaque - it is only meaningful to us, and only once.
Step 2: redirect the browser
Send the person to the authorize endpoint with client_id and request_uri and nothing else:
GET /oauth/oidc/authorize ?client_id=YOUR_CLIENT_ID &request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3AcPkc0bdMi3tYpeEYAPR57ZizsansgCuf Host: sandbox.truevault.com.au
Any other parameter in that query string is ignored. The pushed set is the only one that counts, so there is no way to add a claim or change the redirect_uri after the push.
The person then comes back to your redirect_uri with a code and your state, and you carry on as in the quick start: check the state, exchange the code with the original code_verifier, validate the id_token, call userinfo.
In code
PHP:
// Step 1: push, authenticated with client_secret_basic
$discovery = json_decode(file_get_contents(
'https://sandbox.truevault.com.au/.well-known/openid-configuration'
), true);
$push = curl_init($discovery['pushed_authorization_request_endpoint']);
curl_setopt_array($push, [
CURLOPT_POST => true,
CURLOPT_USERPWD => $client_id . ':' . $client_secret,
CURLOPT_POSTFIELDS => http_build_query([
'response_type' => 'code',
'redirect_uri' => 'https://your-app.com/callback',
'scope' => 'openid',
'claims' => json_encode(['true' => ['trueidentity.valid']]),
'state' => $state,
'nonce' => $nonce,
'code_challenge' => $code_challenge,
'code_challenge_method' => 'S256',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$par = json_decode(curl_exec($push), true);
// Step 2: redirect with only client_id and request_uri
header('Location: ' . $discovery['authorization_endpoint'] . '?' . http_build_query([
'client_id' => $client_id,
'request_uri' => $par['request_uri'],
]));
Node:
// Step 1: push, authenticated with client_secret_basic
const discovery = await fetch(
'https://sandbox.truevault.com.au/.well-known/openid-configuration'
).then((r) => r.json());
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const par = await fetch(discovery.pushed_authorization_request_endpoint, {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
response_type: 'code',
redirect_uri: 'https://your-app.com/callback',
scope: 'openid',
claims: JSON.stringify({ true: ['trueidentity.valid'] }),
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
}),
}).then((r) => r.json());
// Step 2: redirect with only client_id and request_uri
const authorizeUrl = new URL(discovery.authorization_endpoint);
authorizeUrl.searchParams.set('client_id', clientId);
authorizeUrl.searchParams.set('request_uri', par.request_uri);
res.redirect(authorizeUrl.toString());
Most OIDC libraries already speak PAR. Look for a pushedAuthorizationRequest or par option before writing the call yourself.
Errors
The push returns standard OAuth 2.0 error JSON:
{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}
| What happened | error | HTTP status |
|---|---|---|
| Anything other than POST | invalid_request | 405 |
No client_id in the header or body | invalid_request | 400 |
| Unknown client, or wrong secret | invalid_client | 401 |
| Credentials in both the header and the body | invalid_client | 401 |
| A public client pushed | unauthorized_client | 401 |
response_type or scope missing | invalid_request | 400 |
At the authorize endpoint a bad request_uri is also invalid_request (HTTP 400), with an error_description that says which: the request_uri has expired or is unknown, was already used, or does not match the client_id you sent with it. The full list is in Error handling.
Discovery
The discovery document tells you where to push and that pushing is optional:
{
"pushed_authorization_request_endpoint": "https://sandbox.truevault.com.au/oauth/oidc/par",
"require_pushed_authorization_requests": false,
"request_uri_parameter_supported": false
}
request_uri_parameter_supported is false because that flag is about OpenID Connect request objects by reference (JAR), which we do not support. A PAR request_uri is different and is always accepted at the authorize endpoint.
Try it
On a sandbox account, the built-in test relying party at /test-oidc has a "Start Login (PAR)" button. The "(inspect)" link beside it shows the exact form that was pushed, the 201 response, the short authorize URL and how many bytes it saved compared with the direct request, before it sends you on. Reloading the authorize URL afterwards shows the "already used" error.