Current section

Files

Jump to
Raw

README.md

# AttestoClient

[![Hex.pm](https://img.shields.io/hexpm/v/attesto_client)](https://hex.pm/packages/attesto_client)
[![Hexdocs.pm](https://img.shields.io/badge/docs-hexdocs.pm-blue)](https://hexdocs.pm/attesto_client)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![OpenID RP Certified — Attesto 1.0 (2026-07-07)](https://img.shields.io/badge/OpenID_RP-Certified_(v1.0,_2026--07--07)-F78C40)](https://openid.net/certification/certified-openid-relying-parties-profiles/)
[![OpenID4VCI 1.0 + HAIP 1.0 Wallet Certified — Attesto Client 2.7.0 / Attesto 2.3.1 (2026-10-08)](https://img.shields.io/badge/OpenID4VCI_1.0_%2B_HAIP_1.0-Wallet_Certified_(v2.7.0_/_v2.3.1,_2026--10--08)-F78C40)](https://openid.net/certification/certified-oid4vci-haip-final/)
[![OpenID4VP 1.0 + HAIP 1.0 Wallet Certified — Attesto Client 2.7.0 / Attesto 2.3.1 (2026-10-08)](https://img.shields.io/badge/OpenID4VP_1.0_%2B_HAIP_1.0-Wallet_Certified_(v2.7.0_/_v2.3.1,_2026--10--08)-F78C40)](https://openid.net/certification/certified-oid4vp-haip-final/)

<a href="https://openid.net/certification/certified-openid-relying-parties-profiles/"><img src="https://openid.net/wordpress-content/uploads/2016/04/oid-l-certification-mark-l-rgb-150dpi-90mm.png" alt="OpenID Certified" width="180" align="right"></a>

AttestoClient is
[OpenID Certified](https://openid.net/certification/certified-openid-relying-parties-profiles/)
as a Relying Party library to the **Basic**, **Config**, and **Dynamic** RP
profiles (`Attesto 1.0`, certified `2026-07-07`), and as an
[**OpenID4VCI 1.0 + HAIP 1.0 Wallet**](https://openid.net/certification/certified-oid4vci-haip-final/)
and [**OpenID4VP 1.0 + HAIP 1.0 Wallet**](https://openid.net/certification/certified-oid4vp-haip-final/)
(`Attesto Client 2.7.0` / `Attesto 2.3.1`, certified `2026-10-08`), run against
the OpenID Foundation's conformance suite.

Run a secure OpenID Connect Authorization Code + PKCE exchange, refresh and
revoke tokens, build RP-Initiated Logout requests, and build or verify the OAuth
and OpenID Connect wire artifacts an Elixir client needs: `private_key_jwt`,
signed authorization request objects (JAR), strict ID Token verification, JARM,
ID-JAG/EMA assertions, signed introspection and UserInfo, and discovery/JWKS.

Use it when you are writing a relying party or OAuth client that needs secure
protocol mechanics without delegating application policy:

- Authenticate to token, PAR, or introspection endpoints with
  `private_key_jwt`.
- Send signed authorization requests for JAR/FAPI deployments.
- Verify ID Tokens, signed authorization responses returned through JARM, signed
  introspection responses, and signed UserInfo responses.
- Build ID-JAG/EMA identity assertions for the JWT-bearer grant.
- Generate S256 PKCE verifier/challenge pairs.
- Fetch authorization-server metadata and JWKS with issuer validation.
- Verify RFC 9068 JWT access tokens from a remote issuer with coordinated JWKS
  rotation, bounded caching and stale-key use, DPoP/mTLS binding, and Plug
  integration.
- Correlate state/nonce/PKCE and an opaque browser-session binding in an atomic,
  expiring transaction store.
- Single-flight concurrent refresh-token rotation with bounded deadlines.
- Revoke tokens and create RP-Initiated Logout requests.
- Run the OID4VCI/OID4VP **wallet-holder** flow: receive a credential offer,
  request and verify credentials, and build presentations (EU wallet / HAIP).

`AttestoClient` is the client-side counterpart to
[`attesto`](https://hex.pm/packages/attesto). `attesto` verifies client artifacts
and issues server artifacts with the authorization server's keystore;
`AttestoClient` builds artifacts signed with the client's own key and verifies
the server artifacts a client receives.

The Attesto family also includes:

- [`attesto`](https://hex.pm/packages/attesto) — the transport-neutral OAuth,
  OIDC, FAPI, DPoP, and mTLS engine beneath this package.
- [`attesto_phoenix`](https://hex.pm/packages/attesto_phoenix) — the
  batteries-included Phoenix/Ecto authorization server built on `attesto`.
- [`attesto_mcp`](https://hex.pm/packages/attesto_mcp) — the reusable OAuth
  resource-server boundary for custom MCP integrations.
- [`attesto_mcp_server`](https://hex.pm/packages/attesto_mcp_server) — the
  batteries-included authenticated Model Context Protocol server.

It does **not** make authorization decisions or own application sessions. Its
included ETS store retains only short-lived protocol correlation data, and its
refresh coordinator retains no token set after a flight completes. The host
chooses its durable/distributed store, atomically persists rotation results,
maps verified identities to authorization, and applies session-retention
policy. `AttestoClient.DPoP` generates the RFC 9449 proofs for its own token
and credential requests; [`req_dpop`](https://hex.pm/packages/req_dpop) remains
the tool for DPoP-binding arbitrary outgoing requests.

## What it provides

- `AttestoClient.ClientAssertion` — `private_key_jwt` client authentication
  assertions (RFC 7523 / OpenID Connect Core §9).
- `AttestoClient.RequestObject` — signed authorization request objects (JAR,
  RFC 9101 / FAPI 2.0 Message Signing §5.3.1).
- `AttestoClient.JARM` — verify signed authorization responses (JARM, FAPI 2.0
  Message Signing §5.4) with unique-key selection and key-aware FAPI policy.
- `AttestoClient.IDToken` — verify OpenID Connect ID Tokens, including nonce,
  `max_age`, and key-aware `at_hash`, `c_hash`, and `s_hash` for RSA, EC,
  Ed25519, and Ed448 signatures.
- `AttestoClient.IdentityAssertion` — build Identity Assertion JWT
  Authorization Grant assertions (ID-JAG / EMA).
- `AttestoClient.PKCE` — generate S256 PKCE verifier/challenge pairs.
- `AttestoClient.SignedIntrospection` — verify RFC 9701 signed token
  introspection responses.
- `AttestoClient.UserInfo` — verify signed OpenID Connect UserInfo responses
  and bind them to a verified ID Token subject when supplied.
- `AttestoClient.Discovery` — fetch and read authorization-server metadata and
  JWKS (RFC 8414 / OpenID Connect Discovery 1.0), and find the authorization
  server for a protected resource from its RFC 9728 metadata.
- `AttestoClient.WWWAuthenticate` — read `WWW-Authenticate` challenges,
  including the RFC 9728 `resource_metadata` pointer.
- `AttestoClient.Issuer` — a supervised cache of one issuer's discovery
  metadata and JWKS, refreshed on a schedule and on an unknown `kid`; pass
  `issuer: {AttestoClient.Issuer, name}` to verify ID Tokens against it.
- `AttestoClient.ResourceServer` — verify remote-issuer RFC 9068 JWT access
  tokens, coordinate JWKS refresh, and enforce exact OAuth scopes.
- `AttestoClient.ResourceServer.Plug` — authenticate Bearer, DPoP, and mTLS
  requests, fail closed on missing sender-constraint evidence, and assign
  verified claims.
- `AttestoClient.AuthorizationCode` — OAuth or OIDC Authorization Code flow
  with S256 PKCE, PAR, DPoP, RFC 8707 resource indicators, issuer and
  browser-session binding, and one-time state; OIDC additionally verifies nonce
  and the ID Token. `start/2` with `:return` hands the transaction to the
  application instead of a store, and `exchange/2` is the code-for-token
  request on its own.
- `AttestoClient.AuthorizationTransaction.Store.ETS` — bounded, expiring,
  single-node transaction store with atomic consumption.
- `AttestoClient.RefreshCoordinator` and `AttestoClient.Token` — deadline-bound,
  single-flight refresh rotation and RFC 7009 revocation.
- `AttestoClient.Logout` — RP-Initiated Logout request construction.

### Wallet-holder (OID4VCI / OID4VP)

The client-side of the EU-wallet stack — the holder role that receives and
presents verifiable credentials, mirroring `attesto`'s issuer/verifier core.

- `AttestoClient.Wallet` — the OID4VCI issuance flow end to end: exchange a
  credential offer's pre-authorized code, prove the holder key, request the
  credential, and verify each returned SD-JWT VC, `jwt_vc_json`, or mdoc. Supports
  batch issuance, bounded deferred polling, encrypted credential requests and
  responses, and the OID4VCI §11 Notification Endpoint. Authorization-code
  issuance uses `AttestoClient.AuthorizationCode` in plain OAuth mode.
- `AttestoClient.DPoP` — RFC 9449 DPoP proofs, sender-constraining the token and
  credential requests (with a `use_dpop_nonce` retry).
- `AttestoClient.WalletAttestation` / `AttestoClient.KeyAttestation` — OAuth
  attestation-based client authentication (Client Attestation + PoP) and OID4VCI
  key attestation, the HAIP-recommended wallet credentials.
- `AttestoClient.Wallet.Proof` / `AttestoClient.Wallet.CredentialOffer` — the
  holder key proof and credential-offer parsing.
- `AttestoClient.Wallet.Presentation` / `AttestoClient.Wallet.PresentationRequest`
  — OID4VP: verify a signed Authorization Request, match DCQL queries, and
  build a `direct_post` or encrypted `direct_post.jwt` response (SD-JWT VC with
  a holder Key Binding JWT, or an ISO 18013-5 mdoc DeviceResponse). HAIP request
  verification uses explicit certificate trust anchors. The application owns
  consent and chooses the credentials and claims to disclose.

Start with the [Credential wallet guide](guides/credential-wallet.md), which
progresses from a local selective-disclosure presentation to issuance, OAuth
authorization, encryption, deferred polling and HAIP integration. The
[interactive wallet Livebook](guides/digital_wallet.livemd) runs a local
SD-JWT VC example and an encrypted presentation without an HTTP server.

The RP certification above covers the listed OpenID Connect profiles (`Attesto 1.0`, certified `2026-07-07`).
Wallet-role [**OpenID4VCI 1.0 + HAIP 1.0**](https://openid.net/certification/certified-oid4vci-haip-final/)
and [**OpenID4VP 1.0 + HAIP 1.0**](https://openid.net/certification/certified-oid4vp-haip-final/)
certification was completed under `Attesto Client 2.7.0` (`Attesto 2.3.1`) on `2026-10-08`, covering SD-JWT VC and ISO mdoc for wallet-initiated, issuer-initiated, and pre-authorized code issuance flows and `direct_post.jwt` presentations.

## Example

```elixir
key = JOSE.JWK.generate_key({:ec, "P-256"})

{:ok, assertion} =
  AttestoClient.ClientAssertion.build(key,
    client_id: "my-client",
    audience: "https://op.example.com"
  )

# Submit it at the token / PAR / introspection endpoint:
#   client_assertion_type = AttestoClient.ClientAssertion.assertion_type()
#   client_assertion      = assertion
```

For a full authorization flow, store setup, callback handling, refresh,
revocation, and logout, see the
[Authorization Code guide](guides/authorization-code.md).

For inbound JWT access-token verification backed by a remote issuer, including
key rotation, stale-key behavior, scope checks, and Plug wiring, see the
[Remote issuer resource-server guide](guides/resource-server.md).

## Assurance

Build-side artifacts carry **cross-language parity tests** where practical: they
are checked against an independent, non-Elixir reference implementation (e.g.
PyJWT or Node's native crypto implementation), so correctness does not rest only
on this library and `attesto` agreeing with each other. This includes exact and
legacy Ed25519/Ed448 signatures plus the curve-dependent OIDC hash construction.
The mirror modules also carry in-family interop tests against the corresponding
attesto server-side issuer or verifier. External-reference tests skip cleanly
when their toolchain is absent, so they never block a plain `mix test`.

For release confidence, run them explicitly against a Python with the reference
libraries installed (the system Python is usually PEP-668 externally managed, so
use a venv):

```sh
python3 -m venv .venv
.venv/bin/pip install "pyjwt[crypto]"
ATTESTO_CLIENT_PYTHON=.venv/bin/python ATTESTO_PATH=1 mix test
```

When `ATTESTO_CLIENT_PYTHON` is unset the harness falls back to `python3` on the
`PATH`.

## Status

A stable `2.x` release: the public API follows [semantic versioning](https://semver.org/) —
minor and patch releases are backward-compatible, and breaking changes wait for
a new major version. Pin to `~> 2.17`.

## Requirements

AttestoClient requires Elixir 1.18 or later, Attesto `>= 2.15.0 and < 3.0.0`,
and JOSE 1.11.13 or later within the JOSE 1.x line.
The JOSE range keeps the patched
security floor while allowing native OTP SHA-3 and Ed448 improvements in later
compatible releases. Both this package and `attesto` use Elixir's built-in
`JSON` module, so lowering only this package's declared floor would not create a
working older-Elixir installation.

The Attesto floor supplies the shared JWS verifier, DPoP proof builder, URI
and outbound-address policies, X.509 path validator, and strict CBOR decoder.
`AttestoClient.Wallet.Presentation.CertificateTrust` retains its public
functions as delegates to `Attesto.X509`. Wallet CBOR decoding refuses repeated
map keys, including nested maps, COSE protected headers, and embedded mdoc
items (RFC 9052 §3; RFC 8949 §5.6), with the existing byte limits and core's
default nesting limit of 32.

Ed448 verification and OIDC hash claims additionally require JOSE to have a
working Curve448 and SHAKE256 backend. That may come from supported native OTP
crypto in a later JOSE 1.x release, an installed backend, or JOSE's cryptographic
fallback. Operations fail closed when the required capability is unavailable.