Xeonr Developer Docs
Guides

Rich Authorization Requests (RAR)

Request fine-grained resource-level permissions using authorization_details.

Rich Authorization Requests (RFC 9396) let a client request access to specific resources — not just broad scopes. Instead of asking for uploads:read, your app can ask for read access to a specific bucket or folder by ID, and the auth server will verify the user actually has access to that resource before issuing the token.

This page covers both sides:

  • App admin: registering resource types and implementing the validation callback
  • Client: sending authorization_details in an authorization request

How it works

When a client includes authorization_details in an authorization request, the auth server calls your app's validation callback at up to three points:

  1. PAR — when the request is first pushed (no user context yet). Validates that the resources exist.
  2. Consent — when the user approves the request. Validates the user has access to each resource, and returns display names for the consent screen.
  3. Token exchange — when a token is exchanged or delegated. Validates that downscoped details are acceptable.

If any validation fails, the authorization request is rejected with invalid_authorization_details.

The resulting access token contains the approved authorization_details as a claim, which your resource server can inspect.


App admin: setting up RAR

1. Register resource types

Each resource type defines the shape of an authorization_details entry your app accepts. Configure these on your application via the Xeonr Auth UI:

FieldDescription
typeShort name for this resource (e.g. bucket). Clients use app-slug:type
json_schemaJSON Schema that validates entries of this type
display_title_fieldWhich response field your callback returns as the display title
display_description_fieldWhich response field your callback returns as the display description

2. Register a validation callback URL

Set validation_callback_url on your application via the Xeonr Auth UI. The auth server will POST to this URL at each validation phase.

3. Implement the callback

The auth server sends a signed JWT as a Bearer token in each callback request — verify it before processing.

Verifying the JWT:

  • Fetch the auth server's JWKS from https://auth.xeonr.io/.well-known/jwks.json
  • Verify the signature using RS256
  • Check iss is auth.xeonr.io, aud is your application's external ID, and exp hasn't passed
  • Check scope matches the expected phase (see below)

Request from the auth server:

POST /api/auth/validate HTTP/1.1
Host: your-app.example.com
Authorization: Bearer eyJ...  (signed by auth server, expires in 30s)
Content-Type: application/json

{
  "phase": "VALIDATION_PHASE_PAR",
  "authorizationDetails": [
    {
      "type": "uploads:bucket",
      "bucket_id": "bucket-123",
      "permissions": ["read", "write"]
    }
  ],
  "clientId": "550e8400-e29b-41d4-a716-446655440000",
  "subject": "",
  "scopes": []
}

Phases and what they receive:

Phasescope in JWTsubjectWhen called
VALIDATION_PHASE_PARrar:validate:parEmptyAt PAR time
VALIDATION_PHASE_CONSENTrar:validate:consentUser URNWhen user approves
VALIDATION_PHASE_EXCHANGErar:validate:exchangeUser URNAt token exchange/delegation

During VALIDATION_PHASE_EXCHANGE, the request also includes:

  • originalAuthorizationDetails — details from the source token
  • actorClientId — the client performing the delegation

Response format:

Return one ValidatedDetail entry per entry in authorizationDetails, in the same order:

{
  "details": [
    {
      "type": "uploads:bucket",
      "approved": true,
      "fields": {
        "title": "My Photos",
        "description": "234 files"
      }
    },
    {
      "type": "uploads:bucket",
      "approved": false,
      "error": "bucket not found"
    }
  ]
}
FieldTypeDescription
typestringMust match the entry's type
approvedbooleanWhether access is granted
fieldsobjectDisplay fields shown on the consent screen
errorstringReason for rejection (shown to the auth server, not the end user)

Timeout: The auth server waits up to 10 seconds for your callback. A non-2xx response or timeout causes the authorization request to fail with HTTP 503.


Client: sending authorization_details

Include authorization_details as a JSON-encoded array in your authorization request. We recommend using PAR so the full payload isn't exposed in the browser's URL bar.

Using PAR

Push the authorization request first:

POST /api/v1/oauth/par HTTP/1.1
Host: auth.xeonr.io
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

response_type=code
&client_id=550e8400-e29b-41d4-a716-446655440000
&redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fcallback
&scope=openid%20uploads%3Aread
&state=abc123
&authorization_details=%5B%7B%22type%22%3A%22uploads%3Abucket%22%2C%22bucket_id%22%3A%22bucket-123%22%2C%22permissions%22%3A%5B%22read%22%5D%7D%5D

Decoded authorization_details:

[
  {
    "type": "uploads:bucket",
    "bucket_id": "bucket-123",
    "permissions": ["read"]
  }
]

PAR response (HTTP 201):

{
  "request_uri": "urn:ietf:params:oauth:request_uri:550e8400-e29b-41d4-a716-446655440000",
  "expires_in": 600
}

PAR endpoint parameters:

ParameterRequiredDescription
response_typeYescode
client_idYesYour client UUID
redirect_uriYesRegistered redirect URI
scopeYesSpace-separated scopes
stateYesCSRF protection value
authorization_detailsYesJSON array of resource requests
code_challengeNoPKCE challenge
code_challenge_methodNoS256 or plain

Redirect using the request_uri

Use the returned request_uri instead of inline parameters:

https://auth.xeonr.io/auth/authorize
  ?client_id=550e8400-e29b-41d4-a716-446655440000
  &request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A550e8400-e29b-41d4-a716-446655440000

The rest of the flow (code exchange, token use) is identical to the Authorization Code guide.

authorization_details in the token

Once issued, the access token includes the approved details as a claim:

{
  "sub": "urn:xeonr:user:12345",
  "authorization_details": [
    {
      "type": "uploads:bucket",
      "bucket_id": "bucket-123",
      "permissions": ["read"]
    }
  ]
}

Your resource server should validate this claim to enforce access at the resource level. The same data is returned by the introspection endpoint.

On this page