oidc
The OpenID Connect relying party: the login half of enterprise SSO. discover reads an issuer's well-known document and its JWKS; config adds the caller's own client identity; begin generates state, nonce and a PKCE verifier together and returns the authorization URL with them; complete redeems the code and validates the ID token in full; refresh redeems a refresh token, honouring rotation, and revalidates the returned ID token against the original claims per OIDC Core section 12.2; device-begin and device-poll are RFC 8628, the grant a terminal logs in with — a CLI, an agent, a device with no browser — where waiting answers a device-pending VALUE rather than a refusal; userinfo reads the claims a thin ID token leaves behind the UserInfo endpoint; logout-url and verify-logout-token cover RP-initiated and back-channel logout. Verification composes cx-stdlib/crypto section 3.10 rather than repeating it. The module stores nothing — no discovery cache, no session, no token store — so the caller persists what it needs, and every validation verb is pure and reads no clock, which is what lets a whole login replay in a fixture. When a provider rotates its signing keys the impure verbs re-fetch the published JWKS exactly once on an unknown kid and hand the refreshed set back for the caller to persist; the pure validator never dials, so it refuses and leaves the re-read to its caller.
oidc:discover
[$oidc:discover] -> element — Fetch an issuer's discovery document and its JWKS as one provider value; refuses when the document names a different issuer.
[?lib 'cx-stdlib/oidc']
[$oidc:discover "https://idp.example" {}]
cx-err:CXER0271
oidc:config
[$oidc:config] -> element — Combine provider metadata with this client's own identity; refuses a non-https endpoint outside loopback.
[?lib 'cx-stdlib/oidc']
[?let [= $c [$oidc:config [oidc-provider issuer="http://127.0.0.1:9000" authorization-endpoint="http://127.0.0.1:9000/authorize" token-endpoint="http://127.0.0.1:9000/token" jwks-uri="http://127.0.0.1:9000/jwks"] {client-id: "my-client", redirect-uri: "http://127.0.0.1:8080/cb", allow-insecure: true}]]
$c@client-id]
my-client
oidc:begin
[$oidc:begin] -> element — Generate state, nonce and a PKCE verifier TOGETHER and return them with the authorization URL — so a caller cannot omit one. opts.client-auth, when given, refuses a public client here rather than after the round trip if the provider does not advertise S256.
[?lib 'cx-stdlib/oidc']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[$oidc:begin $c {}]]
cx-err:CXER0271
oidc:complete
[$oidc:complete] -> element — Redeem the authorization code and validate the ID token in full; checks state before the code is ever spent.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[= $r [auth-request state="ST" nonce="NO" code-verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"]]
[$oidc:complete $c $r {code: "abc", state: "FORGED"} [client-auth method=:none] [$time:datetime 2023 11 14 22 13 20] {}]]
cx-err:CXER5305
oidc:refresh
[$oidc:refresh] -> element — Redeem a refresh token; when the provider rotates, the NEW refresh token is what comes back and must be persisted.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[$oidc:refresh $c "rt-1" [client-auth method=:none] [$time:datetime 2023 11 14 22 13 20] {}]]
cx-err:CXER0271
oidc:client-credentials
[$oidc:client-credentials] -> element — The machine grant: exchange the client's own credentials for an access token. No user, so no ID token and no refresh token — a machine token is re-minted before expires-at.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[$oidc:client-credentials $c [client-auth method=:secret-basic secret="s3cr3t"] [$time:datetime 2023 11 14 22 13 20] {scope: "invoices.read"}]]
cx-err:CXER0271
oidc:device-begin
[$oidc:device-begin] -> element — RFC 8628 device authorization: ask the provider for a user code and the URL to type it at. Refuses a provider that publishes no device_authorization_endpoint — that identity provider runs no device flow, and saying so is a configuration answer rather than a dial that fails.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[$oidc:device-begin $c [client-auth method=:none] [$time:datetime 2023 11 14 22 13 20] {}]]
cx-err:CXER5302
oidc:device-poll
[$oidc:device-poll] -> element — Ask the token endpoint whether the person has authorized the device yet. Waiting answers [device-pending …] — a VALUE, not a refusal — and a device code past its expires-at is refused here rather than at the provider, because a CLI polls every five seconds forever.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks" device-authorization-endpoint="https://idp.example/device"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[= $d [device-authorization device-code="dc-1" user-code="WDJB-MJHT" verification-uri="https://idp.example/device" interval=5 expires-at=1700000600]]
[$oidc:device-poll $c $d [client-auth method=:none] [$time:datetime 2023 11 14 23 13 20] {}]]
cx-err:CXER5306
oidc:userinfo
[$oidc:userinfo] -> element — GET the UserInfo endpoint with the access token; the response sub MUST equal the ID token's, which is what makes an Okta-style thin ID token usable.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/bytes']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks" userinfo-endpoint="https://idp.example/userinfo" [$crypto:jwks-parse "{\"keys\":[{\"kid\":\"ec-1\",\"kty\":\"EC\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"4Mg1NqshqIG122ZxPAg46RorcuJa3mXcY4U1-eRFeY8\",\"y\":\"XGRq9zTDZm0MiVHsG1-1r8rKpzCgGHzwEp27eTRlQGU\"}]}"]]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[= $r [auth-request state="ST" nonce="NO" code-verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"]]
[= $k [ec-private-key crv="P-256" [d [$bytes:from-hex "e4219571384fcb5e48a3093856cb2a801453f0ebd002b4021688c593418963be"]]]]
[= $now [$time:datetime 2023 11 14 22 13 20]]
[= $t [$crypto:jwt-sign {iss: 'https://idp.example', sub: 'user-1', aud: 'my-client', nonce: 'NO', exp: 1700003600} $k {alg: "ES256", kid: "ec-1"}]]
[= $tk [tokens access="at-1" [$oidc:validate-id-token $c $r $t $now {}]]]
[$oidc:userinfo $c $tk $now {}]]
cx-err:CXER0271
oidc:pkce-challenge
[$oidc:pkce-challenge] -> string — Derive the S256 PKCE code challenge from a verifier (RFC 7636); plain is refused.
[?lib 'cx-stdlib/oidc']
[$oidc:pkce-challenge "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"]
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
oidc:validate-id-token
[$oidc:validate-id-token] -> element — Validate an ID token beyond its signature — issuer, subject, audience, azp, nonce binding, at_hash and auth_time — each failure with its own code; opts.alg narrows the signature algorithms this deployment accepts.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/bytes']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks" [$crypto:jwks-parse "{\"keys\":[{\"kid\":\"ec-1\",\"kty\":\"EC\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"4Mg1NqshqIG122ZxPAg46RorcuJa3mXcY4U1-eRFeY8\",\"y\":\"XGRq9zTDZm0MiVHsG1-1r8rKpzCgGHzwEp27eTRlQGU\"}]}"]]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[= $r [auth-request state="ST" nonce="NO" code-verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"]]
[= $k [ec-private-key crv="P-256" [d [$bytes:from-hex "e4219571384fcb5e48a3093856cb2a801453f0ebd002b4021688c593418963be"]]]]
[= $now [$time:datetime 2023 11 14 22 13 20]]
[= $t [$crypto:jwt-sign {iss: 'https://idp.example', sub: 'user-1', aud: 'my-client', nonce: 'NO', exp: 1700003600} $k {alg: "ES256", kid: "ec-1"}]]
[$crypto:claim [$oidc:validate-id-token $c $r $t $now {}] "sub"]]
user-1
oidc:logout-url
[$oidc:logout-url] -> string — Build the RP-initiated logout URL.
[?lib 'cx-stdlib/oidc']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks" end-session-endpoint="https://idp.example/logout"]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[$oidc:logout-url $c {post-logout-redirect-uri: "https://rp.example/bye"}]]
'https://idp.example/logout?post_logout_redirect_uri=https%3A%2F%2Frp.example%2Fbye&client_id=my-client'
oidc:verify-logout-token
[$oidc:verify-logout-token] -> element — Verify an OIDC back-channel logout token; refuses one carrying a nonce, one typed as anything but logout+jwt, or one whose aud is not this client — each of which is some other token presented at the logout endpoint.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/bytes']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks" [$crypto:jwks-parse "{\"keys\":[{\"kid\":\"ec-1\",\"kty\":\"EC\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"4Mg1NqshqIG122ZxPAg46RorcuJa3mXcY4U1-eRFeY8\",\"y\":\"XGRq9zTDZm0MiVHsG1-1r8rKpzCgGHzwEp27eTRlQGU\"}]}"]]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[= $r [auth-request state="ST" nonce="NO" code-verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"]]
[= $k [ec-private-key crv="P-256" [d [$bytes:from-hex "e4219571384fcb5e48a3093856cb2a801453f0ebd002b4021688c593418963be"]]]]
[= $now [$time:datetime 2023 11 14 22 13 20]]
[= $t [$crypto:jwt-sign {iss: 'https://idp.example', sub: 'u', aud: 'my-client', exp: 1700003600, events: {'http://schemas.openid.net/event/backchannel-logout': {}}} $k {alg: "ES256", kid: "ec-1", typ: "logout+jwt"}]]
[$crypto:claim [$oidc:verify-logout-token $c $t $now] "sub"]]
u
oidc:claims
[$oidc:claims] -> element — Read the verified claim-set out of a tokens value — a path step does not give the element back, and this is what session and authz consume.
[?lib 'cx-stdlib/oidc']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/bytes']
[?lib 'cx-stdlib/time']
[?let [= $p [oidc-provider issuer="https://idp.example" authorization-endpoint="https://idp.example/authorize" token-endpoint="https://idp.example/token" jwks-uri="https://idp.example/jwks" [$crypto:jwks-parse "{\"keys\":[{\"kid\":\"ec-1\",\"kty\":\"EC\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"4Mg1NqshqIG122ZxPAg46RorcuJa3mXcY4U1-eRFeY8\",\"y\":\"XGRq9zTDZm0MiVHsG1-1r8rKpzCgGHzwEp27eTRlQGU\"}]}"]]]
[= $c [$oidc:config $p {client-id: "my-client", redirect-uri: "https://rp.example/cb"}]]
[= $r [auth-request state="ST" nonce="NO" code-verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"]]
[= $k [ec-private-key crv="P-256" [d [$bytes:from-hex "e4219571384fcb5e48a3093856cb2a801453f0ebd002b4021688c593418963be"]]]]
[= $now [$time:datetime 2023 11 14 22 13 20]]
[= $t [$crypto:jwt-sign {iss: "https://idp.example", sub: "user-1", aud: "my-client", nonce: "NO", exp: 1700003600} $k {alg: "ES256", kid: "ec-1"}]]
[= $tk [tokens access="at-1" [$oidc:validate-id-token $c $r $t $now {}]]]
[$crypto:claim [$oidc:claims $tk] "sub"]]
user-1