Attesto.IdentityAssertion (Attesto v0.13.0)

Copy Markdown View Source

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

In EMA the client first performs an RFC 8693 token exchange at the enterprise IdP, trading the user's ID token / SAML assertion for an ID-JAG: a short-lived JWT, signed by the IdP, asserting one user for one resource application. The client then presents that ID-JAG to this server's token endpoint as an RFC 7523 §4 JWT-bearer authorization grant (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, the assertion in the assertion parameter) and receives a normal access token. This module verifies that assertion.

It is deliberately conn-free and side-effect-free: it verifies the compact JWT's signature against a caller-supplied trusted JWKS and validates the draft's claim rules, returning the claims or a typed error. The caller (the AttestoPhoenix token layer) owns the stateful concerns: resolving which trusted issuer's JWKS to use (and fetching/caching it), jti replay protection, subject resolution, and mapping every error here to the RFC 6749 §5.2 invalid_grant the token endpoint must return.

This is NOT private_key_jwt client authentication (RFC 7523 §3, which asserts the client's identity) nor the RFC 8693 token-exchange grant (which runs at the IdP, not here). It shares JWT-validation shape with Attesto.RequestObject but enforces ID-JAG's distinct claim rules - notably iss is the IdP (NOT equal to client_id), aud is this server's issuer, and the JOSE typ is pinned to oauth-id-jag+jwt.

Validated per the draft

  • JOSE header typ MUST be oauth-id-jag+jwt (a media type, compared case-insensitively per RFC 7515 §4.1.9 / RFC 2045 §5.1).
  • signature verifies against the trusted issuer JWKS, selecting the key by kid and the accepted algorithms.
  • iss matches the caller-supplied trusted issuer.
  • aud is exactly this server's issuer identifier - a single string, or an array of exactly one element equal to it (draft §6.1).
  • client_id matches the authenticated client making the token request.
  • the REQUIRED claims iss, sub, aud, client_id, jti, exp, iat are present and well-typed.
  • exp is in the future; iat/nbf are not in the future (60s skew); the lifetime exp - iat does not exceed :max_lifetime_seconds when set.

Summary

Types

The validated, string-keyed ID-JAG claim set.

Functions

Read the unverified iss claim from an assertion so the caller can select the trusted issuer (and its JWKS) before verifying the signature.

Verify an ID-JAG assertion against a trusted issuer JWKS and return its claims.

Types

claims()

@type claims() :: %{optional(String.t()) => term()}

The validated, string-keyed ID-JAG claim set.

verify_error()

@type verify_error() ::
  :malformed
  | :unsupported_critical_header
  | :unsupported_alg
  | :invalid_typ
  | :invalid_signature
  | :invalid_issuer
  | :invalid_audience
  | :missing_claim
  | :client_mismatch
  | :expired
  | :not_yet_valid

verify_opts()

@type verify_opts() :: [
  now: DateTime.t() | non_neg_integer(),
  issuer: String.t(),
  audience: String.t(),
  client_id: String.t(),
  accepted_algs: [Attesto.SigningAlg.alg()],
  max_lifetime_seconds: pos_integer() | nil
]

Functions

peek_issuer(jwt)

@spec peek_issuer(String.t()) :: {:ok, String.t()} | :error

Read the unverified iss claim from an assertion so the caller can select the trusted issuer (and its JWKS) before verifying the signature.

This decodes the JWT payload WITHOUT verifying it - the returned issuer is untrusted until verify/3 confirms the signature and re-checks iss against the caller-supplied :issuer. Returns :error for a malformed JWT or an absent/blank iss.

verify(jwt, trusted_jwks, opts \\ [])

@spec verify(String.t(), map() | [map()], verify_opts()) ::
  {:ok, claims()} | {:error, verify_error()}

Verify an ID-JAG assertion against a trusted issuer JWKS and return its claims.

trusted_jwks is the asserting IdP's JWK set (a %{"keys" => [...]} map, a bare list of JWK maps, or a single JWK map). Required opts:

  • :issuer - the trusted issuer the assertion's iss must equal.
  • :audience - this server's issuer identifier the assertion's aud must identify.
  • :client_id - the authenticated client; the client_id claim must equal it (draft §6.1).

Optional opts:

  • :accepted_algs - JOSE algorithms a candidate key may sign with. Defaults to Attesto.SigningAlg.allowed/0 (includes RS256, which enterprise IdPs commonly use - unlike the FAPI request-object default).
  • :max_lifetime_seconds - reject an assertion whose exp - iat exceeds this bound.
  • :now - the verification instant (a DateTime or unix seconds); defaults to the system clock.

Returns {:ok, claims} (string-keyed, including the registered claims) or {:error, t:verify_error/0}. The caller maps every error to invalid_grant.