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
typMUST beoauth-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
kidand the accepted algorithms. issmatches the caller-supplied trusted issuer.audis exactly this server's issuer identifier - a single string, or an array of exactly one element equal to it (draft §6.1).client_idmatches the authenticated client making the token request.- the REQUIRED claims
iss,sub,aud,client_id,jti,exp,iatare present and well-typed. expis in the future;iat/nbfare not in the future (60s skew); the lifetimeexp - iatdoes not exceed:max_lifetime_secondswhen set.
Summary
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
The validated, string-keyed ID-JAG claim set.
@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
@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
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.
@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'sissmust equal.:audience- this server's issuer identifier the assertion'saudmust identify.:client_id- the authenticated client; theclient_idclaim must equal it (draft §6.1).
Optional opts:
:accepted_algs- JOSE algorithms a candidate key may sign with. Defaults toAttesto.SigningAlg.allowed/0(includes RS256, which enterprise IdPs commonly use - unlike the FAPI request-object default).:max_lifetime_seconds- reject an assertion whoseexp - iatexceeds this bound.:now- the verification instant (aDateTimeor 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.