haste_server/auth_n/middleware/
jwt.rs1use crate::error::ServerError;
2use crate::{
3 auth_n::certificates,
4 config::ServerConfig,
5 extract::{
6 bearer_token::AuthBearer,
7 path_tenant::{ProjectIdentifier, TenantIdentifier},
8 },
9 route_path::{api_fhir_root_url, project_path},
10 services::ServerState,
11};
12use axum::{
13 extract::{OriginalUri, Request, State},
14 http::{HeaderMap, StatusCode, Uri},
15 middleware::Next,
16 response::{IntoResponse as _, Response},
17};
18use axum_extra::extract::Cached;
19use derivative::Derivative;
20use haste_fhir_operation_error::OperationOutcomeError;
21use haste_fhir_search::SearchEngine;
22use haste_fhir_terminology::FHIRTerminology;
23use haste_jwt::{ProjectId, TenantId, claims::UserTokenClaims};
24use haste_repository::Repository;
25use jsonwebtoken::Validation;
26use std::{path::PathBuf, sync::Arc};
27use url::Url;
28
29#[derive(Derivative)]
30#[derivative(Debug)]
31pub struct User {
32 #[allow(dead_code)]
33 #[derivative(Debug = "ignore")]
34 pub token: Option<String>,
35 pub claims: haste_jwt::claims::UserTokenClaims,
36}
37
38pub(crate) fn validate_jwt(
42 config: &ServerConfig,
43 tenant: &TenantId,
44 project: &ProjectId,
45 token: &str,
46) -> Result<UserTokenClaims, StatusCode> {
47 let header = jsonwebtoken::decode_header(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
48
49 let kid = header.kid.ok_or(StatusCode::UNAUTHORIZED)?;
50
51 let cert_provider = certificates::get_certification_provider(config);
52
53 let decoding_key = cert_provider
54 .decoding_key(kid.as_str())
55 .map_err(|_| StatusCode::UNAUTHORIZED)?;
56
57 let expected_audience = api_fhir_root_url(&config.api_uri, tenant, project)
60 .map_err(|_| StatusCode::UNAUTHORIZED)?;
61
62 let mut validation = Validation::new(jsonwebtoken::Algorithm::RS256);
63 validation.set_audience(&[expected_audience.as_str()]);
64
65 let result =
66 jsonwebtoken::decode::<UserTokenClaims>(token, &decoding_key.decoding_key, &validation)
67 .map_err(|_| StatusCode::UNAUTHORIZED)?;
68
69 Ok(result.claims)
70}
71
72pub fn derive_well_known_openid_configuration_url(
73 api_url: &str,
74 tenant: &TenantId,
75 project: &ProjectId,
76) -> Result<Url, OperationOutcomeError> {
77 let path = PathBuf::from("/.well-known/openid-configuration");
78
79 Url::parse(api_url)
80 .map_err(ServerError::from)?
81 .join(
82 path.join(project_path(tenant, project).strip_prefix("/").unwrap())
83 .to_str()
84 .unwrap_or_default(),
85 )
86 .map_err(|e| ServerError::from(e).into())
87}
88
89pub fn derive_protected_resource_metadata_url(
90 resource_uri: &Uri,
91 api_url: &str,
92) -> Result<Url, OperationOutcomeError> {
93 let path = PathBuf::from("/.well-known/oauth-protected-resource");
94 let tenant_url = Url::parse(api_url)
95 .map_err(ServerError::from)?
96 .join(
97 path.join(resource_uri.path().strip_prefix("/").unwrap_or_default())
98 .to_str()
99 .unwrap_or_default(),
100 )
101 .map_err(ServerError::from)?;
102
103 Ok(tenant_url)
104}
105
106fn invalid_jwt_response(uri: &Uri, api_url: &str, status_code: StatusCode) -> Response {
107 tracing::warn!(
108 "Invalid JWT token provided in request sending '{}'",
109 status_code
110 );
111
112 let Ok(protected_resource_metadata_url) = derive_protected_resource_metadata_url(uri, api_url)
113 else {
114 return (status_code).into_response();
115 };
116
117 let mut headers = HeaderMap::new();
118 headers.insert(
119 axum::http::header::WWW_AUTHENTICATE,
120 format!(
121 r#"Bearer resource_metadata="{}""#,
122 protected_resource_metadata_url
123 )
124 .parse()
125 .unwrap(),
126 );
127 (status_code, headers).into_response()
128}
129
130pub async fn token_verifcation<
131 Repo: Repository + Send + Sync + 'static,
132 Search: SearchEngine + Send + Sync + 'static,
133 Terminology: FHIRTerminology + Send + Sync + 'static,
134>(
135 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
136 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
137 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
138 AuthBearer(token): AuthBearer,
140 OriginalUri(uri): OriginalUri,
144 mut request: Request,
145 next: Next,
146) -> Result<Response, Response> {
147 let Some(token) = token else {
148 return Err(invalid_jwt_response(
149 &uri,
150 &state.config.api_uri,
151 StatusCode::UNAUTHORIZED,
152 ));
153 };
154
155 match validate_jwt(state.config.as_ref(), &tenant, &project, &token) {
156 Ok(claims) => {
157 request.extensions_mut().insert(Arc::new(User {
158 token: Some(token),
159 claims,
160 }));
161 Ok(next.run(request).await)
162 }
163 Err(status_code) => match status_code {
164 StatusCode::UNAUTHORIZED => Err(invalid_jwt_response(
165 &uri,
166 &state.config.api_uri,
167 status_code,
168 )),
169 _ => Err((status_code).into_response()),
170 },
171 }
172}