axum_conf/fluent/
auth.rs

1//! Authentication middleware: OIDC, Basic Auth, and user span recording.
2
3use super::router::FluentRouter;
4use super::user_span;
5
6#[allow(unused_imports)]
7use crate::{HttpMiddleware, Result};
8
9#[cfg(feature = "keycloak")]
10use {
11    crate::Role,
12    axum_keycloak_auth::{
13        PassthroughMode, Url, decode::ProfileAndEmail, instance::KeycloakAuthInstance,
14        instance::KeycloakConfig, layer::KeycloakAuthLayer,
15    },
16};
17
18#[cfg(feature = "basic-auth")]
19use {super::basic_auth, std::sync::Arc};
20
21impl<State> FluentRouter<State>
22where
23    State: Clone + Send + Sync + 'static,
24{
25    /// Sets up OpenID Connect (OIDC) authentication using Keycloak.
26    ///
27    /// Configures JWT token validation and role-based access control. Requires
28    /// the `keycloak` feature to be enabled.
29    ///
30    /// # Configuration
31    ///
32    /// ```toml
33    /// [http.oidc]
34    /// issuer_url = "https://keycloak.example.com/realms/myrealm"
35    /// realm = "myrealm"
36    /// audiences = ["my-client"]
37    /// client_id = "my-client"
38    /// client_secret = "{{ KEYCLOAK_CLIENT_SECRET }}"
39    /// ```
40    ///
41    /// # Returns
42    ///
43    /// A `Result` containing the configured router or an error if OIDC setup fails.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if:
48    /// - OIDC configuration is invalid
49    /// - Cannot connect to the Keycloak server
50    /// - Client credentials are incorrect
51    #[cfg(feature = "keycloak")]
52    pub fn setup_oidc(mut self) -> Result<Self> {
53        if let Some(oidc) = &self.config.http.oidc
54            && self.is_middleware_enabled(HttpMiddleware::Oidc)
55        {
56            tracing::trace!(
57                realm = %oidc.realm,
58                issuer_url = %oidc.issuer_url,
59                "OIDC middleware enabled"
60            );
61            let keycloak_auth_instance = KeycloakAuthInstance::new(
62                KeycloakConfig::builder()
63                    .server(Url::parse(&oidc.issuer_url)?)
64                    .realm(oidc.realm.clone())
65                    .build(),
66            );
67
68            self.inner = self.inner.route_layer(
69                KeycloakAuthLayer::<Role, ProfileAndEmail>::builder()
70                    .instance(keycloak_auth_instance)
71                    .passthrough_mode(PassthroughMode::Block)
72                    .expected_audiences(oidc.audiences.clone())
73                    .persist_raw_claims(true)
74                    .build(),
75            );
76        }
77        Ok(self)
78    }
79
80    /// Sets up HTTP Basic Auth and/or API Key authentication.
81    ///
82    /// When configured, protects all routes (except health endpoints) with authentication.
83    /// Supports HTTP Basic Auth (RFC 7617), API Key authentication, or both.
84    ///
85    /// # Configuration
86    ///
87    /// ```toml
88    /// [http.basic_auth]
89    /// mode = "either"  # "basic", "api_key", or "either"
90    /// api_key_header = "X-API-Key"
91    ///
92    /// [[http.basic_auth.users]]
93    /// username = "admin"
94    /// password = "{{ ADMIN_PASSWORD }}"
95    ///
96    /// [[http.basic_auth.api_keys]]
97    /// key = "{{ API_KEY }}"
98    /// name = "service-a"
99    /// ```
100    ///
101    /// # Extracting Identity in Handlers
102    ///
103    /// ```rust,ignore
104    /// use axum::Extension;
105    /// use axum_conf::AuthenticatedIdentity;
106    ///
107    /// async fn handler(Extension(identity): Extension<AuthenticatedIdentity>) -> String {
108    ///     format!("Hello, {}!", identity.name)
109    /// }
110    /// ```
111    #[cfg(feature = "basic-auth")]
112    pub fn setup_basic_auth(mut self) -> Result<Self> {
113        if let Some(basic_auth_config) = &self.config.http.basic_auth
114            && self.is_middleware_enabled(HttpMiddleware::BasicAuth)
115        {
116            tracing::trace!(
117                mode = ?basic_auth_config.mode,
118                "BasicAuth middleware enabled"
119            );
120            let config = Arc::new(basic_auth_config.clone());
121
122            self.inner = self
123                .inner
124                .route_layer(axum::middleware::from_fn(move |request, next| {
125                    let config = Arc::clone(&config);
126                    basic_auth::basic_auth_middleware(config, request, next)
127                }));
128        }
129        Ok(self)
130    }
131
132    /// Records the authenticated username to the logging span.
133    ///
134    /// This middleware runs after authentication and records the username
135    /// to the `user` field of the current tracing span. This ensures all
136    /// subsequent logs within the request include the authenticated user.
137    ///
138    /// Works with both Basic Auth (`AuthenticatedIdentity`) and OIDC
139    /// (`KeycloakToken`) authentication methods.
140    ///
141    /// For unauthenticated requests (e.g., health endpoints), the `user`
142    /// field remains empty and won't appear in log output.
143    #[must_use]
144    pub fn setup_user_span(mut self) -> Self {
145        tracing::trace!("UserSpan middleware enabled");
146        self.inner = self
147            .inner
148            .layer(axum::middleware::from_fn(user_span::record_user_to_span));
149        self
150    }
151}