haste_server/auth_n/oidc/routes/
discovery.rs1use crate::{
2 auth_n::oidc::{
3 error::{OIDCError, OIDCErrorCode},
4 routes::{AUTH_NESTED_PATH, authorize, jwks, token},
5 },
6 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
7 services::AppState,
8};
9use axum::{
10 extract::{FromRequestParts, Json, Path, State},
11 http::request::Parts,
12 response::{IntoResponse, Response},
13};
14use axum_extra::extract::Cached;
15use haste_fhir_search::SearchEngine;
16use haste_fhir_terminology::FHIRTerminology;
17use haste_jwt::{ProjectId, TenantId, scopes::Scopes};
18use haste_repository::Repository;
19use serde::{Deserialize, Serialize};
20use std::sync::Arc;
21use url::Url;
22
23#[derive(Serialize, Deserialize, Debug, Clone)]
24pub struct WellKnownDiscoveryDocument {
25 pub issuer: String,
26 pub authorization_endpoint: String,
27 pub jwks_uri: String,
28 pub token_endpoint: String,
29 pub scopes_supported: Vec<String>,
30 pub response_types_supported: Vec<String>,
31 pub token_endpoint_auth_methods_supported: Vec<String>,
32 pub id_token_signing_alg_values_supported: Vec<String>,
33 pub subject_types_supported: Vec<String>,
34}
35
36#[derive(Serialize, Deserialize, Debug, Clone)]
37pub struct OAuthProtectedResourceDocument {
38 resource: String,
43
44 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
55 authorization_servers: Option<Vec<String>>,
56
57 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
68 jwks_uri: Option<String>,
69
70 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
78 scopes_supported: Option<Vec<String>>,
79
80 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
91 bearer_methods_supported: Option<Vec<String>>,
92
93 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
101 resource_signing_alg_values_supported: Option<Vec<String>>,
102
103 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
110 resource_name: Option<String>,
111
112 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
119 resource_documentation: Option<String>,
120
121 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
128 resource_policy_uri: Option<String>,
129
130 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
136 resource_tos_uri: Option<String>,
137
138 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
144 tls_client_certificate_bound_access_tokens: Option<bool>,
145
146 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
152 authorization_details_types_supported: Option<Vec<String>>,
153
154 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
161 dpop_signing_alg_values_supported: Option<Vec<String>>,
162
163 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
169 dpop_bound_access_tokens_required: Option<bool>,
170}
171
172fn construct_oidc_route(tenant: &TenantId, project: &ProjectId, path: &str) -> String {
173 format!(
174 "/w/{}/{}/api/v1/oidc{}",
175 tenant.as_ref(),
176 project.as_ref(),
177 path
178 )
179}
180
181#[derive(Deserialize, Clone)]
182pub struct ResourcePath {
183 pub resource: String,
184}
185
186impl<S: Send + Sync> FromRequestParts<S> for ResourcePath {
187 type Rejection = Response;
188
189 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
190 let Path(resource) = Path::<ResourcePath>::from_request_parts(parts, state)
191 .await
192 .map_err(|err| err.into_response())?;
193
194 Ok(resource)
195 }
196}
197
198pub async fn oauth_protected_resource<
199 Repo: Repository + Send + Sync,
200 Search: SearchEngine + Send + Sync,
201 Terminology: FHIRTerminology + Send + Sync,
202>(
203 Cached(ResourcePath { resource }): Cached<ResourcePath>,
204 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
205 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
206 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
207) -> Result<Json<OAuthProtectedResourceDocument>, OIDCError> {
208 let api_url_string = state
209 .config
210 .get(crate::ServerEnvironmentVariables::APIURI)
211 .unwrap_or_default();
212
213 if api_url_string.is_empty() {
214 return Err(OIDCError::new(
215 OIDCErrorCode::ServerError,
216 Some("API_URL is not set in the configuration".to_string()),
217 None,
218 ));
219 }
220
221 let Ok(api_url) = Url::parse(&api_url_string) else {
222 return Err(OIDCError::new(
223 OIDCErrorCode::ServerError,
224 Some("Invalid API_URL format".to_string()),
225 None,
226 ));
227 };
228
229 let default_scopes =
231 Scopes::try_from("openid profile user/*.* offline_access").unwrap_or_default();
232
233 let oauth_protected_resource = OAuthProtectedResourceDocument {
234 resource: api_url
235 .join(&format!(
236 "/w/{}/{}/{}",
237 tenant.as_ref(),
238 project.as_ref(),
239 resource
240 ))
241 .unwrap()
242 .to_string(),
243
244 authorization_servers: Some(vec![
245 api_url
246 .join(&format!("/w/{}/{}", tenant.as_ref(), project.as_ref()))
247 .unwrap()
248 .to_string(),
249 ]),
250 jwks_uri: None,
251
252 scopes_supported: Some(
253 default_scopes
254 .0
255 .into_iter()
256 .map(|s| String::from(s))
257 .collect::<Vec<_>>(),
258 ),
259 bearer_methods_supported: None,
260 resource_signing_alg_values_supported: None,
261 resource_name: None,
262 resource_documentation: None,
263 resource_policy_uri: None,
264 resource_tos_uri: None,
265 tls_client_certificate_bound_access_tokens: None,
266 authorization_details_types_supported: None,
267 dpop_signing_alg_values_supported: None,
268 dpop_bound_access_tokens_required: None,
269 };
270
271 Ok(Json(oauth_protected_resource))
272}
273
274pub fn create_oidc_discovery_document(
275 tenant: &TenantId,
276 project: &ProjectId,
277 api_url_string: &str,
278) -> Result<WellKnownDiscoveryDocument, OIDCError> {
279 if api_url_string.is_empty() {
280 return Err(OIDCError::new(
281 OIDCErrorCode::ServerError,
282 Some("API_URL is not set in the configuration".to_string()),
283 None,
284 ));
285 }
286
287 let Ok(api_url) = Url::parse(&api_url_string) else {
288 return Err(OIDCError::new(
289 OIDCErrorCode::ServerError,
290 Some("Invalid API_URL format".to_string()),
291 None,
292 ));
293 };
294
295 let authorize_path = construct_oidc_route(
296 &tenant,
297 &project,
298 &(AUTH_NESTED_PATH.to_string() + authorize::AuthorizePath.to_string().as_str()),
299 );
300
301 let token_path = construct_oidc_route(
302 &tenant,
303 &project,
304 &(AUTH_NESTED_PATH.to_string() + token::TokenPath.to_string().as_str()),
305 );
306
307 let jwks_path = construct_oidc_route(&tenant, &project, &(jwks::JWKSPath.to_string().as_str()));
308
309 let oidc_response = WellKnownDiscoveryDocument {
310 issuer: api_url.to_string(),
311 authorization_endpoint: api_url.join(&authorize_path).unwrap().to_string(),
312 token_endpoint: api_url.join(&token_path).unwrap().to_string(),
313 jwks_uri: api_url.join(&jwks_path).unwrap().to_string(),
314 scopes_supported: vec![
315 "openid".to_string(),
316 "profile".to_string(),
317 "email".to_string(),
318 "offline_access".to_string(),
319 ],
320 response_types_supported: vec![
321 "code".to_string(),
322 "id_token".to_string(),
323 "id_token token".to_string(),
324 ],
325 token_endpoint_auth_methods_supported: vec![
326 "client_secret_basic".to_string(),
327 "client_secret_post".to_string(),
328 ],
329 id_token_signing_alg_values_supported: vec!["RS256".to_string()],
330 subject_types_supported: vec!["public".to_string()],
331 };
332
333 Ok(oidc_response)
334}
335
336pub async fn openid_configuration<
337 Repo: Repository + Send + Sync,
338 Search: SearchEngine + Send + Sync,
339 Terminology: FHIRTerminology + Send + Sync,
340>(
341 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
342 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
343 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
344) -> Result<Json<WellKnownDiscoveryDocument>, OIDCError> {
345 let api_url_string = state
346 .config
347 .get(crate::ServerEnvironmentVariables::APIURI)
348 .unwrap_or_default();
349
350 Ok(Json(create_oidc_discovery_document(
351 &tenant,
352 &project,
353 &api_url_string,
354 )?))
355}