axum_keycloak_auth/
lib.rs

1//! Protect axum routes with a JWT emitted by Keycloak.
2//!
3//! # Usage
4//!
5//! This library provides the `KeycloakAuthInstance` which manages OIDC discovery and hold onto decoding keys
6//! and the `KeycloakAuthLayer`, a tower layer / service implementation that parses and validates incoming JWTs.
7//!
8//! Let's set up a protected Axum route!
9//!
10//! To demonstrate the likely case of still requiring some (e.g. /health) public routes,
11//! let us define two functions to create the respective public and protected routers,
12//! adding a `KeycloakAuthLayer` only to the router whose routes should be protected.
13//!
14//! Specifying the `required_roles` is optional. If omitted, role-presence can be checked in each route-handler individually.
15//! The library will then only check that a request was performed with a valid JWT.
16//! Consider using this builder field if you have a long list of route-handlers
17//! which all require the same roles to be present.
18//!
19//! ```rust
20//! use std::sync::Arc;
21//! use axum::{http::StatusCode, response::{Response, IntoResponse}, routing::get, Extension, Router};
22//! use axum_keycloak_auth::{Url, error::AuthError, instance::KeycloakConfig, instance::KeycloakAuthInstance, layer::KeycloakAuthLayer, decode::KeycloakToken, PassthroughMode, expect_role};
23//!
24//! pub fn public_router() -> Router {
25//!     Router::new()
26//!         .route("/health", get(health))
27//! }
28//!
29//! pub fn protected_router(instance: KeycloakAuthInstance) -> Router {
30//!     Router::new()
31//!         .route("/protected", get(protected))
32//!         .layer(
33//!              KeycloakAuthLayer::<String>::builder()
34//!                  .instance(instance)
35//!                  .passthrough_mode(PassthroughMode::Block)
36//!                  .persist_raw_claims(false)
37//!                  .expected_audiences(vec![String::from("account")])
38//!                  .required_roles(vec![String::from("administrator")])
39//!                  .build(),
40//!         )
41//! }
42//!
43//! // You may have multiple routers that you want to see protected by a `KeycloakAuthLayer`.
44//! // You can safely attach new `KeycloakAuthLayer`s to different routers, but consider using only a single `KeycloakAuthInstance` for all of these layers.
45//! // Remember: The `KeycloakAuthInstance` manages the keys used to decode incoming JWTs and dynamically fetches them from your Keycloak server.
46//! // Having multiple instances simoultaniously would incease pressure on your Keycloak instance on service startup and unnecesssarily store duplicated data.
47//! // The `KeycloakAuthLayer` therefore really takes an `Arc<KeycloakAuthInstance>` in its `instance` method!
48//! // Presence of the `Into` trait in the `instance` methods argument let us hide that fact in the previous example.
49//!
50//! #[allow(dead_code)]
51//! pub fn protect(router:Router, instance: Arc<KeycloakAuthInstance>) -> Router {
52//!     router.layer(
53//!         KeycloakAuthLayer::<String>::builder()
54//!             .instance(instance)
55//!             .passthrough_mode(PassthroughMode::Block)
56//!             .persist_raw_claims(false)
57//!             .expected_audiences(vec![String::from("account")])
58//!             .required_roles(vec![String::from("administrator")])
59//!             .build(),
60//!     )
61//! }
62//!
63//! // Lets also define the handlers ('health' and 'protected') defined in our routers.
64//!
65//! // The `health` handler can always be called without a JWT,
66//! // as we only attached an instance of the `KeycloakAuthLayer` to the protected router.
67//!
68//! // The `KeycloakAuthLayer` makes the parsed token data available using axum's `Extension`'s,
69//! // including the users roles, the uuid of the user, its name, email, ...
70//! // The `protected` handler will (in the default `PassthroughMode::Block` case) only be called
71//! // if the request contained a valid JWT which not already expired.
72//! // It may then access that data (as `KeycloakToken<YourRoleType>`) through an Extension
73//! // to get access to the decoded keycloak user information as shown below.
74//!
75//! pub async fn health() -> impl IntoResponse {
76//!     StatusCode::OK
77//! }
78//!
79//! pub async fn protected(Extension(token): Extension<KeycloakToken<String>>) -> Response {
80//!     expect_role!(&token, "administrator");
81//!
82//!     tracing::info!("Token payload is {token:#?}");
83//!
84//!     (
85//!         StatusCode::OK,
86//!         format!(
87//!             "Hello {name} ({subject}). Your token is valid for another {valid_for} seconds.",
88//!             name = token.extra.profile.preferred_username,
89//!             subject = token.subject,
90//!             valid_for = (token.expires_at - time::OffsetDateTime::now_utc()).whole_seconds()
91//!         ),
92//!     ).into_response()
93//! }
94//!
95//! // You can construct a `KeycloakAuthInstance` using a single value of type `KeycloakConfig`, which is constructed using the builder pattern.
96//! // You may want to immediately wrap it inside an `Arc` if you intend to pass it to multiple `KeycloakAuthLayer`s. We are not doing this in this example.
97//!
98//! // Your final router can be created by merging the public and protected routers.
99//!
100//! #[tokio::main]
101//! async fn main() {
102//!     let keycloak_auth_instance = KeycloakAuthInstance::new(
103//!         KeycloakConfig::builder()
104//!             .server(Url::parse("https://localhost:8443/").unwrap())
105//!             .realm(String::from("MyRealm"))
106//!             .build(),
107//!     );
108//!     let router = public_router().merge(protected_router(keycloak_auth_instance));
109//!
110//!     // let addr_and_port = String::from("0.0.0.0:8080");
111//!     // let socket_addr: std::net::SocketAddr = addr_and_port.parse().unwrap();
112//!     // println!("Listening on: {}", addr_and_port);
113//!
114//!     // let tcp_listener = tokio::net::TcpListener::bind(socket_addr).await.unwrap();
115//!     // axum::serve(tcp_listener, router.into_make_service()).await.unwrap();
116//! }
117//! ```
118//!
119//! # Using a custom role type
120//!
121//! You probably noticed a generic `<String>` when creating the `KeycloakAuthLayer` and defining the handler extension.
122//!
123//! This is the type representing a single role and can be replaced with any type implementing the `axum_keycloak_auth::role::Role` trait.
124//!
125//! You could for example create an enum containing all your known roles as variants with a special variant for unknown role names.
126//!
127//! ```rust
128//! #[derive(Debug, PartialEq, Eq, Clone)]
129//! pub enum Role {
130//!     Administrator,
131//!     Unknown(String),
132//! }
133//!
134//! impl axum_keycloak_auth::role::Role for Role {}
135//!
136//! impl std::fmt::Display for Role {
137//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138//!         match self {
139//!             Role::Administrator => f.write_str("Administrator"),
140//!             Role::Unknown(unknown) => f.write_fmt(format_args!("Unknown role: {unknown}")),
141//!         }
142//!     }
143//! }
144//!
145//! impl From<String> for Role {
146//!     fn from(value: String) -> Self {
147//!         match value.as_ref() {
148//!             "administrator" => Role::Administrator,
149//!             _ => Role::Unknown(value),
150//!         }
151//!     }
152//! }
153//!
154//! // You could then (remember to update both locations of the generic type) check for roles using your enum:
155//!
156//! use axum::{http::StatusCode, response::{Response, IntoResponse}, Extension};
157//! use axum_keycloak_auth::{decode::KeycloakToken, expect_role};
158//!
159//! pub async fn protected(Extension(token): Extension<KeycloakToken<Role>>) -> Response {
160//!     expect_role!(&token, Role::Administrator);
161//!     StatusCode::OK.into_response()
162//! }
163//! ```
164//!
165//! # Passthrough modes
166//!
167//! The `KeycloakAuthLayer` provides a `passthrough_mode` field, allowing you to choose between the following modes:
168//!
169//! - `PassthroughMode::Block`: Immediately return an error-response should authentication fail. This is the preferred mode and the default if omitted.
170//! - `PassthroughMode::Pass`: Always store a `KeycloakAuthStatus` containing the authentication result and defer the response generation to the handler or any deeper layers. You may want to use this mode i fine-grained error handling is required or you want to use additional layers which could still prove the user authenticated.
171//!
172//! # Using custom token extractors
173//!
174//! By default, request headers are checked for presence of an "authorization" header,
175//! which is expected to contain the typical "`Bearer <token>`" string.
176//!
177//! You have the ability to change this behavior to your liking through use of the `TokenExtractor` trait,
178//! which allows for customized strategies on how to retrieve the token from an axum request.
179//!
180//! The `token_extractors` field on the `KeycloakAuthLayer` builder accepts a non-empty vec of extractors.
181//!
182//! ```rust,no_run
183//! use std::sync::Arc;
184//! use axum_keycloak_auth::{
185//!     NonEmpty, PassthroughMode,
186//!     instance::KeycloakAuthInstance,
187//!     layer::KeycloakAuthLayer,
188//!     extract::{AuthHeaderTokenExtractor, QueryParamTokenExtractor, TokenExtractor}
189//! };
190//!
191//! let instance: KeycloakAuthInstance = todo!();
192//!
193//! let layer = KeycloakAuthLayer::<String>::builder()
194//!     .instance(instance)
195//!     .passthrough_mode(PassthroughMode::Block)
196//!     .expected_audiences(vec![String::from("account")])
197//!     // ...
198//!     .token_extractors(NonEmpty::<Arc<dyn TokenExtractor>> {
199//!         head: Arc::new(AuthHeaderTokenExtractor::default()),
200//!         tail: vec![
201//!             Arc::new(QueryParamTokenExtractor::default()),
202//!             Arc::new(QueryParamTokenExtractor::extracting_key("jwt")),
203//!         ],
204//!     })
205//!     .build();
206//! ```
207//!
208//! Extractors are called in order of their definition in the `token_extractors` vec.
209//! The token from the first extractor able to successfully extract one is used to further validate the request.
210//! Other extractors are no longer considered.
211//!
212//! This crate implements two extraction strategies:
213//!   - `AuthHeaderTokenExtractor`: Extracts the token from the `http::header::AUTHORIZATION` header.
214//!   - `QueryParamTokenExtractor`: Extracts the token from a query parameter (by default named "token"). Use with caution!
215//!
216//! By default, when not explicitly setting `token_extractors`, a single `AuthHeaderTokenExtractor::default()` is used.
217//!
218
219#![forbid(unsafe_code)]
220//#![warn(missing_docs)]
221#![deny(clippy::unwrap_used)]
222
223use std::sync::Arc;
224
225use role::Role;
226
227mod action;
228pub mod decode;
229pub mod error;
230pub mod extract;
231pub mod instance;
232pub mod layer;
233pub mod oidc;
234pub mod oidc_discovery;
235pub mod role;
236pub mod service;
237
238// Re-export the Url struct used when configuring a `KeycloakAuthInstance`.
239pub use url::Url;
240
241// Re-export the NonEmpty struct used when configuring a `KeycloakAuthLayer`.
242pub use nonempty::NonEmpty;
243
244use serde::de::DeserializeOwned;
245
246/// The mode in which the authentication middleware may operate in.
247///
248/// ```PassthroughMode::Block```: Immediately return a `Response` if authentication failed.
249/// On successful authentication, the parsed token content is stored as an axum extension as a `KeycloakToken`.
250///
251/// ```PassthroughMode::Pass```:  Forward to the response handler regardless of whether there was an authentication failure.
252/// In this mode, the authentication status is stored as an axum extension as a `KeycloakAuthStatus`.
253#[derive(Debug, PartialEq, Eq, Clone, Copy)]
254pub enum PassthroughMode {
255    Block,
256    Pass,
257}
258
259#[derive(Debug, Clone)]
260#[allow(clippy::large_enum_variant)]
261pub enum KeycloakAuthStatus<R, Extra>
262where
263    R: Role,
264    Extra: DeserializeOwned + Clone,
265{
266    // This variant is fairly large, but probably used most of the time. Leaving this non-boxed results in one less allocation each request.
267    Success(decode::KeycloakToken<R, Extra>),
268    Failure(Arc<error::AuthError>),
269}