A flexible, protocol-based OpenID Connect Provider implementation for the JVM using Clojure.
jwks-uri) or as a full OpenID Connect providertoken_type_hint lookuptoken-response, registration-response, revocation-response, userinfo-responseAdd to your deps.edn:
{:deps {net.carcdr/oidc-provider {:mvn/version "0.7.1"}}}
Or for Leiningen:
[net.carcdr/oidc-provider "0.7.1"]
This library does not perform JSON serialization. Public functions accept and return plain Clojure data, and Ring response helpers (token-response, registration-response, revocation-response, userinfo-response) return Ring maps whose :body values are Clojure maps, not JSON strings.
Integrators are responsible for wiring middleware at the edges:
ring.middleware.json/wrap-json-response (or equivalent) to serialize :body maps to JSONring.middleware.json/wrap-json-body so registration-response receives a pre-parsed keyword mapring.middleware.params/wrap-params + ring.middleware.keyword-params/wrap-keyword-params for the token and revocation endpoints(require '[oidc-provider.core :as provider]
'[oidc-provider.protocol :as proto]
'[oidc-provider.util :as util])
;; Implement a claims provider. `scope` is a vector of scope strings.
(defrecord SimpleClaimsProvider []
proto/ClaimsProvider
(get-claims [_ user-id scope]
(cond-> {:sub user-id}
(some #{"profile"} scope) (assoc :name "Test User")
(some #{"email"} scope) (assoc :email "user@example.com"
:email_verified true))))
;; Create a provider
(def my-provider
(provider/create-provider
{:issuer "https://idp.example.com"
:authorization-endpoint "https://idp.example.com/authorize"
:token-endpoint "https://idp.example.com/token"
:jwks-uri "https://idp.example.com/jwks"
:userinfo-endpoint "https://idp.example.com/userinfo"
:claims-provider (->SimpleClaimsProvider)}))
;; Register a client. Secrets are stored hashed — use `util/hash-client-secret`.
(provider/register-client
my-provider
{:client-id "my-app"
:client-type "confidential"
:client-secret-hash (util/hash-client-secret "secret123")
:redirect-uris ["https://app.example.com/callback"]
:grant-types ["authorization_code" "refresh_token"]
:response-types ["code"]
:scopes ["openid" "profile" "email" "offline_access"]
:token-endpoint-auth-method "client_secret_basic"})
;; Discovery + JWKS
(provider/discovery-metadata my-provider)
(provider/jwks my-provider)
Authentication is the responsibility of your host application. The provider handles everything after the user has been authenticated.
;; 1. Parse and validate the authorization request. `params` is the keyword map
;; produced by Ring's wrap-params + wrap-keyword-params middleware.
(def auth-req
(provider/parse-authorization-request
my-provider
{:response_type "code"
:client_id "my-app"
:redirect_uri "https://app.example.com/callback"
:scope "openid profile"
:state "xyz"}))
;; 2. Authenticate the user (your application logic). This library does not
;; handle authentication — use whatever fits (session, SSO, form login, …).
(def user-id "user-123")
(def auth-time (quot (System/currentTimeMillis) 1000)) ;; epoch seconds
;; 3. After consent, build the redirect URL back to the client.
;; `auth-time` flows through to the `auth_time` claim in the ID token.
(provider/authorize my-provider auth-req user-id auth-time)
;; => "https://app.example.com/callback?code=..."
;; 4. Exchange the code for tokens.
(provider/token-request
my-provider
{:grant_type "authorization_code"
:code "authorization-code-from-callback"
:client_id "my-app"
:client_secret "secret123"
:redirect_uri "https://app.example.com/callback"}
nil) ;; or the raw "Authorization: Basic …" header value
;; => {:access_token "…" :id_token "…" :refresh_token "…" …}
A refresh token is only issued when the client has refresh_token in :grant-types and the request included the offline_access scope (OIDC Core §11).
prompt and max_ageparse-authorization-request validates and parses prompt and max_age per OIDC Core §3.1.2.1. The validated request exposes :prompt-values (a keyword set, e.g. #{:login}) and :max-age (an integer). Helpers in oidc-provider.authorization let host applications enforce the semantics:
validate-prompt-none — returns a login_required error redirect when prompt=none is requested but the user is unauthenticatedvalidate-max-age — returns true when the user's auth_time is within the requested windowEach endpoint has a Ring response helper that returns a Ring map with a Clojure data :body. Wire your router and JSON middleware around them.
(require '[ring.middleware.json :as rj]
'[ring.middleware.params :refer [wrap-params]]
'[ring.middleware.keyword-params :refer [wrap-keyword-params]])
(defn handler [provider]
(fn [{:keys [uri request-method] :as request}]
(cond
(and (= uri "/token") (= request-method :post))
(provider/token-response provider request)
(= uri "/userinfo")
(provider/userinfo-response provider request)
(or (= uri "/register") (clojure.string/starts-with? uri "/register/"))
(provider/registration-response provider request)
(and (= uri "/revoke") (= request-method :post))
(provider/revocation-response provider request)
(and (= uri "/.well-known/openid-configuration") (= request-method :get))
{:status 200 :body (provider/discovery-metadata provider)}
(and (= uri "/jwks") (= request-method :get))
{:status 200 :body (provider/jwks provider)}
:else {:status 404 :body {:error "not_found"}})))
(def app
(-> (handler my-provider)
rj/wrap-json-response
rj/wrap-json-body
wrap-keyword-params
wrap-params))
Authorization-endpoint errors can be rendered with authorization-error-response: it dispatches on the oidc-provider.error hierarchy, returning a 400 page for non-redirectable failures (unknown client_id, invalid redirect_uri) and a 302 error redirect otherwise.
Provides user claims for ID tokens and the UserInfo endpoint based on the authenticated user and requested scopes:
(defprotocol ClaimsProvider
(get-claims [this user-id scope]))
Registered JWT claims (iss, sub, aud, exp, iat, nonce, auth_time, azp, at_hash) are protected — returning them from get-claims does not overwrite provider-generated values.
Storage is pluggable. In-memory defaults are provided for development.
(defprotocol ClientStore
(get-client [this client-id])
(register-client [this client-config])
(update-client [this client-id updated-config])
(delete-client [this client-id]))
(defprotocol AuthorizationCodeStore
(save-authorization-code [this code user-id client-id redirect-uri scope
nonce expiry code-challenge code-challenge-method
resource auth-time])
(get-authorization-code [this code])
(delete-authorization-code [this code])
(consume-authorization-code [this code]) ;; atomic get+delete
(mark-code-exchanged [this code access-token refresh-token])
(get-code-tokens [this code])) ;; for replay detection
(defprotocol TokenStore
(save-access-token [this token user-id client-id scope expiry resource])
(get-access-token [this token])
(save-refresh-token [this token user-id client-id scope expiry resource])
(get-refresh-token [this token])
(revoke-token [this token]))
The provider wraps whatever code and token stores you supply with HashingAuthorizationCodeStore / HashingTokenStore, so tokens and codes reach your implementation as SHA-256 hashes. Client secrets and registration access tokens are PBKDF2-hashed via oidc-provider.util/hash-client-secret.
Replaying a previously consumed authorization code revokes the access and refresh tokens that were issued for it, per RFC 6749 §10.5.
Provider configuration options for create-provider:
{:issuer "https://idp.example.com" ;; Required
:authorization-endpoint "…/authorize" ;; Required
:token-endpoint "…/token" ;; Required
;; OIDC — omit all three to run as a plain OAuth2 server
:jwks-uri "…/jwks"
:signing-key rsa-key ;; single RSAKey
:signing-keys [rsa-key-1 rsa-key-2] ;; for rotation
:active-signing-key-id "key-id-for-new-tokens"
;; Optional endpoint advertisements (also surfaced in discovery)
:userinfo-endpoint "…/userinfo"
:registration-endpoint "…/register"
:revocation-endpoint "…/revoke"
;; TTLs (seconds)
:access-token-ttl-seconds 3600 ;; default 3600
:id-token-ttl-seconds 3600 ;; default 3600
:authorization-code-ttl-seconds 600 ;; default 600
:refresh-token-ttl-seconds nil ;; no expiry unless set
:rotate-refresh-tokens true ;; default true
;; Server policy
:grant-types-supported ["authorization_code" "refresh_token"]
:allow-http-issuer false ;; true for local dev
:clock (java.time.Clock/systemUTC)
;; Pluggable implementations
:claims-provider claims-provider
:client-store client-store
:code-store code-store
:token-store token-store}
The issuer URL is validated per RFC 8414 §2: HTTPS with no query or fragment. Set :allow-http-issuer true to permit HTTP issuers during local development. If no signing keys or :jwks-uri are provided, the provider runs as a plain OAuth2 server and omits OIDC features.
(provider/token-request
my-provider
{:grant_type "authorization_code"
:code "..."
:client_id "my-app"
:client_secret "secret123"
:redirect_uri "https://app.example.com/callback"}
nil)
PKCE is enforced for public clients: include code_challenge / code_challenge_method on the authorization request and code_verifier on the exchange.
(provider/token-request
my-provider
{:grant_type "refresh_token"
:refresh_token "..."
:client_id "my-app"
:client_secret "secret123"}
nil)
By default the refresh token is rotated on each grant (:rotate-refresh-tokens true).
(provider/token-request
my-provider
{:grant_type "client_credentials"
:client_id "my-app"
:client_secret "secret123"
:scope "api:read api:write"}
nil)
Only confidential clients may use client_credentials (RFC 6749 §4.4).
dynamic-register-client accepts a registration request map in snake_case wire format, validates it, generates credentials, and returns the registration response:
(provider/dynamic-register-client
my-provider
{:redirect_uris ["https://app.example.com/callback"]
:grant_types ["authorization_code"]
:response_types ["code"]
:client_name "My App"
:token_endpoint_auth_method "client_secret_basic"})
;; => {:client_id "…" :client_secret "…" :client_id_issued_at …
;; :client_secret_expires_at 0 :registration_client_uri "…" …}
When a :registration-endpoint is configured, responses include registration_client_uri and a registration_access_token the client can use with dynamic-read-client, dynamic-update-client, and dynamic-delete-client for subsequent management. registration-response dispatches POST / GET / PUT / DELETE on those resources.
(provider/revocation-response my-provider request)
The request must be POST application/x-www-form-urlencoded with a token parameter and client authentication. token_type_hint (access_token or refresh_token) optimizes lookup. Token ownership is verified before revocation.
(provider/userinfo-response my-provider request)
Accepts GET or POST with Authorization: Bearer <access_token>. The provider validates the token, filters claims by the token's scope, and returns the claims map. On failure the response includes WWW-Authenticate: Bearer per RFC 6750 §3.
clojure -X:test
OIDC conformance suite harness:
clojure -M:conformance # Basic OP profile
clojure -M:conformance-comprehensive # PKCE, dynamic registration, refresh, request objects, strict redirect URIs
Can you improve this documentation? These fine people already did:
github-actions[bot] & Edward PagetEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |