Skip to content

max_age is silently dropped when an authorization request is pushed via PAR #4912

Description

@sahandilshan

Description

The max_age parameter is honoured on the direct GET /oauth2/authorize path but is silently
discarded when the same request is pushed through POST /oauth2/par and redeemed via
request_uri. A relying party that uses PAR cannot force re-authentication.

Root cause

backend/internal/oauth/oauth2/par/service.go:132-149 builds oauth2model.OAuthParameters from
the pushed parameters, assigning State, ClientID, RedirectURI, RedirectURIProvided, ResponseType, StandardScopes, PermissionScopes, CodeChallenge, CodeChallengeMethod, Resources, ClaimsRequest, ClaimsLocales, Nonce, AcrValues, DPoPJkt, Prompt — but never MaxAge.

MaxAge exists on the struct (oauth2/model/parameter.go:31) and the direct path both sets it
(authz/service.go:270,324) and maps it into flow runtime data (authz/service.go:424-426). Via
PAR, runtimeData["max_age"] is therefore never populated, so
checkAssurance has no constraint to enforce.

Reproduction

One cookie jar, one SSO session 25 seconds old, both requests issued in the same wall-clock second:

T0 (real authentication)                = 1786591879
DIRECT  max_age=1  at t=1786591904  ->  FLOW ERROR FET-1082    [session age 25s]
PAR     max_age=1  at t=1786591904  ->  SUCCESS, tokens issued [session age 25s]

Identical session, identical instant, identical flow — the only variable is direct vs PAR.

# with a live SSO session >= 2s old in $JAR
curl -sk -c $JAR -b $JAR "https://localhost:8090/oauth2/authorize?client_id=qa-b2-client&redirect_uri=...&response_type=code&scope=openid&max_age=1"
# -> flow errors with FET-1082

RU=$(curl -sk -u 'qa-b2-client:...' \
  -d 'client_id=qa-b2-client&redirect_uri=...&response_type=code&scope=openid&max_age=1' \
  https://localhost:8090/oauth2/par | jq -r .request_uri)
curl -sk -c $JAR -b $JAR "https://localhost:8090/oauth2/authorize?client_id=qa-b2-client&request_uri=$RU"
# -> flow COMPLETEs, tokens issued

Positive control: max_age=3600 on the direct path against the same reused session succeeds,
so the direct-path rejection is a genuine threshold check rather than a blanket failure.

Expected

par/service.go should copy MaxAge alongside AcrValues, so a pushed request enforces the same
constraint as the equivalent direct request.

Impact

An RP that adopts PAR (which OAuth 2.1 and FAPI encourage) silently loses the ability to require
recent authentication. The failure is silent — no error, no warning.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions