Skip to main content

rskit_auth/
bearer.rs

1//! Tower middleware for header-only bearer-token authentication.
2//!
3//! The layer rejects missing credentials unless [`BearerAuthLayer::accept_missing`]
4//! is selected explicitly. Rejections include a neutral `WWW-Authenticate:
5//! Bearer` challenge; this crate does not hard-code an application realm.
6
7use std::{future::Future, marker::PhantomData, pin::Pin, sync::Arc};
8
9use http::{Request, Response, StatusCode, header};
10use rskit_security::BEARER_AUTH_SCHEME;
11use tower::{Layer, Service};
12
13use crate::{AuthClaims, AuthOutcome, MissingCredentialPolicy, traits::TokenValidator};
14
15/// Tower layer that validates `Authorization: Bearer <token>` headers.
16///
17/// The layer stores successful claims in request extensions as
18/// [`AuthClaims<C>`](crate::AuthClaims) and
19/// [`AuthOutcome<C>`](crate::AuthOutcome). Invalid or rejected requests receive
20/// `401 Unauthorized` with `WWW-Authenticate: Bearer`.
21#[derive(Clone)]
22pub struct BearerAuthLayer<V, C> {
23    validator: Arc<V>,
24    missing_policy: MissingCredentialPolicy,
25    _claims: PhantomData<fn() -> C>,
26}
27
28impl<V: 'static, C> BearerAuthLayer<V, C> {
29    /// Create a new bearer-auth layer that rejects missing credentials.
30    #[must_use]
31    pub fn new(validator: V) -> Self {
32        Self {
33            validator: Arc::new(validator),
34            missing_policy: MissingCredentialPolicy::RejectMissing,
35            _claims: PhantomData,
36        }
37    }
38
39    /// Explicitly accept requests with no credentials.
40    #[must_use]
41    pub const fn accept_missing(mut self) -> Self {
42        self.missing_policy = MissingCredentialPolicy::AcceptMissing;
43        self
44    }
45}
46
47impl<S, V, C> Layer<S> for BearerAuthLayer<V, C>
48where
49    V: 'static,
50{
51    type Service = BearerAuthService<S, V, C>;
52
53    fn layer(&self, inner: S) -> Self::Service {
54        BearerAuthService {
55            inner,
56            validator: Arc::clone(&self.validator),
57            missing_policy: self.missing_policy,
58            _claims: PhantomData,
59        }
60    }
61}
62
63/// Tower service produced by [`BearerAuthLayer`].
64#[derive(Clone)]
65pub struct BearerAuthService<S, V, C> {
66    inner: S,
67    validator: Arc<V>,
68    missing_policy: MissingCredentialPolicy,
69    _claims: PhantomData<fn() -> C>,
70}
71
72impl<S, V, C, ReqBody, ResBody> Service<Request<ReqBody>> for BearerAuthService<S, V, C>
73where
74    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
75    S::Future: Send + 'static,
76    V: TokenValidator<C> + 'static,
77    C: Clone + Send + Sync + 'static,
78    ReqBody: Send + 'static,
79    ResBody: Default + Send + 'static,
80{
81    type Response = S::Response;
82    type Error = S::Error;
83    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
84
85    fn poll_ready(
86        &mut self,
87        cx: &mut std::task::Context<'_>,
88    ) -> std::task::Poll<Result<(), Self::Error>> {
89        self.inner.poll_ready(cx)
90    }
91
92    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
93        let clone = self.inner.clone();
94        let mut inner = std::mem::replace(&mut self.inner, clone);
95        let validator = Arc::clone(&self.validator);
96        let missing_policy = self.missing_policy;
97
98        Box::pin(async move {
99            match extract_bearer_token(&req) {
100                CredentialExtraction::Missing => {
101                    if missing_policy == MissingCredentialPolicy::AcceptMissing {
102                        let mut req = req;
103                        req.extensions_mut().remove::<AuthClaims<C>>();
104                        req.extensions_mut().insert(AuthOutcome::<C>::Missing);
105                        inner.call(req).await
106                    } else {
107                        Ok(unauthorized_bearer_response())
108                    }
109                }
110                CredentialExtraction::Invalid => Ok(unauthorized_bearer_response()),
111                CredentialExtraction::Present(token) => match validator.validate(token).await {
112                    Ok(claims) => {
113                        let mut req = req;
114                        req.extensions_mut().insert(AuthClaims(claims.clone()));
115                        req.extensions_mut()
116                            .insert(AuthOutcome::Authenticated(claims));
117                        inner.call(req).await
118                    }
119                    Err(_) => Ok(unauthorized_bearer_response()),
120                },
121            }
122        })
123    }
124}
125
126enum CredentialExtraction<'a> {
127    Missing,
128    Invalid,
129    Present(&'a str),
130}
131
132fn extract_bearer_token<B>(req: &Request<B>) -> CredentialExtraction<'_> {
133    let mut values = req.headers().get_all(header::AUTHORIZATION).iter();
134    let Some(value) = values.next() else {
135        return CredentialExtraction::Missing;
136    };
137    if values.next().is_some() {
138        return CredentialExtraction::Invalid;
139    }
140    let Ok(value) = value.to_str() else {
141        return CredentialExtraction::Invalid;
142    };
143    let Some((scheme, token)) = value.split_once(' ') else {
144        return CredentialExtraction::Invalid;
145    };
146    if !scheme.eq_ignore_ascii_case(BEARER_AUTH_SCHEME) {
147        return CredentialExtraction::Invalid;
148    }
149    let token = token.trim_start_matches(' ');
150    if token.is_empty() || token.chars().any(char::is_whitespace) {
151        return CredentialExtraction::Invalid;
152    }
153    CredentialExtraction::Present(token)
154}
155
156fn unauthorized_bearer_response<ResBody: Default>() -> Response<ResBody> {
157    let mut response = Response::new(ResBody::default());
158    *response.status_mut() = StatusCode::UNAUTHORIZED;
159    response.headers_mut().insert(
160        header::WWW_AUTHENTICATE,
161        http::HeaderValue::from_static(BEARER_AUTH_SCHEME),
162    );
163    response
164}
165
166#[cfg(test)]
167mod tests {
168    use std::{convert::Infallible, future::Ready};
169
170    use async_trait::async_trait;
171    use http::{Request, Response, StatusCode};
172    use rskit_errors::{AppError, AppResult};
173    use rskit_security::BEARER_AUTH_SCHEME;
174    use serde::{Deserialize, Serialize};
175    use tower::{Layer, Service, ServiceExt};
176
177    use super::{BearerAuthLayer, CredentialExtraction, extract_bearer_token};
178    use crate::{AuthClaims, AuthOutcome, TokenValidator};
179
180    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181    struct Claims {
182        sub: String,
183    }
184
185    struct Validator;
186
187    #[async_trait]
188    impl TokenValidator<Claims> for Validator {
189        async fn validate(&self, token: &str) -> AppResult<Claims> {
190            if token == "good.token" {
191                Ok(Claims {
192                    sub: "user-1".into(),
193                })
194            } else {
195                Err(AppError::invalid_token())
196            }
197        }
198    }
199
200    #[derive(Clone)]
201    struct ExtensionCheckingService;
202
203    impl Service<Request<()>> for ExtensionCheckingService {
204        type Response = Response<()>;
205        type Error = Infallible;
206        type Future = Ready<Result<Self::Response, Self::Error>>;
207
208        fn poll_ready(
209            &mut self,
210            _cx: &mut std::task::Context<'_>,
211        ) -> std::task::Poll<Result<(), Self::Error>> {
212            std::task::Poll::Ready(Ok(()))
213        }
214
215        fn call(&mut self, request: Request<()>) -> Self::Future {
216            let has_claims = request.extensions().get::<AuthClaims<Claims>>().is_some();
217            let has_outcome = request.extensions().get::<AuthOutcome<Claims>>().is_some();
218            let status = if has_claims && has_outcome {
219                StatusCode::OK
220            } else if matches!(
221                request.extensions().get::<AuthOutcome<Claims>>(),
222                Some(AuthOutcome::Missing)
223            ) {
224                StatusCode::NO_CONTENT
225            } else {
226                StatusCode::IM_A_TEAPOT
227            };
228            std::future::ready(Ok(Response::builder().status(status).body(()).unwrap()))
229        }
230    }
231
232    #[test]
233    fn bearer_extraction_requires_single_authorization_header() {
234        for value in [
235            "Bearer abc.def.ghi",
236            "bearer abc.def.ghi",
237            "Bearer  abc.def.ghi",
238        ] {
239            let request = http::Request::builder()
240                .header(http::header::AUTHORIZATION, value)
241                .body(())
242                .unwrap();
243
244            assert!(matches!(
245                extract_bearer_token(&request),
246                CredentialExtraction::Present("abc.def.ghi")
247            ));
248        }
249
250        let request = http::Request::builder()
251            .header(http::header::AUTHORIZATION, "Bearer one")
252            .header(http::header::AUTHORIZATION, "Bearer two")
253            .body(())
254            .unwrap();
255
256        assert!(matches!(
257            extract_bearer_token(&request),
258            CredentialExtraction::Invalid
259        ));
260    }
261
262    #[test]
263    fn bearer_extraction_rejects_missing_or_malformed_values() {
264        let missing = http::Request::builder().body(()).unwrap();
265        assert!(matches!(
266            extract_bearer_token(&missing),
267            CredentialExtraction::Missing
268        ));
269
270        for value in [
271            "Basic token",
272            "Bearer ",
273            "Bearer token with-space",
274            "Bearer\ttoken",
275            " Bearer token",
276        ] {
277            let request = http::Request::builder()
278                .header(http::header::AUTHORIZATION, value)
279                .body(())
280                .unwrap();
281            assert!(matches!(
282                extract_bearer_token(&request),
283                CredentialExtraction::Invalid
284            ));
285        }
286    }
287
288    #[tokio::test]
289    async fn bearer_layer_rejects_missing_by_default_and_accepts_valid_tokens() {
290        let mut service =
291            BearerAuthLayer::<_, Claims>::new(Validator).layer(ExtensionCheckingService);
292
293        let missing = service
294            .ready()
295            .await
296            .unwrap()
297            .call(Request::builder().body(()).unwrap())
298            .await
299            .unwrap();
300        assert_eq!(missing.status(), StatusCode::UNAUTHORIZED);
301        assert_eq!(
302            missing
303                .headers()
304                .get(http::header::WWW_AUTHENTICATE)
305                .expect("missing authenticate challenge")
306                .to_str()
307                .expect("challenge should be visible ASCII"),
308            BEARER_AUTH_SCHEME
309        );
310
311        let valid = service
312            .ready()
313            .await
314            .unwrap()
315            .call(
316                Request::builder()
317                    .header(http::header::AUTHORIZATION, "Bearer good.token")
318                    .body(())
319                    .unwrap(),
320            )
321            .await
322            .unwrap();
323        assert_eq!(valid.status(), StatusCode::OK);
324    }
325
326    #[tokio::test]
327    async fn bearer_layer_accept_missing_is_explicit_and_invalid_still_fails() {
328        let mut service = BearerAuthLayer::<_, Claims>::new(Validator)
329            .accept_missing()
330            .layer(ExtensionCheckingService);
331
332        let missing = service
333            .call(Request::builder().body(()).unwrap())
334            .await
335            .unwrap();
336        assert_eq!(missing.status(), StatusCode::NO_CONTENT);
337
338        let invalid = service
339            .call(
340                Request::builder()
341                    .header(http::header::AUTHORIZATION, "Bearer bad.token")
342                    .body(())
343                    .unwrap(),
344            )
345            .await
346            .unwrap();
347        assert_eq!(invalid.status(), StatusCode::UNAUTHORIZED);
348    }
349
350    #[tokio::test]
351    async fn bearer_layer_accept_missing_clears_stale_claims() {
352        let mut service = BearerAuthLayer::<_, Claims>::new(Validator)
353            .accept_missing()
354            .layer(ExtensionCheckingService);
355        let mut request = Request::builder().body(()).unwrap();
356        request.extensions_mut().insert(AuthClaims(Claims {
357            sub: "stale-user".into(),
358        }));
359
360        let response = service.call(request).await.unwrap();
361
362        assert_eq!(response.status(), StatusCode::NO_CONTENT);
363    }
364}