OpenIdConnectHandler does not validate auth_time against configured MaxAge on the callback
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
### Summary
`OpenIdConnectHandler` sends the configured `MaxAge` as the `max_age` parameter on the outbound authorization request, but it never validates the `auth_time` claim of the received `id_token` against that value on the callback path. As a result, the `max_age` directive is effectively request-only: the handler relies entirely on the identity provider to honor it and performs no local check of its own.
This is raised as a **defense-in-depth / spec-compliance** improvement.
### Details
- On challenge, `OpenIdConnectHandler.HandleChallengeAsync` reads `MaxAge` (from challenge properties or `Options.MaxAge`) and writes `message.MaxAge` on the outbound request.
- On the callback, `HandleRemoteAuthenticateAsync` validates the token and protocol details but contains no reference to `auth_time` or `Options.MaxAge`. A workspace-wide search for `auth_time` / `AuthTime` across the OpenID Connect handler source returns no matches.
OpenID Connect Core 1.0 describes a corresponding client-side check:
> §3.1.3.7 (ID Token Validation), item 13: "If the `auth_time` Claim was requested, either through a specific request for this Claim or by using the `max_age` parameter, the Client SHOULD check the `auth_time` Claim value and request re-authentication if it determines too much time has elapsed since the last End-User authentication."
The handler currently does not implement this client-side check.
Relevant spec references:
- `max_age` — [OpenID Connect Core 1.0 §3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) (when `max_age` is used, the returned ID Token MUST include `auth_time`).
- `auth_time` — [OpenID Connect Core 1.0 §2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) (REQUIRED when `max_age` is requested; seconds since Unix epoch, UTC).
- RP check — [OpenID Connect Core 1.0 §3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
### Affected source
- `src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs`
### Hints toward a fix
- Add an opt-in option (e.g. `OpenIdConnectOptions.EnforceMaxAge`, default `false`) so existing behavior is preserved unless the check is requested.
- Carry the effective `max_age` (in seconds) through the round trip. Note that `OpenIdConnectChallengeProperties.MaxAge` is stored in `AuthenticationProperties.Parameters`, which is not serialized into `state`; persisting the effective value into `AuthenticationProperties.Items` (which is serialized, like the existing PKCE `CodeVerifierKey`) makes the per-challenge value available on the callback.
- After `id_token` validation (covering both the implicit/hybrid `id_token` and the authorization-code/token-endpoint `id_token`), compare `auth_time` against the requested `max_age`:
- Read `auth_time` via `JwtSecurityToken.Payload.AuthTime` (`int?`, seconds since epoch).
- Use `TimeProvider.GetUtcNow()` for the current time.
- Allow tolerance via the existing `TokenValidationParameters.ClockSkew`.
- Treat a missing `auth_time` as a failure when `max_age` was requested (per §2 it is REQUIRED in that case).
- Surface a failure through the existing `HandleRequestResult.Fail(...)` path so it flows through the `RemoteFailure` event.
For reference, `OpenIdConnectProtocolValidator.RequireAuthTime` only checks for the *presence* of `auth_time`; it does not compare elapsed time against `max_age`, so enabling it alone does not implement the §3.1.3.7 check.
Contributor guide
Research direction
Start in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs, reading HandleChallengeAsync and HandleRemoteAuthenticateAsync, then inspect how CodeVerifierKey persists challenge state. Trace both implicit/hybrid and authorization-code ID-token validation paths. Done means an opt-in MaxAge check compares auth_time using the configured clock skew, rejects missing or expired values through HandleRequestResult.Fail, and preserves existing behavior when disabled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- authentication, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100