Obexal Docs

Docs/Authentication/PAR, DPoP and private_key_jwt

Advanced client security

Harden your OAuth clients with pushed authorization requests (PAR), sender-constrained tokens (DPoP) and private_key_jwt client authentication.

Beyond PKCE and client secrets, Obexal implements three standard mechanisms for high-assurance clients: PAR protects the integrity of the authorization request, DPoP makes stolen tokens unusable, and private_key_jwt removes shared secrets entirely. They are independent and compose freely; all three are advertised in the discovery document.

Note

Examples use accounts.obexal.com, the default domain. With a custom domain, that domain is the issuer.

Pushed Authorization Requests (PAR, RFC 9126)

What it does: instead of putting all authorization parameters in the browser URL, the client pushes them directly to the server first and receives an opaque, single-use request_uri. The browser then carries only that reference: parameters cannot be tampered with in transit, and they do not leak through browser history, logs or referrers.

When to use it: confidential clients with strong integrity requirements, FAPI-style deployments, or whenever the authorization request contains parameters you do not want exposed.

Push the request (a confidential client must authenticate; the request must not itself contain a request_uri):

curl -sS -X POST https://accounts.obexal.com/oauth/par \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d 'response_type=code' \
  -d "client_id=$CLIENT_ID" \
  -d 'redirect_uri=https://app.example.eu/callback' \
  -d 'scope=openid profile email' \
  -d 'state=xyz' \
  -d "code_challenge=$CHALLENGE" \
  -d 'code_challenge_method=S256'
{ "request_uri": "urn:ietf:params:oauth:request_uri:3Zk9v2...", "expires_in": 90 }

Then redirect the browser with only two parameters:

https://accounts.obexal.com/oauth/authorize?client_id=<CLIENT_ID>&request_uri=urn:ietf:params:oauth:request_uri:3Zk9v2...

The request_uri is valid for 90 seconds and consumed on first use. The rest of the flow (code exchange, PKCE) is unchanged.

DPoP (RFC 9449): sender-constrained tokens

What it does: binds the access token to a key pair held by the client. Each request carries a short-lived proof JWT in the DPoP header, and the token itself carries the key thumbprint in its cnf.jkt claim. A stolen token is useless without the private key.

When to use it: any client where token theft is a realistic risk, and as a hardening layer for AI agent tokens.

The proof is a JWT with header typ: dpop+jwt, signed ES256 or RS256, whose jwk header contains the public key, and whose claims are: htm (HTTP method), htu (target URL without query), iat (at most 60 seconds old) and a unique jti (single use, replay-checked). When presenting the token to a resource endpoint, add ath, the base64url SHA-256 hash of the access token.

Send the proof at the token endpoint; the issued token is then bound and the token_type becomes DPoP:

curl -sS -X POST https://accounts.obexal.com/oauth/token \
  -H "DPoP: $DPOP_PROOF" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d 'grant_type=client_credentials'
# 200 -> {"access_token":"<JWT with cnf.jkt>","token_type":"DPoP",...}

Use the token with a fresh proof on each call, using the DPoP authorization scheme:

curl -sS https://accounts.obexal.com/oauth/userinfo \
  -H "Authorization: DPoP $ACCESS_TOKEN" \
  -H "DPoP: $DPOP_PROOF"

A bound token presented without a valid matching proof is refused (fail closed). Tokens issued without a proof remain plain Bearer tokens: DPoP is opt-in per request.

private_key_jwt (RFC 7523): authentication without a shared secret

What it does: the client authenticates by signing a short assertion with its private key; Obexal verifies it against the public JWKS registered for the client. No secret is transmitted, stored or shared.

When to use it: confidential clients that want to eliminate secret distribution, and machine identities where key rotation is easier to govern than secret rotation. It pairs naturally with PAR.

First register the client's public key set through the jwks field of POST /v1/applications (or update it later with PATCH). Then authenticate at the token or PAR endpoint with an assertion instead of a secret:

  • Signed with RS256, ES256 or PS256.
  • iss and sub both equal to the client_id.
  • aud equal to the issuer URL, or to the token or PAR endpoint URL.
  • exp required; jti unique (anti-replay window of 10 minutes).
curl -sS -X POST https://accounts.obexal.com/oauth/token \
  -d 'grant_type=client_credentials' \
  -d 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
  -d "client_assertion=$ASSERTION"

Limits and notes

  • Obexal signs its own tokens (access tokens, ID tokens, logout tokens) with RS256 only. The ES256 and PS256 algorithms above apply to material signed by your client, not by the server.
  • PAR, DPoP and private_key_jwt are independent: adopt them separately or together. A high-assurance profile combines all three.
  • For validating what the server issues, see Validate tokens.