haste_server/auth_n/middleware/
jwt.rs1use 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 AuthBearer(token): AuthBearer,
108 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}