Skip to main content

haste_server/auth_n/middleware/
jwt.rs

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