haste_server/auth_n/middleware/
basic_auth.rs1use 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
30const 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 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
50static 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 BasicCredentialsHeader(credentials): BasicCredentialsHeader,
68 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}