Skip to content

Wire identityFromToken into the OAuth2 upstream provider #5156

@jhrozek

Description

@jhrozek

Wire identityFromToken into the OAuth2 upstream provider

Description

Phase 3 of the Snowflake / identityFromToken story (#5150). Connect
the pure helper (#5152) and the CRD field (#5155) into the embedded
auth server's runtime: extract identity from the raw token-endpoint
response body and use it as the resolved upstream identity, sibling
to the existing userInfo and PR #5094 synthesis paths.

Context

The existing token-response rewriter already reads and parses every
successful token-endpoint response in order to normalise non-standard
envelopes (e.g. GovSlack's nested fields). Extend it to also extract
user identity claims from the same body when an
IdentityFromTokenConfig is configured on OAuth2Config.

Identity extraction runs on the RAW pre-rewrite body, so identity
paths are resolved against the original provider response even when
tokenResponseMapping is also configured (Slack v2 needs both: the
user-level access token nested under authed_user.access_token and
the user identifier under authed_user.id).

The new priority chain in BaseOAuth2Provider.ExchangeCodeForIdentity:

  1. IdentityFromToken — when configured, return the extracted
    identity. If extraction failed (path didn't resolve), return
    ErrIdentityResolutionFailed without falling back to userInfo
    or synthesis — "identityFromToken set" is an explicit operator
    declaration that the token response is the identity source, and
    silently falling through would mask misconfiguration.
  2. UserInfo — existing fetchUserInfo path, unchanged.
  3. Synthesis — existing synthesizeIdentity path (PR Allow OAuth2 upstreams to omit userInfo config #5094),
    unchanged.

The refresh path passes nil for the identity config, because providers
like Snowflake omit username on refresh; identity is captured once
at auth-code time and persisted into session storage by the callback
handler.

OIDC providers always have an ID-token-derived subject and discard
the rewriter's identityFromToken return value. A defence-in-depth
WARN fires if a future config-loader bug ever sets IdentityFromToken
on an OIDC base config (the field is structurally absent on the OIDC
CRD type today).

A tripwire test asserts the userinfo HTTP endpoint is never contacted
when identityFromToken is configured — including on extraction
failure. This is the single most important security-relevant test in
the chain.

Dependencies: #5152 (helper), #5155 (CRD type and runtime config
mirror).
Blocks: nothing in the implementation chain — the operator
translation phase consumes the runtime-config field added here, but
the controller-side translation can be built in parallel and merged
in either order.

Acceptance Criteria

  • OAuth2Config has the runtime mirror of IdentityFromToken,
    with Validate() requiring a non-empty subject path when the block
    is set.
  • The token-response rewriter extracts identity from the raw
    pre-rewrite body when IdentityFromTokenConfig is configured;
    extraction runs before any wire-format rewrite.
  • ExchangeCodeForIdentity honours the priority chain:
    identityFromToken first, then userInfo, then synthesis.
  • When identityFromToken is configured but extraction fails
    (e.g. wrong path), the call returns an error wrapping
    ErrIdentityResolutionFailed. The userinfo HTTP endpoint is NOT
    contacted in this case (asserted by a tripwire test).
  • When identityFromToken is configured and extraction succeeds,
    the returned Identity has Synthetic == false so the callback
    handler runs UserResolver normally.
  • The refresh path does not perform identity extraction.
  • OIDC providers ignore IdentityFromToken if it is somehow set
    on their base config, and emit a defence-in-depth WARN.
  • No part of the response body appears in any returned error or
    any logged record at any level.
  • task lint-fix and task test pass (including a go test -race run) with no regressions in the existing
    pkg/authserver/upstream/... test suite.

Out of Scope

  • Operator-side translation from CRD field to runtime config —
    separate task.
  • Updating synthesis-mode-detection helpers (the operator status
    condition + provider-construction WARN) to recognise
    identityFromToken as a real-identity path — separate task.
  • YAML examples — separate task.
  • Audit-log changes — the JWT name claim populated from the
    extracted Identity.Name flows through the existing audit reader
    unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    authenhancementNew feature or requestgoPull requests that update go code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions