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