CX · Reference guideoidc
PlaygroundDownloadsAboutv0.18.0-pre.1

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:authorize-url

[$oidc:authorize-url] -> string — Build the authorization redirect URL; parameter order is fixed, so an equal request is a byte-identical 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"]]
      [= $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:authorize-url $c $r {}]]
    
      'https://idp.example/authorize?response_type=code&client_id=my-client&redirect_uri=https%3A%2F%2Frp.example%2Fcb&scope=openid&state=ST&nonce=NO&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256'
    

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