TrueVault Identity Manager

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

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 happenederrorHTTP status
Anything other than POSTinvalid_request405
No client_id in the header or bodyinvalid_request400
Unknown client, or wrong secretinvalid_client401
Credentials in both the header and the bodyinvalid_client401
A public client pushedunauthorized_client401
response_type or scope missinginvalid_request400

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.