kanidmd_core/https/extractors/
mod.rs

1use crate::https::ServerState;
2use axum::{
3    async_trait,
4    extract::{connect_info::Connected, FromRequestParts},
5    http::{header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode},
6};
7use axum_extra::extract::cookie::CookieJar;
8use compact_jwt::JwsCompact;
9use kanidm_proto::internal::COOKIE_BEARER_TOKEN;
10use kanidmd_lib::prelude::{ClientAuthInfo, ClientCertInfo, Source};
11use std::net::{IpAddr, SocketAddr};
12use std::str::FromStr;
13
14// Re-export
15pub use kanidmd_lib::idm::server::DomainInfoRead;
16
17pub struct VerifiedClientInformation(pub ClientAuthInfo);
18
19#[async_trait]
20impl FromRequestParts<ServerState> for VerifiedClientInformation {
21    type Rejection = (StatusCode, &'static str);
22
23    // Need to skip all to prevent leaking tokens to logs.
24    #[instrument(level = "debug", skip_all)]
25    async fn from_request_parts(
26        parts: &mut Parts,
27        state: &ServerState,
28    ) -> Result<Self, Self::Rejection> {
29        let ClientConnInfo {
30            connection_addr: _,
31            client_ip_addr,
32            client_cert,
33        } = parts.extensions.remove::<ClientConnInfo>().ok_or((
34            StatusCode::INTERNAL_SERVER_ERROR,
35            "request info contains invalid data",
36        ))?;
37
38        let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
39            if let Some((authz_type, authz_data)) = header
40                .to_str()
41                .map_err(|err| {
42                    warn!(?err, "Invalid authz header, ignoring");
43                })
44                .ok()
45                .and_then(|s| s.split_once(' '))
46            {
47                let authz_type = authz_type.to_lowercase();
48
49                if authz_type == "basic" {
50                    (Some(authz_data.to_string()), None)
51                } else if authz_type == "bearer" {
52                    if let Ok(jwsc) = JwsCompact::from_str(authz_data) {
53                        (None, Some(jwsc))
54                    } else {
55                        warn!("bearer jws invalid");
56                        (None, None)
57                    }
58                } else {
59                    warn!("authorisation header invalid, ignoring");
60                    (None, None)
61                }
62            } else {
63                (None, None)
64            }
65        } else {
66            // Only if there are no credentials in bearer, do we examine cookies.
67            let jar = CookieJar::from_headers(&parts.headers);
68
69            let value: Option<&str> = jar.get(COOKIE_BEARER_TOKEN).map(|c| c.value());
70
71            let maybe_bearer = value.and_then(|authz_data| JwsCompact::from_str(authz_data).ok());
72
73            (None, maybe_bearer)
74        };
75
76        let mut client_auth_info = ClientAuthInfo::new(
77            Source::Https(client_ip_addr),
78            client_cert,
79            bearer_token,
80            basic_authz,
81        );
82
83        // now, we want to update the client auth info with the sessions user-auth-token
84        // if any. We ignore errors here as the auth info MAY NOT be a valid token
85        // and so in that case no prevalidation will occur.
86        let _ = state
87            .qe_r_ref
88            .pre_validate_client_auth_info(&mut client_auth_info)
89            .await;
90
91        Ok(VerifiedClientInformation(client_auth_info))
92    }
93}
94
95pub struct AuthorisationHeaders(pub ClientAuthInfo);
96
97#[async_trait]
98impl FromRequestParts<ServerState> for AuthorisationHeaders {
99    type Rejection = (StatusCode, &'static str);
100
101    // Need to skip all to prevent leaking tokens to logs.
102    #[instrument(level = "debug", skip_all)]
103    async fn from_request_parts(
104        parts: &mut Parts,
105        _state: &ServerState,
106    ) -> Result<Self, Self::Rejection> {
107        let ClientConnInfo {
108            connection_addr: _,
109            client_ip_addr,
110            client_cert,
111        } = parts.extensions.remove::<ClientConnInfo>().ok_or((
112            StatusCode::INTERNAL_SERVER_ERROR,
113            "request info contains invalid data",
114        ))?;
115
116        let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
117            if let Some((authz_type, authz_data)) = header
118                .to_str()
119                .map_err(|err| {
120                    warn!(?err, "Invalid authz header, ignoring");
121                })
122                .ok()
123                .and_then(|s| s.split_once(' '))
124            {
125                let authz_type = authz_type.to_lowercase();
126
127                if authz_type == "basic" {
128                    (Some(authz_data.to_string()), None)
129                } else if authz_type == "bearer" {
130                    if let Ok(jwsc) = JwsCompact::from_str(authz_data) {
131                        (None, Some(jwsc))
132                    } else {
133                        warn!("bearer jws invalid");
134                        (None, None)
135                    }
136                } else {
137                    warn!("authorisation header invalid, ignoring");
138                    (None, None)
139                }
140            } else {
141                (None, None)
142            }
143        } else {
144            (None, None)
145        };
146
147        let client_auth_info = ClientAuthInfo::new(
148            Source::Https(client_ip_addr),
149            client_cert,
150            bearer_token,
151            basic_authz,
152        );
153
154        Ok(AuthorisationHeaders(client_auth_info))
155    }
156}
157
158pub struct DomainInfo(pub DomainInfoRead);
159
160#[async_trait]
161impl FromRequestParts<ServerState> for DomainInfo {
162    type Rejection = (StatusCode, &'static str);
163
164    // Need to skip all to prevent leaking tokens to logs.
165    #[instrument(level = "debug", skip_all)]
166    async fn from_request_parts(
167        _parts: &mut Parts,
168        state: &ServerState,
169    ) -> Result<Self, Self::Rejection> {
170        Ok(DomainInfo(state.qe_r_ref.domain_info_read()))
171    }
172}
173
174#[derive(Debug, Clone)]
175pub struct ClientConnInfo {
176    /// This is the address that is *connected* to Kanidm right now
177    /// for this operation.
178    #[allow(dead_code)]
179    pub connection_addr: SocketAddr,
180    /// This is the client address as reported by a remote IP source
181    /// such as x-forward-for or the PROXY protocol header
182    pub client_ip_addr: IpAddr,
183    // Only set if the certificate is VALID
184    pub client_cert: Option<ClientCertInfo>,
185}
186
187// This is the normal way that our extractors get the ip info
188impl Connected<ClientConnInfo> for ClientConnInfo {
189    fn connect_info(target: ClientConnInfo) -> Self {
190        target
191    }
192}
193
194// This is only used for plaintext http - in other words, integration tests only.
195impl Connected<SocketAddr> for ClientConnInfo {
196    fn connect_info(connection_addr: SocketAddr) -> Self {
197        ClientConnInfo {
198            client_ip_addr: connection_addr.ip().to_canonical(),
199            connection_addr,
200            client_cert: None,
201        }
202    }
203}