1use crate::{
2 auth_n::oidc::{
3 error::{OIDCError, OIDCErrorCode},
4 routes::{authorize, introspect, jwks, token},
5 },
6 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
7 route_path::{api_v1_oidc_auth_path, api_v1_oidc_path, issuer_url, 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: issuer_url(api_url_string, tenant, project)
444 .map_err(|e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None))?
445 .to_string(),
446 authorization_endpoint: api_url
447 .join(authorize_path.to_str().unwrap_or_default())
448 .unwrap()
449 .to_string(),
450 token_endpoint: api_url
451 .join(token_path.to_str().unwrap_or_default())
452 .unwrap()
453 .to_string(),
454 jwks_uri: api_url
455 .join(jwks_path.to_str().unwrap_or_default())
456 .unwrap()
457 .to_string(),
458 scopes_supported: vec![
459 "openid".to_string(),
460 "profile".to_string(),
461 "email".to_string(),
462 "offline_access".to_string(),
463 "fhirUser".to_string(),
464 ],
465 response_types_supported: vec!["code".to_string()],
467 token_endpoint_auth_methods_supported: vec![
468 "client_secret_basic".to_string(),
469 "client_secret_post".to_string(),
470 "none".to_string(),
471 ],
472 id_token_signing_alg_values_supported: vec!["RS256".to_string()],
473 subject_types_supported: vec!["public".to_string()],
474 };
475
476 Ok(oidc_response)
477}
478
479pub fn create_smart_configuration(
480 tenant: &TenantId,
481 project: &ProjectId,
482 api_url_string: &str,
483) -> Result<SmartConfigurationDocument, OIDCError> {
484 if api_url_string.is_empty() {
485 return Err(OIDCError::new(
486 OIDCErrorCode::ServerError,
487 Some("api_uri is not set in the configuration".to_string()),
488 None,
489 ));
490 }
491
492 let Ok(api_url) = Url::parse(api_url_string) else {
493 return Err(OIDCError::new(
494 OIDCErrorCode::ServerError,
495 Some("Invalid api_uri format".to_string()),
496 None,
497 ));
498 };
499
500 let authorize_path = api_v1_oidc_auth_path(tenant, project).join(
501 authorize::AuthorizePath
502 .to_string()
503 .strip_prefix("/")
504 .unwrap(),
505 );
506
507 let token_path = api_v1_oidc_auth_path(tenant, project)
508 .join(token::TokenPath.to_string().strip_prefix("/").unwrap());
509
510 let jwks_path = api_v1_oidc_path(tenant, project)
511 .join(jwks::JWKSPath.to_string().strip_prefix("/").unwrap());
512
513 let introspect_path = api_v1_oidc_auth_path(tenant, project).join(
514 introspect::IntrospectPath
515 .to_string()
516 .strip_prefix("/")
517 .unwrap(),
518 );
519
520 let smart_document = SmartConfigurationDocument {
521 issuer: Some(
522 issuer_url(api_url_string, tenant, project)
523 .map_err(|e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None))?
524 .to_string(),
525 ),
526 authorization_endpoint: Some(
527 api_url
528 .join(authorize_path.to_str().unwrap_or_default())
529 .unwrap()
530 .to_string(),
531 ),
532 token_endpoint: api_url
533 .join(token_path.to_str().unwrap_or_default())
534 .unwrap()
535 .to_string(),
536 jwks_uri: Some(
537 api_url
538 .join(jwks_path.to_str().unwrap_or_default())
539 .unwrap()
540 .to_string(),
541 ),
542 scopes_supported: Some(vec![
543 "openid".to_string(),
544 "profile".to_string(),
545 "email".to_string(),
546 "offline_access".to_string(),
547 "fhirUser".to_string(),
548 "launch".to_string(),
549 "launch/patient".to_string(),
550 "launch/encounter".to_string(),
551 "patient/*.rs".to_string(),
552 "user/*.cruds".to_string(),
553 "system/*.cruds".to_string(),
554 ]),
555 response_types_supported: Some(vec!["code".to_string()]),
556 token_endpoint_auth_methods_supported: Some(vec![
557 "client_secret_basic".to_string(),
558 "client_secret_post".to_string(),
559 "none".to_string(),
561 ]),
562 grant_types_supported: vec![
563 "authorization_code".to_string(),
564 "client_credentials".to_string(),
565 "refresh_token".to_string(),
566 ],
567 capabilities: vec![
568 "sso-openid-connect".to_string(),
569 "authorize-post".to_string(),
570 "client-public".to_string(),
571 "client-confidential-symmetric".to_string(),
572 "launch-standalone".to_string(),
573 "launch-ehr".to_string(),
574 "context-ehr-patient".to_string(),
575 "context-ehr-encounter".to_string(),
576 "context-banner".to_string(),
577 "context-standalone-patient".to_string(),
578 "context-style".to_string(),
579 "permission-offline".to_string(),
580 "permission-patient".to_string(),
581 "permission-user".to_string(),
582 "permission-v1".to_string(),
583 "permission-v2".to_string(),
584 ],
585 code_challenge_methods_supported: vec!["S256".to_string()],
586 registration_endpoint: None,
587 smart_app_state_endpoint: None,
588 associated_endpoints: None,
589 user_access_brand_bundle: None,
590 user_access_brand_identifier: None,
591 management_endpoint: None,
592 introspection_endpoint: Some(
593 api_url
594 .join(introspect_path.to_str().unwrap_or_default())
595 .unwrap()
596 .to_string(),
597 ),
598 revocation_endpoint: None,
599 };
600
601 Ok(smart_document)
602}
603
604pub async fn openid_configuration<
605 Repo: Repository + Send + Sync,
606 Search: SearchEngine + Send + Sync,
607 Terminology: FHIRTerminology + Send + Sync,
608>(
609 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
610 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
611 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
612) -> Result<Json<WellKnownDiscoveryDocument>, OIDCError> {
613 let api_url_string = &state.config.api_uri;
614
615 Ok(Json(create_oidc_discovery_document(
616 &tenant,
617 &project,
618 api_url_string,
619 )?))
620}
621
622pub async fn smart_configuration<
623 Repo: Repository + Send + Sync,
624 Search: SearchEngine + Send + Sync,
625 Terminology: FHIRTerminology + Send + Sync,
626>(
627 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
628 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
629 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
630) -> Result<Json<SmartConfigurationDocument>, OIDCError> {
631 let api_url_string = &state.config.api_uri;
632
633 Ok(Json(create_smart_configuration(
634 &tenant,
635 &project,
636 api_url_string,
637 )?))
638}