haste_server/auth_n/middleware/
jwt.rs

1use crate::{
2    auth_n::certificates,
3    extract::{
4        bearer_token::AuthBearer,
5        path_tenant::{ProjectIdentifier, TenantIdentifier},
6    },
7    services::AppState,
8};
9use axum::{
10    extract::{Request, State},
11    http::{HeaderMap, StatusCode},
12    middleware::Next,
13    response::{IntoResponse as _, Response},
14};
15use axum_extra::extract::Cached;
16use haste_fhir_model::r4::generated::terminology::IssueType;
17use haste_fhir_operation_error::OperationOutcomeError;
18use haste_fhir_search::SearchEngine;
19use haste_fhir_terminology::FHIRTerminology;
20use haste_jwt::{ProjectId, TenantId, claims::UserTokenClaims};
21use haste_repository::Repository;
22use jsonwebtoken::Validation;
23use std::sync::{Arc, LazyLock};
24use url::Url;
25
26static VALIDATION_CONFIG: LazyLock<Validation> = LazyLock::new(|| {
27    let mut config = Validation::new(jsonwebtoken::Algorithm::RS256);
28    config.validate_aud = false;
29    config
30});
31
32fn validate_jwt(token: &str) -> Result<UserTokenClaims, StatusCode> {
33    let result = jsonwebtoken::decode::<UserTokenClaims>(
34        token,
35        certificates::decoding_key(),
36        &*VALIDATION_CONFIG,
37    )
38    .map_err(|_| StatusCode::UNAUTHORIZED)?;
39
40    Ok(result.claims)
41}
42
43pub fn derive_well_known_url(
44    api_url: &str,
45    tenant: &TenantId,
46    project: &ProjectId,
47) -> Result<Url, OperationOutcomeError> {
48    if let Ok(api_url) = Url::parse(&api_url) {
49        api_url
50            .join(&format!(
51                "/.well-known/openid-configuration/w/{}/{}",
52                tenant.as_ref(),
53                project.as_ref(),
54            ))
55            .map_err(|e| {
56                tracing::error!("Failed to derive well-known URL: {:?}", e);
57                OperationOutcomeError::error(
58                    IssueType::Invalid(None),
59                    "Invalid API URL configured".to_string(),
60                )
61            })
62    } else {
63        Err(OperationOutcomeError::error(
64            IssueType::Invalid(None),
65            "Invalid API URL configured".to_string(),
66        ))
67    }
68}
69
70fn invalid_jwt_response(
71    api_url: &str,
72    tenant: &TenantId,
73    project: &ProjectId,
74    status_code: StatusCode,
75) -> Response {
76    tracing::warn!(
77        "Invalid JWT token provided in request sending '{}'",
78        status_code
79    );
80
81    let Ok(well_known_url) = derive_well_known_url(api_url, tenant, project) else {
82        return (status_code).into_response();
83    };
84
85    let mut headers = HeaderMap::new();
86    headers.insert(
87        axum::http::header::WWW_AUTHENTICATE,
88        format!(
89            r#"Bearer resource_metadata="{}""#,
90            well_known_url.to_string()
91        )
92        .parse()
93        .unwrap(),
94    );
95    (status_code, headers).into_response()
96}
97
98pub async fn token_verifcation<
99    Repo: Repository + Send + Sync + 'static,
100    Search: SearchEngine + Send + Sync + 'static,
101    Terminology: FHIRTerminology + Send + Sync + 'static,
102>(
103    Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
104    Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
105    State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
106    // run the `HeaderMap` extractor
107    AuthBearer(token): AuthBearer,
108    // you can also add more extractors here but the last
109    // extractor must implement `FromRequest` which
110    // `Request` does
111    mut request: Request,
112    next: Next,
113) -> Result<Response, Response> {
114    let Some(token) = token else {
115        return Err(invalid_jwt_response(
116            &state
117                .config
118                .get(crate::ServerEnvironmentVariables::APIURI)
119                .unwrap_or_default(),
120            &tenant,
121            &project,
122            StatusCode::UNAUTHORIZED,
123        ));
124    };
125
126    match validate_jwt(&token) {
127        Ok(claims) => {
128            request.extensions_mut().insert(Arc::new(claims));
129            Ok(next.run(request).await)
130        }
131        Err(status_code) => match status_code {
132            StatusCode::UNAUTHORIZED => Err(invalid_jwt_response(
133                &state
134                    .config
135                    .get(crate::ServerEnvironmentVariables::APIURI)
136                    .unwrap_or_default(),
137                &tenant,
138                &project,
139                status_code,
140            )),
141            _ => Err((status_code).into_response()),
142        },
143    }
144}