### Custom Role Type Example Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Demonstrates how to define a custom enum for type-safe role-based access control. The custom role type must implement `Role` and `From`. ```APIDOC ## Custom role type — type-safe role-based access control Replace `String` with a custom enum that implements `Role` (requires `Debug + Display + Clone + PartialEq + Eq + Send + Sync + From`) to gain compile-time role safety. All role strings from the JWT are converted via `From` at decode time. ```rust use std::fmt; use axum::{Extension, response::{IntoResponse, Response}}; use axum_keycloak_auth::{decode::KeycloakToken, expect_role}; use http::StatusCode; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AppRole { Admin, Editor, Viewer, Unknown(String), } impl fmt::Display for AppRole { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppRole::Admin => write!(f, "admin"), AppRole::Editor => write!(f, "editor"), AppRole::Viewer => write!(f, "viewer"), AppRole::Unknown(s) => write!(f, "{s}"), } } } impl axum_keycloak_auth::role::Role for AppRole {} impl From for AppRole { fn from(s: String) -> Self { match s.as_str() { "admin" => AppRole::Admin, "editor" => AppRole::Editor, "viewer" => AppRole::Viewer, _ => AppRole::Unknown(s), } } } // Handler using the custom role type: pub async fn admin_only(Extension(token): Extension>) -> Response { // Macro returns a 403 response immediately if the role is absent. expect_role!(&token, AppRole::Admin); (StatusCode::OK, "Welcome, admin!").into_response() } // Layer must use the same generic: KeycloakAuthLayer::::builder()... ``` ``` -------------------------------- ### Configure Token Extractors for JWT Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Customize how the JWT is located in requests by providing a NonEmpty list of TokenExtractor implementations. Extractors are tried in order, and the first success is used. This example shows using the standard Authorization header with fallbacks to query parameters. ```rust use std::sync::Arc; use axum_keycloak_auth:: NonEmpty, PassthroughMode, extract::{AuthHeaderTokenExtractor, QueryParamTokenExtractor, TokenExtractor}, instance::{KeycloakAuthInstance, KeycloakConfig}, layer::KeycloakAuthLayer, Url, ; let instance = KeycloakAuthInstance::new( KeycloakConfig::builder() .server(Url::parse("https://keycloak.example.com/").unwrap()) .realm("my-realm".into()) .build(), ); let layer = KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Block) .expected_audiences(vec!["account".into()]) .token_extractors(NonEmpty::> { // Primary: standard Authorization header head: Arc::new(AuthHeaderTokenExtractor::default()), tail: vec![ // Fallback 1: ?token= Arc::new(QueryParamTokenExtractor::default()), // Fallback 2: ?jwt= Arc::new(QueryParamTokenExtractor::extracting_key("jwt")), ], }) .build(); // Custom extractor: implement TokenExtractor for your own type. // pub trait TokenExtractor: Send + Sync + Debug { // fn extract<'a>(&self, request: &'a Request) -> Result, AuthError>; // } ``` -------------------------------- ### Create and Manage KeycloakAuthInstance Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Instantiate KeycloakAuthInstance with minimal configuration including server URL and realm. Discovery happens in the background. Use Arc to share across layers. ```rust use std::sync::Arc; use axum_keycloak_auth:: Url, instance::{KeycloakAuthInstance, KeycloakConfig}, }; #[tokio::main] async fn main() { // Minimal config: server base URL + realm name. let config = KeycloakConfig::builder() .server(Url::parse("https://keycloak.example.com/").unwrap()) .realm(String::from("my-realm")) // Optional: override retry strategy (max_attempts, delay_secs). Default: (5, 1). .retry((10, 2)) .build(); // Discovery begins immediately in the background. let instance = Arc::new(KeycloakAuthInstance::new(config)); // Poll operational status — useful in a /health endpoint. if instance.is_operational().await { println!("OIDC discovery succeeded, ready to validate JWTs."); } else { eprintln!("Still waiting for OIDC discovery…"); } } ``` -------------------------------- ### Axum Full Application with Keycloak Auth Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Sets up an Axum application with both public and protected routes. Requires `axum-keycloak-auth` and `tokio`. The `KeycloakAuthLayer` is configured with a shared `KeycloakAuthInstance` and specific role/audience requirements for protected routes. ```rust use std::sync::Arc; use axum::{ Extension, Json, Router, response::{IntoResponse, Response}, routing::get, }; use axum_keycloak_auth::{ Url, PassthroughMode, decode::KeycloakToken, instance::{KeycloakAuthInstance, KeycloakConfig}, layer::KeycloakAuthLayer, }; use http::StatusCode; use serde::Serialize; #[derive(Debug, Clone, PartialEq, Eq)] enum Role { Admin, Unknown(String) } impl std::fmt::Display for Role { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Role::Admin => write!(f, "admin"), Role::Unknown(s) => write!(f, "{s}"), } } } impl axum_keycloak_auth::role::Role for Role {} impl From for Role { fn from(s: String) -> Self { match s.as_str() { "admin" => Role::Admin, _ => Role::Unknown(s) } } } async fn health() -> impl IntoResponse { StatusCode::OK } async fn protected(Extension(token): Extension>) -> Response { expect_role!(&token, Role::Admin); #[derive(Serialize)] struct Info { user: String, valid_for: i64 } (StatusCode::OK, Json(Info { user: token.extra.profile.preferred_username.clone(), valid_for: (token.expires_at - time::OffsetDateTime::now_utc()).whole_seconds(), })).into_response() } #[tokio::main] async fn main() { let instance = Arc::new(KeycloakAuthInstance::new( KeycloakConfig::builder() .server(Url::parse("https://keycloak.example.com/").unwrap()) .realm("production".into()) .retry((5, 1)) .build(), )); let app = Router::new() .route("/health", get(health)) .merge( Router::new() .route("/api/protected", get(protected)) .layer( KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Block) .persist_raw_claims(false) .expected_audiences(vec!["account".into()]) .required_roles(vec![Role::Admin]) .build(), ) ); let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); axum::serve(listener, app).await.unwrap(); } ``` -------------------------------- ### Role Assertion Macros Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Provides four macros for early-return role enforcement within handlers, simplifying role checks and returning a 403 Forbidden response on failure. ```APIDOC ## Role assertion macros — in-handler role checks Four macros provide early-return role enforcement inside handlers without manual match boilerplate. They call `ExpectRoles::expect_roles` / `not_expect_roles` and return a `403 Forbidden` response on failure. ```rust use axum::{Extension, response::{IntoResponse, Response}}; use axum_keycloak_auth::{ decode::KeycloakToken, expect_role, expect_roles, not_expect_role, not_expect_roles, }; use http::StatusCode; pub async fn sensitive_handler(Extension(token): Extension>) -> Response { // Require a single role: expect_role!(&token, "admin"); // Require multiple roles (all must be present): expect_roles!(&token, &["admin", "auditor"]); // Forbid a single role (returns 403 if present): not_expect_role!(&token, "banned"); // Forbid multiple roles (returns 403 if any are present): not_expect_roles!(&token, &["suspended", "banned"]); (StatusCode::OK, "Access granted").into_response() } ``` ``` -------------------------------- ### Protect Axum Route with Keycloak JWT Authentication Source: https://github.com/lpotthast/axum-keycloak-auth/blob/main/README.md This snippet demonstrates how to set up a protected Axum route using `KeycloakAuthLayer`. It requires a custom `Role` enum for parsing roles from the JWT and configures the authentication layer with an instance and passthrough mode. The `protected` handler extracts and uses the validated `KeycloakToken`. ```rust enum Role { Administrator, Unknown(String), } pub fn protected_router(instance: KeycloakAuthInstance) -> Router { Router::new() .route("/protected", get(protected)) .layer( KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Block) .build(), ) } pub async fn protected(Extension(token): Extension>) -> Response { expect_role!(&token, Role::Administrator); info!("Token payload is {token:#?}"); ( StatusCode::OK, format!( "Hello {name} ({subject}). Your token is valid for another {valid_for} seconds.", name = token.extra.profile.preferred_username, subject = token.subject, valid_for = (token.expires_at - time::OffsetDateTime::now_utc()).whole_seconds() ), ).into_response() } ``` -------------------------------- ### Configure PassthroughMode for Authentication Failures Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Use `PassthroughMode::Pass` to always call the handler, delivering the authentication result as a `KeycloakAuthStatus` extension. This allows the handler to decide how to respond to authentication failures. ```rust use axum::{Extension, Router, routing::get, response::{IntoResponse, Response}}; use axum_keycloak_auth::{ KeycloakAuthStatus, PassthroughMode, decode::{KeycloakToken, ProfileAndEmail}, instance::{KeycloakAuthInstance, KeycloakConfig}, layer::KeycloakAuthLayer, Url, }; use http::StatusCode; use std::sync::Arc; // Handler for a Pass-mode route: inspect KeycloakAuthStatus manually. async fn am_i_authenticated( Extension(status): Extension>, ) -> Response { match status { KeycloakAuthStatus::Success(token) => ( StatusCode::OK, format!("Hello, {}!", token.extra.profile.preferred_username), ).into_response(), KeycloakAuthStatus::Failure(err) => ( StatusCode::OK, format!("Not authenticated: {err}"), ).into_response(), } } fn pass_router(instance: Arc) -> Router { Router::new() .route("/am-i-authenticated", get(am_i_authenticated)) .layer( KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Pass) // <-- never blocks; handler always runs .expected_audiences(vec!["account".into()]) .build(), ) } ``` -------------------------------- ### Use Role Assertion Macros for In-Handler Checks Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Employ macros like `expect_role!`, `expect_roles!`, `not_expect_role!`, and `not_expect_roles!` for concise, early-return role enforcement within handlers. These macros return a `403 Forbidden` response on failure, reducing boilerplate match statements. ```rust use axum::{Extension, response::{IntoResponse, Response}}; use axum_keycloak_auth::{ decode::KeycloakToken, expect_role, expect_roles, not_expect_role, not_expect_roles, }; use http::StatusCode; pub async fn sensitive_handler(Extension(token): Extension>) -> Response { // Require a single role: expect_role!(&token, "admin"); // Require multiple roles (all must be present): expect_roles!(&token, &["admin", "auditor"]); // Forbid a single role (returns 403 if present): not_expect_role!(&token, "banned"); // Forbid multiple roles (returns 403 if any are present): not_expect_roles!(&token, &["suspended", "banned"]); (StatusCode::OK, "Access granted").into_response() } ``` -------------------------------- ### Attach KeycloakAuthLayer to Axum Router Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Add KeycloakAuthLayer to protect routes. Configure passthrough mode, audience, and required roles. The generic parameter R defines the role type. ```rust use std::sync::Arc; use axum::{Router, routing::get}; use axum_keycloak_auth:: PassthroughMode, instance::{KeycloakAuthInstance, KeycloakConfig}, layer::KeycloakAuthLayer, Url, }; pub fn build_router(instance: Arc) -> Router { // Public routes — no layer applied. let public = Router::new().route("/health", get(|| async { "OK" })); // Protected routes — layer blocks requests without a valid JWT. let protected = Router::new() .route("/api/data", get(data_handler)) .layer( KeycloakAuthLayer::::builder() .instance(instance) // Arc or KeycloakAuthInstance (auto-wrapped) .passthrough_mode(PassthroughMode::Block) // Return 401/403 immediately on failure .persist_raw_claims(false) // Set true to expose HashMap extension .expected_audiences(vec![String::from("account")]) // Must match JWT "aud" claim .required_roles(vec![String::from("data-reader")]) // Every request must carry this realm/client role .build(), ); public.merge(protected) } async fn data_handler() -> &'static str { "secret data" } ``` -------------------------------- ### Define Custom Extra Type for JWT Claims Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Replace the default ProfileAndEmail with a custom struct that implements serde::Deserialize + Clone to access application-specific claims from the JWT. This allows reading custom fields without using a raw HashMap. ```rust use serde::Deserialize; use axum::{ Extension, response::{IntoResponse, Response} }; use axum_keycloak_auth:: decode::KeycloakToken, layer::KeycloakAuthLayer, PassthroughMode, ; use http::StatusCode; // Custom claims expected in every token issued by this Keycloak client. #[derive(Debug, Clone, Deserialize)] pub struct MyAppClaims { pub tenant_id: String, pub subscription_tier: String, // Flatten standard profile fields if still needed: #[serde(flatten)] pub profile: axum_keycloak_auth::decode::ProfileAndEmail, } // Handler with custom Extra type: pub async fn tenant_handler( Extension(token): Extension>, ) -> Response { ( StatusCode::OK, format!( "tenant={{}} tier={{}} user={{}}", token.extra.tenant_id, token.extra.subscription_tier, token.extra.profile.profile.preferred_username, ), ).into_response() } // Layer must also carry the same Extra generic: // KeycloakAuthLayer::::builder()... ``` -------------------------------- ### Define Custom Role Type for Type-Safe RBAC Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Implement a custom enum that derives `Role` for compile-time safety. All role strings from the JWT are converted via `From` at decode time. Ensure the enum implements `Debug + Display + Clone + PartialEq + Eq + Send + Sync + From`. ```rust use std::fmt; use axum::{Extension, response::{IntoResponse, Response}}; use axum_keycloak_auth::{decode::KeycloakToken, expect_role}; use http::StatusCode; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AppRole { Admin, Editor, Viewer, Unknown(String), } impl fmt::Display for AppRole { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppRole::Admin => write!(f, "admin"), AppRole::Editor => write!(f, "editor"), AppRole::Viewer => write!(f, "viewer"), AppRole::Unknown(s) => write!(f, "{s}"), } } } impl axum_keycloak_auth::role::Role for AppRole {} impl From for AppRole { fn from(s: String) -> Self { match s.as_str() { "admin" => AppRole::Admin, "editor" => AppRole::Editor, "viewer" => AppRole::Viewer, _ => AppRole::Unknown(s), } } } // Handler using the custom role type: pub async fn admin_only(Extension(token): Extension>) -> Response { // Macro returns a 403 response immediately if the role is absent. expect_role!(&token, AppRole::Admin); (StatusCode::OK, "Welcome, admin!").into_response() } // Layer must use the same generic: KeycloakAuthLayer::::builder()... ``` -------------------------------- ### AuthError Status Code Mapping Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt AuthError implements IntoResponse, converting variants into JSON error bodies with appropriate HTTP status codes. Role names are redacted in release builds. ```rust // Status code mapping: // // 500 Internal Server Error: // AuthError::NoOidcDiscovery — discovery never ran // AuthError::OidcDiscovery — discovery HTTP request failed // AuthError::NoJwkSetDiscovery — JWK set never fetched // AuthError::JwkEndpoint — JWK URI could not be parsed // AuthError::JwkSetDiscovery — JWK set HTTP request failed // AuthError::CreateDecodingKey — key could not be constructed // AuthError::JsonParse — JWT body failed JSON deserialization // // 400 Bad Request: // AuthError::DecodeHeader — JWT header could not be decoded // // 401 Unauthorized: // AuthError::MissingAuthorizationHeader // AuthError::InvalidAuthorizationHeader // AuthError::MissingBearerToken // AuthError::MissingQueryParams // AuthError::MissingTokenQueryParam // AuthError::EmptyTokenQueryParam // AuthError::MissingToken // AuthError::NoDecodingKeys // AuthError::Decode // AuthError::TokenExpired // AuthError::InvalidToken // // 403 Forbidden: // AuthError::MissingExpectedRole — role name hidden in release builds // AuthError::UnexpectedRole // Example JSON responses: // {"error": "The 'Authorization' header was not present on a request."} // {"error": "The tokens lifetime is expired."} // {"error": "Missing expected role"} ← release build (no role name) // {"error": "Missing expected role: admin"} ← debug build ``` -------------------------------- ### Access Decoded JWT Data in Handlers Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt In `Block` mode, the middleware inserts a `KeycloakToken` extension. Handlers can extract this token to access decoded JWT data such as username, email, UUID, roles, and expiration time. ```rust use axum::{Extension, Json, response::{IntoResponse, Response}}; use axum_keycloak_auth::decode::KeycloakToken; use http::StatusCode; use serde::Serialize; #[derive(Serialize)] struct WhoAmIResponse { username: String, email: String, keycloak_uuid: String, roles: Vec, token_valid_for_seconds: i64, } pub async fn who_am_i(Extension(token): Extension>) -> Response { ( StatusCode::OK, Json(WhoAmIResponse { username: token.extra.profile.preferred_username.clone(), email: token.extra.email.email.clone(), keycloak_uuid: token.subject.clone(), // Keycloak user UUID roles: token.roles.iter().map(|r| r.role().clone()).collect(), token_valid_for_seconds: (token.expires_at - time::OffsetDateTime::now_utc()) .whole_seconds(), }), ).into_response() } // Fields available on KeycloakToken: // token.expires_at — time::OffsetDateTime // token.issued_at — time::OffsetDateTime // token.jwt_id — String (jti) // token.issuer — String (iss) // token.audience — Vec (aud) // token.subject — String (sub) — Keycloak user UUID // token.authorized_party — String (azp) // token.roles — Vec> (realm + client roles) // token.extra — Extra (default: ProfileAndEmail) ``` -------------------------------- ### KeycloakAuthLayer::validate_raw_token Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Enables out-of-band JWT validation using `validate_raw_token`, suitable for scenarios like WebSocket upgrades or background job authorization, reusing the same instance and configuration. ```APIDOC ## KeycloakAuthLayer::validate_raw_token — out-of-band token validation Use `validate_raw_token` to validate a JWT string outside the middleware pipeline — e.g., for WebSocket upgrade handshakes or background job authorization — reusing the same instance and configuration. ```rust use axum_keycloak_auth::{ layer::KeycloakAuthLayer, instance::{KeycloakAuthInstance, KeycloakConfig}, PassthroughMode, Url, }; async fn validate_ws_token(raw_jwt: &str) { let instance = KeycloakAuthInstance::new( KeycloakConfig::builder() .server(Url::parse("https://keycloak.example.com/").unwrap()) .realm("my-realm".into()) .build(), ); let layer = KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Block) .expected_audiences(vec!["account".into()]) .build(); match layer.validate_raw_token(raw_jwt).await { Ok((_raw_claims_opt, token)) => { println!("Valid token for user: {}", token.extra.profile.preferred_username); println!("Roles: {:?}", token.roles); } Err(err) => { eprintln!("Token rejected: {err}"); } } } // raw_jwt is the bare token string — no "Bearer " prefix. // Returns (Option, KeycloakToken) on success. ``` ``` -------------------------------- ### Validate Raw JWT Token Out-of-Band Source: https://context7.com/lpotthast/axum-keycloak-auth/llms.txt Utilize `validate_raw_token` to verify JWT strings outside the standard middleware pipeline, suitable for scenarios like WebSocket upgrades or background job authorization. This method reuses the same `KeycloakAuthInstance` and configuration. ```rust use axum_keycloak_auth::{ layer::KeycloakAuthLayer, instance::{KeycloakAuthInstance, KeycloakConfig}, PassthroughMode, Url, }; async fn validate_ws_token(raw_jwt: &str) { let instance = KeycloakAuthInstance::new( KeycloakConfig::builder() .server(Url::parse("https://keycloak.example.com/").unwrap()) .realm("my-realm".into()) .build(), ); let layer = KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Block) .expected_audiences(vec!["account".into()]) .build(); match layer.validate_raw_token(raw_jwt).await { Ok((_raw_claims_opt, token)) => { println!("Valid token for user: {}", token.extra.profile.preferred_username); println!("Roles: {:?}", token.roles); } Err(err) => { eprintln!("Token rejected: {err}"); } } } // raw_jwt is the bare token string — no "Bearer " prefix. // Returns (Option, KeycloakToken) on success. ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.