AttestoPhoenix.AuthorizationServer.JwtBearer (AttestoPhoenix v2.13.0)

Copy Markdown View Source

The resource server's half of the Identity Assertion JWT Authorization Grant (ID-JAG), the grant behind MCP Enterprise-Managed Authorization (EMA) - draft-ietf-oauth-identity-assertion-authz-grant-04.

A token request arrives with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer and an assertion parameter carrying an ID-JAG: a short-lived JWT the enterprise IdP signed (after its own RFC 8693 token exchange) asserting one user for this resource application. authorize/3 turns that assertion into the local subject and scope ceiling the token endpoint mints from. It owns the stateful concerns that Attesto.IdentityAssertion (conn-free, pure) deliberately leaves out:

  • issuer trust - the assertion's iss must be a configured trusted issuer (jwt_bearer: [issuers: %{...}]); an unconfigured issuer is denied without revealing which issuers are trusted.
  • JWKS resolution - static keys, a cached jwks_uri fetch (reusing the SSRF-guarded CIMD fetcher + cache), or a custom :jwks_resolver.
  • jti replay - prepares an issuer-scoped, fixed-size identity for the configured :replay_check seam; the token core claims it only after all exchange policy has passed, and before minting.
  • subject resolution - after request policy passes, the host's :resolve_jwt_bearer_subject callback maps the validated claims to a local principal subject (or denies).

Every failure returns {:error, atom}; the token core (AttestoPhoenix.AuthorizationServer.Token) maps a missing assertion parameter to RFC 6749 §5.2 invalid_request and every assertion/trust/replay/ subject failure to invalid_grant, as the draft requires.

This is NOT private_key_jwt client authentication (RFC 7523 §3) nor the RFC 8693 token-exchange grant (which runs at the IdP).

Summary

Types

A verified assertion's fixed-size replay identity and remaining acceptance window.

The resolved local subject, the assertion's scope ceiling (nil when the assertion carried no scope claim, so the host policy alone decides), and the validated claims.

Functions

Validate the ID-JAG assertion, claim its replay identity, and resolve the local subject.

Atomically claim a replay identity returned by prepare/3 before minting.

Validate an ID-JAG while deferring subject resolution and its atomic replay claim.

Resolve verified ID-JAG claims to a local principal subject.

Types

error()

@type error() ::
  :missing_assertion
  | :untrusted_issuer
  | :jwks_unavailable
  | :invalid_assertion
  | :replay
  | :subject_denied

pending_claim()

@type pending_claim() :: {String.t(), pos_integer()}

A verified assertion's fixed-size replay identity and remaining acceptance window.

prepared_result()

@type prepared_result() :: %{
  scope_ceiling: [String.t()] | nil,
  claims: Attesto.IdentityAssertion.claims(),
  replay_claim: pending_claim()
}

result()

@type result() :: %{
  subject: String.t(),
  scope_ceiling: [String.t()] | nil,
  claims: Attesto.IdentityAssertion.claims()
}

The resolved local subject, the assertion's scope ceiling (nil when the assertion carried no scope claim, so the host policy alone decides), and the validated claims.

Functions

authorize(config, client_id, params)

@spec authorize(AttestoPhoenix.Config.t(), String.t() | nil, map()) ::
  {:ok, result()} | {:error, error()}

Validate the ID-JAG assertion, claim its replay identity, and resolve the local subject.

client_id is the already-authenticated client's identifier (the token endpoint resolved it from client authentication); the assertion's client_id claim MUST equal it. Returns {:ok, %{subject, scope_ceiling, claims}} or {:error, t:error/0}.

commit_replay_claim(config, arg)

@spec commit_replay_claim(AttestoPhoenix.Config.t(), pending_claim()) ::
  :ok | {:error, :replay}

Atomically claim a replay identity returned by prepare/3 before minting.

prepare(config, client_id, params)

@spec prepare(AttestoPhoenix.Config.t(), String.t() | nil, map()) ::
  {:ok, prepared_result()} | {:error, error()}

Validate an ID-JAG while deferring subject resolution and its atomic replay claim.

The token core uses this variant so sender binding, resource, scope, and host policy can finish before invoking a potentially side-effecting subject resolver or consuming the assertion. A caller MUST then call resolve_subject/2 and pass :replay_claim to commit_replay_claim/2 before minting or releasing an authorization result. Most callers should use authorize/3, which performs both steps itself.

resolve_subject(config, claims)

@spec resolve_subject(AttestoPhoenix.Config.t(), Attesto.IdentityAssertion.claims()) ::
  {:ok, String.t()} | {:error, :subject_denied}

Resolve verified ID-JAG claims to a local principal subject.

Required when the feature is enabled (Config.validate!/1 enforces this), so an unset callback is a configuration fault rather than a per-request denial.