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}