# openidconnect-rs `openidconnect` (v4.0.1) is an extensible, strongly-typed Rust library for the [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) protocol. It implements the full Relying Party (client) interface for authenticating users via providers such as Google, GitLab, Microsoft, and any other OIDC-compliant server. It also provides strongly-typed data structures for building OpenID Connect Provider (server-side) components, including Discovery documents, JSON Web Key Sets, and signed ID tokens. The library is built on top of the `oauth2` crate and exposes a stateful, typestate-checked `Client` struct where endpoint availability is tracked at compile time — attempting to call an operation that requires a missing endpoint produces a compiler error rather than a runtime panic. It supports synchronous and asynchronous HTTP clients (`reqwest`, `ureq`, `curl`, or custom), PKCE, device authorization grants, token introspection, token revocation, RP-Initiated Logout, and Dynamic Client Registration. All user-facing types are strongly typed; secrets (nonce, CSRF token, access token) do not implement `PartialEq` by default to discourage timing-unsafe comparisons. --- ## Cargo.toml setup Add the crate with the desired HTTP backend feature flags. ```toml [dependencies] # Default: async reqwest + rustls-tls openidconnect = "4" # Synchronous (blocking) reqwest client openidconnect = { version = "4", features = ["reqwest-blocking"] } # ureq (sync only, minimal deps) openidconnect = { version = "4", default-features = false, features = ["ureq"] } # curl (sync only) openidconnect = { version = "4", default-features = false, features = ["curl"] } # No built-in HTTP client (bring your own) openidconnect = { version = "4", default-features = false } # Timing-resistant PartialEq for secret types (sha256-based constant-time compare) openidconnect = { version = "4", features = ["timing-resistant-secret-traits"] } ``` --- ## `CoreProviderMetadata::discover` / `discover_async` — OIDC Discovery Fetches the provider's `.well-known/openid-configuration` document. Returns a `ProviderMetadata` struct used to construct a `Client` automatically. ```rust use openidconnect::core::CoreProviderMetadata; use openidconnect::{IssuerUrl, reqwest}; // --- Synchronous --- let http_client = reqwest::blocking::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) // SSRF prevention .build() .expect("Client should build"); let provider_metadata = CoreProviderMetadata::discover( &IssuerUrl::new("https://accounts.google.com".to_string()).unwrap(), &http_client, ) .expect("Failed to discover provider"); println!("Token endpoint: {:?}", provider_metadata.token_endpoint()); println!("JWKS URI: {}", provider_metadata.jwks_uri()); // --- Asynchronous --- let http_client_async = reqwest::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); let provider_metadata_async = CoreProviderMetadata::discover_async( IssuerUrl::new("https://accounts.google.com".to_string()).unwrap(), &http_client_async, ) .await .expect("Failed to discover provider"); ``` --- ## `Client::new` — Manual client construction Constructs a `Client` directly without discovery when endpoint URLs are already known. ```rust use openidconnect::core::CoreClient; use openidconnect::{ AuthUrl, ClientId, ClientSecret, IssuerUrl, JsonWebKeySet, RedirectUrl, TokenUrl, }; let client = CoreClient::new( ClientId::new("my-client-id".to_string()), IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), JsonWebKeySet::default(), // supply real JWKS in production ) .set_client_secret(ClientSecret::new("my-client-secret".to_string())) .set_auth_uri(AuthUrl::new("https://accounts.example.com/authorize".to_string()).unwrap()) .set_token_uri(TokenUrl::new("https://accounts.example.com/token".to_string()).unwrap()) .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string()).unwrap()); // client now has EndpointSet for auth + token endpoints at compile time ``` --- ## `Client::from_provider_metadata` — Client from discovery Constructs a `Client` from previously fetched `ProviderMetadata`. Endpoints discovered from the provider are set to `EndpointMaybeSet` (fallible access), since their presence cannot be guaranteed at compile time. ```rust use openidconnect::core::{CoreClient, CoreProviderMetadata}; use openidconnect::{ClientId, ClientSecret, IssuerUrl, RedirectUrl, reqwest}; let http_client = reqwest::blocking::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build() .unwrap(); let provider_metadata = CoreProviderMetadata::discover( &IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), &http_client, ).unwrap(); let client = CoreClient::from_provider_metadata( provider_metadata, ClientId::new("client_id".to_string()), Some(ClientSecret::new("client_secret".to_string())), ) .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string()).unwrap()); ``` --- ## `Client::authorize_url` — Build authorization URL Generates the authorization URL to redirect the user's browser to. Returns the URL, a CSRF token, and a nonce. Supports Authorization Code, Implicit, and Hybrid flows. ```rust use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}; use openidconnect::{ ClientId, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, RedirectUrl, Scope, reqwest, }; let http_client = reqwest::blocking::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build().unwrap(); let provider_metadata = CoreProviderMetadata::discover( &IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), &http_client, ).unwrap(); let client = CoreClient::from_provider_metadata( provider_metadata, ClientId::new("client_id".to_string()), None, ) .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string()).unwrap()); // PKCE for public clients (no client secret, native/mobile/SPA apps) let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); let (auth_url, csrf_token, nonce) = client .authorize_url( CoreAuthenticationFlow::AuthorizationCode, CsrfToken::new_random, // random CSRF state Nonce::new_random, // random nonce ) .add_scope(Scope::new("email".to_string())) .add_scope(Scope::new("profile".to_string())) .set_pkce_challenge(pkce_challenge) .url(); // Redirect the user's browser to auth_url println!("Redirect user to: {}", auth_url); // Store csrf_token and nonce in session for later verification ``` --- ## `Client::exchange_code` — Authorization code exchange Exchanges the authorization code returned by the provider for an access token, refresh token, and ID token. ```rust use openidconnect::{ AccessTokenHash, AuthorizationCode, OAuth2TokenResponse, PkceCodeVerifier, TokenResponse, }; // After the user is redirected back with ?code=...&state=... // Verify state == csrf_token.secret() first! let token_response = client .exchange_code(AuthorizationCode::new("authorization_code_from_provider".to_string())) .unwrap() // ConfigurationError if token endpoint missing .set_pkce_verifier(pkce_verifier) // only if PKCE was used .request(&http_client) .expect("Token exchange failed"); println!("Access token: {}", token_response.access_token().secret()); println!("Scopes: {:?}", token_response.scopes()); // Verify the ID token let id_token = token_response .id_token() .expect("Server did not return an ID token"); let verifier = client.id_token_verifier(); let claims = id_token .claims(&verifier, &nonce) // verifies signature, nonce, iss, aud, exp .expect("ID token verification failed"); println!("Subject: {}", claims.subject().as_str()); println!("Email: {:?}", claims.email()); // Verify access token hash (at_hash) to prevent substitution attacks if let Some(expected_at_hash) = claims.access_token_hash() { let actual_at_hash = AccessTokenHash::from_token( token_response.access_token(), id_token.signing_alg().unwrap(), id_token.signing_key(&verifier).unwrap(), ).unwrap(); assert_eq!(actual_at_hash, *expected_at_hash, "Access token hash mismatch"); } ``` --- ## `Client::user_info` — Fetch UserInfo endpoint Retrieves additional user claims from the provider's UserInfo endpoint using the access token. ```rust use openidconnect::core::CoreUserInfoClaims; let userinfo: CoreUserInfoClaims = client .user_info(token_response.access_token().to_owned(), Some(claims.subject().clone())) // passing expected_subject prevents token substitution attacks .unwrap() // ConfigurationError if user_info endpoint not set .request(&http_client) .expect("Failed to fetch user info"); println!("Name: {:?}", userinfo.name()); println!("Email: {:?}", userinfo.email()); println!("Picture: {:?}", userinfo.picture()); ``` --- ## `Client::exchange_refresh_token` — Refresh token exchange Exchanges a refresh token for a new access token. ```rust use openidconnect::OAuth2TokenResponse; if let Some(refresh_token) = token_response.refresh_token() { let refreshed = client .exchange_refresh_token(refresh_token) .unwrap() .request(&http_client) .expect("Refresh token exchange failed"); println!("New access token: {}", refreshed.access_token().secret()); } ``` --- ## `Client::revoke_token` — RFC 7009 Token Revocation Revokes an access or refresh token at the provider's revocation endpoint. ```rust use openidconnect::{OAuth2TokenResponse, RevocationUrl}; use openidconnect::core::CoreRevocableToken; let client_with_revocation = client .set_revocation_url(RevocationUrl::new("https://accounts.example.com/revoke".to_string()).unwrap()); let token_to_revoke: CoreRevocableToken = match token_response.refresh_token() { Some(rt) => rt.into(), None => token_response.access_token().into(), }; client_with_revocation .revoke_token(token_to_revoke) .expect("Revocation request build failed") .request(&http_client) .expect("Token revocation failed"); println!("Token successfully revoked"); ``` --- ## `Client::introspect` — RFC 7662 Token Introspection Retrieves metadata about an active token from the provider's introspection endpoint. ```rust use openidconnect::{IntrospectionUrl, TokenIntrospectionResponse}; let client_with_introspection = client .set_introspection_url(IntrospectionUrl::new("https://accounts.example.com/introspect".to_string()).unwrap()); let introspection = client_with_introspection .introspect(token_response.access_token()) .request(&http_client) .expect("Introspection request failed"); println!("Token active: {}", introspection.active()); println!("Token scopes: {:?}", introspection.scopes()); println!("Token subject: {:?}", introspection.sub()); ``` --- ## `Client::exchange_device_code` / `exchange_device_access_token` — RFC 8628 Device Flow Implements the Device Authorization Grant for browserless devices (TVs, CLI tools, etc.). ```rust use openidconnect::{DeviceAuthorizationUrl, Scope}; use openidconnect::core::CoreDeviceAuthorizationResponse; let client_device = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) .set_device_authorization_url( DeviceAuthorizationUrl::new("https://accounts.example.com/device/code".to_string()).unwrap() ); // Step 1: get a device code let device_response: CoreDeviceAuthorizationResponse = client_device .exchange_device_code() .add_scope(Scope::new("profile".to_string())) .request(&http_client) .expect("Device code request failed"); println!( "Visit {} and enter code: {}", device_response.verification_uri_complete().unwrap().secret(), device_response.user_code().secret() ); // Step 2: poll for the access token (blocks until user approves or timeout) let token = client_device .exchange_device_access_token(&device_response) .unwrap() .request(&http_client, std::thread::sleep, None) .expect("Device token exchange failed"); println!("Access token: {}", token.access_token().secret()); println!("ID token: {:?}", token.extra_fields().id_token()); ``` --- ## `IdToken::new` — Sign an ID token (Provider side) Creates and signs an ID token for use in a provider implementation. ```rust use chrono::{Duration, Utc}; use openidconnect::{ AccessToken, Audience, EmptyAdditionalClaims, EmptyExtraTokenFields, EndUserEmail, IssuerUrl, JsonWebKeyId, StandardClaims, SubjectIdentifier, }; use openidconnect::core::{ CoreIdToken, CoreIdTokenClaims, CoreIdTokenFields, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, CoreTokenResponse, CoreTokenType, }; let rsa_pem = include_str!("private_key.pem"); // PEM-encoded RSA private key let access_token = AccessToken::new("opaque_access_token".to_string()); let id_token = CoreIdToken::new( CoreIdTokenClaims::new( IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), vec![Audience::new("client-id-123".to_string())], Utc::now() + Duration::seconds(300), // expiry Utc::now(), // issued at StandardClaims::new( SubjectIdentifier::new("5f83e0ca-2b8e-4e8c-ba0a-f80fe9bc3632".to_string()) ) .set_email(Some(EndUserEmail::new("alice@example.com".to_string()))) .set_email_verified(Some(true)), EmptyAdditionalClaims {}, ), &CoreRsaPrivateSigningKey::from_pem(rsa_pem, Some(JsonWebKeyId::new("key1".to_string()))) .expect("Invalid RSA private key"), CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, Some(&access_token), // sets at_hash automatically None, // c_hash (set when returning alongside auth code) ).expect("ID token signing failed"); let token_response = CoreTokenResponse::new( access_token, CoreTokenType::Bearer, CoreIdTokenFields::new(Some(id_token), EmptyExtraTokenFields {}), ); let json = serde_json::to_string(&token_response).unwrap(); // Serve this JSON from your token endpoint ``` --- ## `CoreProviderMetadata::new` — Build Discovery document (Provider side) Constructs a provider metadata document to serve at `GET /.well-known/openid-configuration`. ```rust use openidconnect::{ AuthUrl, EmptyAdditionalProviderMetadata, IssuerUrl, JsonWebKeySetUrl, ResponseTypes, Scope, TokenUrl, UserInfoUrl, }; use openidconnect::core::{ CoreClaimName, CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreResponseType, CoreSubjectIdentifierType, }; let provider_metadata = CoreProviderMetadata::new( IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), AuthUrl::new("https://accounts.example.com/authorize".to_string()).unwrap(), JsonWebKeySetUrl::new("https://accounts.example.com/jwks.json".to_string()).unwrap(), vec![ResponseTypes::new(vec![CoreResponseType::Code])], vec![CoreSubjectIdentifierType::Pairwise], vec![CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256], EmptyAdditionalProviderMetadata {}, ) .set_token_endpoint(Some(TokenUrl::new("https://accounts.example.com/token".to_string()).unwrap())) .set_userinfo_endpoint(Some(UserInfoUrl::new("https://accounts.example.com/userinfo".to_string()).unwrap())) .set_scopes_supported(Some(vec![ Scope::new("openid".to_string()), Scope::new("email".to_string()), Scope::new("profile".to_string()), ])) .set_claims_supported(Some(vec![ CoreClaimName::new("sub".to_string()), CoreClaimName::new("email".to_string()), CoreClaimName::new("email_verified".to_string()), CoreClaimName::new("name".to_string()), CoreClaimName::new("given_name".to_string()), CoreClaimName::new("family_name".to_string()), ])); // Serialize and serve at GET /.well-known/openid-configuration let json = serde_json::to_string(&provider_metadata).unwrap(); ``` --- ## `CoreJsonWebKeySet::new` / `CoreRsaPrivateSigningKey` — JWKS endpoint (Provider side) Constructs the JSON Web Key Set served at the `jwks_uri` to allow clients to verify ID tokens. ```rust use openidconnect::{JsonWebKeyId, PrivateSigningKey}; use openidconnect::core::{CoreJsonWebKeySet, CoreRsaPrivateSigningKey}; let rsa_pem = include_str!("private_key.pem"); let jwks = CoreJsonWebKeySet::new(vec![ CoreRsaPrivateSigningKey::from_pem( rsa_pem, Some(JsonWebKeyId::new("key1".to_string())), ) .expect("Invalid RSA private key") .as_verification_key(), // expose only the public key in JWKS ]); // Serialize and serve at GET /jwks.json let json = serde_json::to_string(&jwks).unwrap(); ``` --- ## `LogoutRequest` — RP-Initiated Logout Builds the end-session URL for RP-Initiated Logout (OpenID Connect RP-Initiated Logout 1.0). ```rust use openidconnect::{ ClientId, CsrfToken, EndSessionUrl, IssuerUrl, LanguageTag, LogoutRequest, PostLogoutRedirectUrl, ProviderMetadataWithLogout, reqwest, }; use openidconnect::types::LogoutHint; // Fetch provider metadata that includes end_session_endpoint let http_client = reqwest::blocking::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build().unwrap(); let provider_metadata: ProviderMetadataWithLogout = ProviderMetadataWithLogout::discover( &IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), &http_client, ).unwrap(); if let Some(end_session_url) = &provider_metadata.additional_metadata().end_session_endpoint { let logout_url = LogoutRequest::from(end_session_url.clone()) // Attach the ID token to help provider identify the session .set_id_token_hint(&id_token) .set_client_id(ClientId::new("client_id".to_string())) .set_post_logout_redirect_uri( PostLogoutRedirectUrl::new("https://myapp.example.com/logged-out".to_string()).unwrap() ) .set_state(CsrfToken::new_random()) .add_ui_locale(LanguageTag::new("en-US".to_string())) .http_get_url(); // Redirect the user's browser to logout_url println!("Logout URL: {}", logout_url); } ``` --- ## `AdditionalProviderMetadata` — Custom provider metadata extensions Extends discovery metadata to capture provider-specific fields (e.g., Google's `revocation_endpoint`). ```rust use openidconnect::{ AdditionalProviderMetadata, IssuerUrl, ProviderMetadata, RevocationUrl, reqwest, }; use openidconnect::core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, }; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] struct GoogleExtras { revocation_endpoint: String, } impl AdditionalProviderMetadata for GoogleExtras {} type GoogleProviderMetadata = ProviderMetadata< GoogleExtras, CoreAuthDisplay, CoreClientAuthMethod, CoreClaimName, CoreClaimType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, >; let http_client = reqwest::blocking::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build().unwrap(); let metadata = GoogleProviderMetadata::discover( &IssuerUrl::new("https://accounts.google.com".to_string()).unwrap(), &http_client, ).unwrap(); let revocation_url = RevocationUrl::new( metadata.additional_metadata().revocation_endpoint.clone() ).unwrap(); println!("Google revocation endpoint: {}", revocation_url.url()); ``` --- ## `Client::exchange_client_credentials` — Client Credentials Flow Obtains a token using the OAuth 2.0 Client Credentials Grant (machine-to-machine, no user). ```rust use openidconnect::{AuthUrl, ClientId, ClientSecret, IssuerUrl, JsonWebKeySet, Scope, TokenUrl}; use openidconnect::core::CoreClient; use openidconnect::OAuth2TokenResponse; let http_client = reqwest::blocking::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build().unwrap(); let client = CoreClient::new( ClientId::new("service-account-id".to_string()), IssuerUrl::new("https://accounts.example.com".to_string()).unwrap(), JsonWebKeySet::default(), ) .set_client_secret(ClientSecret::new("service-account-secret".to_string())) .set_auth_uri(AuthUrl::new("https://accounts.example.com/authorize".to_string()).unwrap()) .set_token_uri(TokenUrl::new("https://accounts.example.com/token".to_string()).unwrap()); let token = client .exchange_client_credentials() .add_scope(Scope::new("api.read".to_string())) .request(&http_client) .expect("Client credentials exchange failed"); println!("Service token: {}", token.access_token().secret()); ``` --- ## Asynchronous API All `.request()` calls have `.request_async()` counterparts for use in `async` contexts with `tokio` or similar runtimes. ```rust use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata, CoreUserInfoClaims}; use openidconnect::{ AccessTokenHash, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, reqwest, }; #[tokio::main] async fn main() -> Result<(), Box> { let http_client = reqwest::ClientBuilder::new() .redirect(reqwest::redirect::Policy::none()) .build()?; let provider_metadata = CoreProviderMetadata::discover_async( IssuerUrl::new("https://accounts.example.com".to_string())?, &http_client, ).await?; let client = CoreClient::from_provider_metadata( provider_metadata, ClientId::new("client_id".to_string()), Some(ClientSecret::new("client_secret".to_string())), ) .set_redirect_uri(RedirectUrl::new("http://localhost:8080/callback".to_string())?); let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); let (auth_url, _csrf_token, nonce) = client .authorize_url( CoreAuthenticationFlow::AuthorizationCode, CsrfToken::new_random, Nonce::new_random, ) .add_scope(Scope::new("email".to_string())) .set_pkce_challenge(pkce_challenge) .url(); println!("Browse to: {}", auth_url); // ... receive code from redirect ... let code = AuthorizationCode::new("received_code".to_string()); let token_response = client .exchange_code(code)? .set_pkce_verifier(pkce_verifier) .request_async(&http_client) // <-- async version .await?; let id_token = token_response.id_token().ok_or("No ID token")?; let verifier = client.id_token_verifier(); let claims = id_token.claims(&verifier, &nonce)?; println!("Authenticated: {}", claims.subject().as_str()); let userinfo: CoreUserInfoClaims = client .user_info(token_response.access_token().to_owned(), None)? .request_async(&http_client) // <-- async version .await?; println!("UserInfo email: {:?}", userinfo.email()); Ok(()) } ``` --- ## Summary `openidconnect-rs` is the definitive Rust crate for building OIDC Relying Parties (clients). The most common integration pattern is: discover provider metadata via `CoreProviderMetadata::discover` or `discover_async`, construct a `CoreClient::from_provider_metadata`, generate a PKCE-protected authorization URL with `authorize_url`, exchange the returned code via `exchange_code`, verify the ID token with `id_token_verifier`, then optionally fetch richer claims from the UserInfo endpoint. The typestate-tracked `Client` struct enforces at compile time that only endpoints already configured can be used, eliminating entire categories of runtime errors. For machine-to-machine scenarios, `exchange_client_credentials` is available; for browserless devices, the Device Authorization Grant (`exchange_device_code` + `exchange_device_access_token`) is fully supported. On the provider side, the crate supplies all the data structures needed to build a compliant OIDC server: `ProviderMetadata` for the discovery document, `CoreJsonWebKeySet` + `CoreRsaPrivateSigningKey` for the JWKS endpoint, and `CoreIdToken::new` for signing ID tokens with RSA, ECDSA, HMAC, or EdDSA keys. Provider metadata is fully extensible via the `AdditionalProviderMetadata` trait, which is demonstrated in the Google example to surface the non-standard `revocation_endpoint` field. The library integrates cleanly with any async runtime or HTTP stack via the `AsyncHttpClient` / `SyncHttpClient` traits, making it suitable for web frameworks (Axum, Actix-Web, Rocket) and CLI tools alike.