1use crate::{
2 auth_n::oidc::{
3 error::{OIDCError, OIDCErrorCode},
4 routes::{authorize, jwks, token},
5 },
6 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
7 route_path::{api_v1_oidc_auth_path, api_v1_oidc_path, project_path},
8 services::ServerState,
9};
10use axum::{
11 extract::{FromRequestParts, Json, Path, State},
12 http::request::Parts,
13 response::{IntoResponse, Response},
14};
15use axum_extra::extract::Cached;
16use haste_fhir_search::SearchEngine;
17use haste_fhir_terminology::FHIRTerminology;
18use haste_jwt::{ProjectId, TenantId, scopes::Scopes};
19use haste_repository::Repository;
20use serde::{Deserialize, Serialize};
21use std::sync::Arc;
22use url::Url;
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25pub struct WellKnownDiscoveryDocument {
26 pub issuer: String,
27 pub authorization_endpoint: String,
28 pub jwks_uri: String,
29 pub token_endpoint: String,
30 pub scopes_supported: Vec<String>,
31 pub response_types_supported: Vec<String>,
32 pub token_endpoint_auth_methods_supported: Vec<String>,
33 pub id_token_signing_alg_values_supported: Vec<String>,
34 pub subject_types_supported: Vec<String>,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone)]
38pub struct SmartConfigurationAssociatedEndpoint {
39 pub url: String,
40 pub capabilities: Vec<String>,
41}
42
43#[derive(Serialize, Deserialize, Debug, Clone)]
44pub struct SmartConfigurationDocument {
45 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
51 pub issuer: Option<String>,
52
53 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
59 pub jwks_uri: Option<String>,
60
61 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
67 pub authorization_endpoint: Option<String>,
68
69 pub grant_types_supported: Vec<String>,
76
77 pub token_endpoint: String,
81
82 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
88 pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
89
90 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
95 pub registration_endpoint: Option<String>,
96
97 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
103 pub smart_app_state_endpoint: Option<String>,
104
105 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
111 pub associated_endpoints: Option<Vec<SmartConfigurationAssociatedEndpoint>>,
112
113 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
117 pub user_access_brand_bundle: Option<String>,
118
119 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
124 pub user_access_brand_identifier: Option<String>,
125
126 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
133 pub scopes_supported: Option<Vec<String>>,
134
135 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
141 pub response_types_supported: Option<Vec<String>>,
142
143 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
149 pub management_endpoint: Option<String>,
150
151 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
156 pub introspection_endpoint: Option<String>,
157
158 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
163 pub revocation_endpoint: Option<String>,
164
165 pub capabilities: Vec<String>,
170
171 pub code_challenge_methods_supported: Vec<String>,
177}
178
179#[derive(Serialize, Deserialize, Debug, Clone)]
180pub struct OAuthProtectedResourceDocument {
181 resource: String,
186
187 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
198 authorization_servers: Option<Vec<String>>,
199
200 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
211 jwks_uri: Option<String>,
212
213 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
221 scopes_supported: Option<Vec<String>>,
222
223 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
234 bearer_methods_supported: Option<Vec<String>>,
235
236 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
244 resource_signing_alg_values_supported: Option<Vec<String>>,
245
246 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
253 resource_name: Option<String>,
254
255 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
262 resource_documentation: Option<String>,
263
264 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
271 resource_policy_uri: Option<String>,
272
273 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
279 resource_tos_uri: Option<String>,
280
281 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
287 tls_client_certificate_bound_access_tokens: Option<bool>,
288
289 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
295 authorization_details_types_supported: Option<Vec<String>>,
296
297 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
304 dpop_signing_alg_values_supported: Option<Vec<String>>,
305
306 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
312 dpop_bound_access_tokens_required: Option<bool>,
313}
314
315#[derive(Deserialize, Clone)]
316pub struct ResourcePath {
317 pub resource: String,
318}
319
320impl<S: Send + Sync> FromRequestParts<S> for ResourcePath {
321 type Rejection = Response;
322
323 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
324 let Path(resource) = Path::<ResourcePath>::from_request_parts(parts, state)
325 .await
326 .map_err(|err| err.into_response())?;
327
328 Ok(resource)
329 }
330}
331
332pub async fn oauth_protected_resource<
333 Repo: Repository + Send + Sync,
334 Search: SearchEngine + Send + Sync,
335 Terminology: FHIRTerminology + Send + Sync,
336>(
337 Cached(ResourcePath { resource }): Cached<ResourcePath>,
338 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
339 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
340 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
341) -> Result<Json<OAuthProtectedResourceDocument>, OIDCError> {
342 let api_url_string = &state.config.api_uri;
343
344 if api_url_string.is_empty() {
345 return Err(OIDCError::new(
346 OIDCErrorCode::ServerError,
347 Some("API_URL is not set in the configuration".to_string()),
348 None,
349 ));
350 }
351
352 let Ok(api_url) = Url::parse(api_url_string) else {
353 return Err(OIDCError::new(
354 OIDCErrorCode::ServerError,
355 Some("Invalid API_URL format".to_string()),
356 None,
357 ));
358 };
359
360 let default_scopes =
362 Scopes::try_from("openid profile user/*.* offline_access fhirUser").unwrap_or_default();
363
364 let oauth_protected_resource = OAuthProtectedResourceDocument {
365 resource: api_url
366 .join(
367 project_path(&tenant, &project)
368 .join(&resource)
369 .to_str()
370 .unwrap_or_default(),
371 )
372 .unwrap()
373 .to_string(),
374 authorization_servers: Some(vec![
375 api_url
376 .join(project_path(&tenant, &project).to_str().unwrap())
377 .unwrap()
378 .to_string(),
379 ]),
380 jwks_uri: None,
381
382 scopes_supported: Some(
383 default_scopes
384 .0
385 .into_iter()
386 .map(String::from)
387 .collect::<Vec<_>>(),
388 ),
389 bearer_methods_supported: None,
390 resource_signing_alg_values_supported: None,
391 resource_name: None,
392 resource_documentation: None,
393 resource_policy_uri: None,
394 resource_tos_uri: None,
395 tls_client_certificate_bound_access_tokens: None,
396 authorization_details_types_supported: None,
397 dpop_signing_alg_values_supported: None,
398 dpop_bound_access_tokens_required: None,
399 };
400
401 Ok(Json(oauth_protected_resource))
402}
403
404pub fn create_oidc_discovery_document(
405 tenant: &TenantId,
406 project: &ProjectId,
407 api_url_string: &str,
408) -> Result<WellKnownDiscoveryDocument, OIDCError> {
409 if api_url_string.is_empty() {
410 return Err(OIDCError::new(
411 OIDCErrorCode::ServerError,
412 Some("API_URL is not set in the configuration".to_string()),
413 None,
414 ));
415 }
416
417 let Ok(api_url) = Url::parse(api_url_string) else {
418 return Err(OIDCError::new(
419 OIDCErrorCode::ServerError,
420 Some("Invalid API_URL format".to_string()),
421 None,
422 ));
423 };
424
425 let authorize_path = api_v1_oidc_auth_path(tenant, project).join(
426 authorize::AuthorizePath
427 .to_string()
428 .strip_prefix("/")
429 .unwrap(),
430 );
431
432 let token_path = api_v1_oidc_auth_path(tenant, project)
433 .join(token::TokenPath.to_string().strip_prefix("/").unwrap());
434
435 let jwks_path = api_v1_oidc_path(tenant, project)
436 .join(jwks::JWKSPath.to_string().strip_prefix("/").unwrap());
437
438 let oidc_response = WellKnownDiscoveryDocument {
439 issuer: api_url
444 .join(project_path(tenant, project).to_str().unwrap_or_default())
445 .unwrap_or(api_url.clone())
446 .to_string(),
447 authorization_endpoint: api_url
448 .join(authorize_path.to_str().unwrap_or_default())
449 .unwrap()
450 .to_string(),
451 token_endpoint: api_url
452 .join(token_path.to_str().unwrap_or_default())
453 .unwrap()
454 .to_string(),
455 jwks_uri: api_url
456 .join(jwks_path.to_str().unwrap_or_default())
457 .unwrap()
458 .to_string(),
459 scopes_supported: vec![
460 "openid".to_string(),
461 "profile".to_string(),
462 "email".to_string(),
463 "offline_access".to_string(),
464 "fhirUser".to_string(),
465 ],
466 response_types_supported: vec![
467 "code".to_string(),
468 "id_token".to_string(),
469 "id_token token".to_string(),
470 ],
471 token_endpoint_auth_methods_supported: vec![
472 "client_secret_basic".to_string(),
473 "client_secret_post".to_string(),
474 ],
475 id_token_signing_alg_values_supported: vec!["RS256".to_string()],
476 subject_types_supported: vec!["public".to_string()],
477 };
478
479 Ok(oidc_response)
480}
481
482pub fn create_smart_configuration(
483 tenant: &TenantId,
484 project: &ProjectId,
485 api_url_string: &str,
486) -> Result<SmartConfigurationDocument, OIDCError> {
487 if api_url_string.is_empty() {
488 return Err(OIDCError::new(
489 OIDCErrorCode::ServerError,
490 Some("api_uri is not set in the configuration".to_string()),
491 None,
492 ));
493 }
494
495 let Ok(api_url) = Url::parse(api_url_string) else {
496 return Err(OIDCError::new(
497 OIDCErrorCode::ServerError,
498 Some("Invalid api_uri format".to_string()),
499 None,
500 ));
501 };
502
503 let authorize_path = api_v1_oidc_auth_path(tenant, project).join(
504 authorize::AuthorizePath
505 .to_string()
506 .strip_prefix("/")
507 .unwrap(),
508 );
509
510 let token_path = api_v1_oidc_auth_path(tenant, project)
511 .join(token::TokenPath.to_string().strip_prefix("/").unwrap());
512
513 let jwks_path = api_v1_oidc_path(tenant, project)
514 .join(jwks::JWKSPath.to_string().strip_prefix("/").unwrap());
515
516 let smart_document = SmartConfigurationDocument {
517 issuer: Some(api_url.to_string()),
518 authorization_endpoint: Some(
519 api_url
520 .join(authorize_path.to_str().unwrap_or_default())
521 .unwrap()
522 .to_string(),
523 ),
524 token_endpoint: api_url
525 .join(token_path.to_str().unwrap_or_default())
526 .unwrap()
527 .to_string(),
528 jwks_uri: Some(
529 api_url
530 .join(jwks_path.to_str().unwrap_or_default())
531 .unwrap()
532 .to_string(),
533 ),
534 scopes_supported: Some(vec![
535 "openid".to_string(),
536 "profile".to_string(),
537 "email".to_string(),
538 "offline_access".to_string(),
539 "fhirUser".to_string(),
540 "user/*.cruds".to_string(),
542 "system/*.cruds".to_string(),
543 ]),
544 response_types_supported: Some(vec![
545 "code".to_string(),
546 "id_token".to_string(),
547 "id_token token".to_string(),
548 ]),
549 token_endpoint_auth_methods_supported: Some(vec![
550 "client_secret_basic".to_string(),
551 "client_secret_post".to_string(),
552 ]),
553 grant_types_supported: vec![
554 "authorization_code".to_string(),
555 "client_credentials".to_string(),
556 ],
557 capabilities: vec![
558 "sso-openid-connect".to_string(),
559 "client-confidential-symmetric".to_string(),
560 "launch-standalone".to_string(),
561 "permission-user".to_string(),
562 "permission-v2".to_string(),
563 ],
564 code_challenge_methods_supported: vec!["S256".to_string()],
565 registration_endpoint: None,
566 smart_app_state_endpoint: None,
567 associated_endpoints: None,
568 user_access_brand_bundle: None,
569 user_access_brand_identifier: None,
570 management_endpoint: None,
571 introspection_endpoint: None,
572 revocation_endpoint: None,
573 };
574
575 Ok(smart_document)
576}
577
578pub async fn openid_configuration<
579 Repo: Repository + Send + Sync,
580 Search: SearchEngine + Send + Sync,
581 Terminology: FHIRTerminology + Send + Sync,
582>(
583 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
584 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
585 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
586) -> Result<Json<WellKnownDiscoveryDocument>, OIDCError> {
587 let api_url_string = &state.config.api_uri;
588
589 Ok(Json(create_oidc_discovery_document(
590 &tenant,
591 &project,
592 api_url_string,
593 )?))
594}
595
596pub async fn smart_configuration<
597 Repo: Repository + Send + Sync,
598 Search: SearchEngine + Send + Sync,
599 Terminology: FHIRTerminology + Send + Sync,
600>(
601 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
602 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
603 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
604) -> Result<Json<SmartConfigurationDocument>, OIDCError> {
605 let api_url_string = &state.config.api_uri;
606
607 Ok(Json(create_smart_configuration(
608 &tenant,
609 &project,
610 api_url_string,
611 )?))
612}