Skip to main content

haste_server/auth_n/middleware/
basic_auth.rs

1use crate::{
2    auth_n::oidc::{
3        error::OIDCError,
4        routes::token::{ClientCredentialsMethod, client_credentials_to_token_response},
5        schemas::token_body::{OAuth2TokenBody, OAuth2TokenBodyGrantType},
6    },
7    extract::{
8        basic_credentials::BasicCredentialsHeader,
9        path_tenant::{ProjectIdentifier, TenantIdentifier},
10    },
11    services::ServerState,
12};
13use axum::{
14    extract::{Request, State},
15    middleware::Next,
16    response::Response,
17};
18use axum_extra::extract::Cached;
19use haste_fhir_search::SearchEngine;
20use haste_fhir_terminology::FHIRTerminology;
21use haste_jwt::{ProjectId, TenantId};
22use haste_repository::Repository;
23
24use sha2::{Digest, Sha256};
25use std::{
26    sync::{Arc, LazyLock},
27    time::Duration,
28};
29
30/// How long a token minted for a Basic-auth credential is reused before the
31/// credential is checked against the client application again.
32const CACHED_TOKEN_TTL: Duration = Duration::from_secs(5 * 60);
33
34#[derive(Hash, PartialEq, Eq)]
35struct CacheTokenKey([u8; 32]);
36impl CacheTokenKey {
37    fn new(tenant: &TenantId, project: &ProjectId, client_id: &str, client_secret: &str) -> Self {
38        let mut hasher = Sha256::new();
39        // Length-prefixed so that no two different credentials can digest to the
40        // same byte string by shifting characters across the field boundaries.
41        for part in [tenant.as_ref(), project.as_ref(), client_id, client_secret] {
42            hasher.update((part.len() as u64).to_be_bytes());
43            hasher.update(part.as_bytes());
44        }
45
46        Self(hasher.finalize().into())
47    }
48}
49
50// Token creation is expensive so caching for performance.
51static CACHED_BASIC_TOKENS: LazyLock<moka::future::Cache<CacheTokenKey, String>> =
52    LazyLock::new(|| {
53        moka::future::Cache::builder()
54            .time_to_live(CACHED_TOKEN_TTL)
55            .build()
56    });
57
58pub async fn basic_auth_middleware<
59    Repo: Repository + Send + Sync + 'static,
60    Search: SearchEngine + Send + Sync + 'static,
61    Terminology: FHIRTerminology + Send + Sync + 'static,
62>(
63    Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
64    Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
65    State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
66    // run the `HeaderMap` extractor
67    BasicCredentialsHeader(credentials): BasicCredentialsHeader,
68    // you can also add more extractors here but the last
69    // extractor must implement `FromRequest` which
70    // `Request` does
71    mut request: Request,
72    next: Next,
73) -> Result<Response, OIDCError> {
74    if let Some(credentials) = credentials {
75        if let Some(cached_token) = CACHED_BASIC_TOKENS
76            .get(&CacheTokenKey::new(
77                &tenant,
78                &project,
79                &credentials.0,
80                &credentials.1,
81            ))
82            .await
83        {
84            request.headers_mut().insert(
85                axum::http::header::AUTHORIZATION,
86                format!("Bearer {}", cached_token).parse().unwrap(),
87            );
88        } else {
89            let res = client_credentials_to_token_response(
90                state.as_ref(),
91                &tenant,
92                &project,
93                &None,
94                &OAuth2TokenBody {
95                    client_id: Some(credentials.0.clone()),
96                    client_secret: Some(credentials.1.clone()),
97                    code: None,
98                    code_verifier: None,
99                    grant_type: OAuth2TokenBodyGrantType::ClientCredentials,
100                    redirect_uri: None,
101                    refresh_token: None,
102                    scope: None,
103                },
104                ClientCredentialsMethod::BasicAuth,
105            )
106            .await?;
107
108            let access_token = res.access_token;
109
110            CACHED_BASIC_TOKENS
111                .insert(
112                    CacheTokenKey::new(&tenant, &project, &credentials.0, &credentials.1),
113                    access_token.clone(),
114                )
115                .await;
116
117            request.headers_mut().insert(
118                axum::http::header::AUTHORIZATION,
119                format!("Bearer {}", access_token).parse().unwrap(),
120            );
121        }
122    }
123
124    Ok(next.run(request).await)
125}