1use crate::{
2 auth_n::{
3 certificates::get_certification_provider,
4 constant_time,
5 oidc::{
6 code_verification,
7 error::{OIDCError, OIDCErrorCode},
8 extract::{body::OAuthTokenBody, client_app::find_client_app},
9 routes::{scope::verify_requested_scope_is_subset, style},
10 schemas,
11 },
12 },
13 config::ServerConfig,
14 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
15 route_path::{api_fhir_root_url, api_v1_oidc_path, issuer_url},
16 services::ServerState,
17};
18use axum::{
19 Json,
20 extract::State,
21 response::{IntoResponse, Response},
22};
23use axum_extra::{TypedHeader, extract::Cached, headers::UserAgent, routing::TypedPath};
24use haste_fhir_client::{
25 FHIRClient,
26 request::{FHIRReadRequest, FHIRRequest, FHIRResponse, FHIRSearchTypeRequest, SearchRequest},
27};
28use haste_fhir_model::r4::generated::{
29 resources::{ClientApplication, Resource, ResourceType},
30 terminology::{BoundCode, ClientapplicationGrantType},
31 types::FHIRUrl,
32};
33use haste_fhir_search::SearchEngine;
34use haste_fhir_terminology::FHIRTerminology;
35use haste_jwt::{
36 AuthorId, AuthorKind, ProjectId, TenantId, UserRole, VersionId,
37 claims::{IdTokenClaims, SubscriptionTier, UserTokenClaims},
38 scopes::{
39 LaunchSystemScope, LaunchType, LaunchTypeScope, OIDCScope, SMARTResourceScope, Scope,
40 Scopes, SmartResourceScopePermission, SmartResourceScopePermissions,
41 SmartResourceScopeUser, SmartScope,
42 },
43};
44use haste_repository::{
45 Repository,
46 admin::{ProjectModelAdmin, TenantModelAdmin},
47 types::{
48 SupportedFHIRVersions,
49 authorization_code::{
50 AuthorizationCode, AuthorizationCodeKind, AuthorizationCodeSearchClaims,
51 CreateAuthorizationCode,
52 },
53 project::CreateProject,
54 scope::{ClientId, CreateScope, ScopeSearchClaims, UserId},
55 tenant::CreateTenant,
56 user::{User, UserRole as RepoUserRole},
57 },
58};
59use jsonwebtoken::{Algorithm, Header};
60use serde::{Deserialize, Serialize};
61use serde_json::json;
62use std::{sync::Arc, time::Duration};
63use url::Url;
64
65#[derive(TypedPath)]
66#[typed_path("/token")]
67pub struct TokenPath;
68
69#[derive(Serialize, Deserialize, Debug)]
70pub enum TokenType {
71 Bearer,
72}
73
74pub static TOKEN_EXPIRATION: usize = 7200; pub static REFRESH_TOKEN_EXPIRATION: usize = 43200; #[derive(Serialize, Deserialize, Debug)]
78pub struct TokenResponse {
79 pub access_token: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 refresh_token: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub id_token: Option<String>,
84 token_type: TokenType,
85 expires_in: usize,
86 #[serde(default)]
88 scope: String,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 patient: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 encounter: Option<String>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
99 need_patient_banner: Option<bool>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 smart_style_url: Option<String>,
103}
104
105struct TokenResponseArguments {
106 user_id: String,
107 user_role: UserRole,
108 user_kind: AuthorKind,
109 client_id: String,
110 scopes: Scopes,
111 tenant: TenantId,
112 project: ProjectId,
113 membership: Option<String>,
114 access_policy_version_ids: Vec<VersionId>,
115 fhir_user: Option<FHIRUrl>,
116 patient: Option<String>,
117 encounter: Option<String>,
118 launch: Option<LaunchContext>,
120 email: Option<String>,
123 email_verified: bool,
124}
125
126struct TokenUser {
128 role: UserRole,
129 email: Option<String>,
130 email_verified: bool,
131}
132
133impl TokenUser {
134 fn of(user: Option<User>) -> TokenUser {
137 match user {
138 Some(user) => TokenUser {
139 role: match user.role {
140 RepoUserRole::Admin => UserRole::Admin,
141 RepoUserRole::Member => UserRole::Member,
142 RepoUserRole::Owner => UserRole::Owner,
143 },
144 email: user.email,
145 email_verified: user.email_verified,
146 },
147 None => TokenUser {
148 role: UserRole::Member,
149 email: None,
150 email_verified: false,
151 },
152 }
153 }
154}
155
156#[derive(Serialize, Deserialize, Debug, Clone)]
160struct LaunchContext {
161 patient: String,
162 encounter: Option<String>,
163}
164
165impl LaunchContext {
166 fn of(code: &AuthorizationCode) -> Option<LaunchContext> {
169 let launch = code.meta.as_ref()?.0.get("launch")?.clone();
170 serde_json::from_value(launch).ok()
171 }
172}
173
174pub(crate) fn is_patient_bound_scope(scope: &Scope) -> bool {
176 matches!(
177 scope,
178 Scope::SMART(SmartScope::Resource(SMARTResourceScope {
179 user: SmartResourceScopeUser::Patient,
180 ..
181 })) | Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
182 launch_type: LaunchType::Patient,
183 }))
184 )
185}
186
187struct SmartContext {
189 fhir_user: Option<FHIRUrl>,
190 patient: Option<String>,
191 encounter: Option<String>,
192 scopes: Scopes,
193}
194
195fn smart_read_only_scopes(scope: SMARTResourceScope) -> Option<SMARTResourceScope> {
197 let kept: Vec<_> = [
198 SmartResourceScopePermission::Read,
199 SmartResourceScopePermission::Search,
200 ]
201 .into_iter()
202 .filter(|permission| scope.permissions.has_permission(permission))
203 .collect();
204
205 (!kept.is_empty()).then(|| SMARTResourceScope {
206 permissions: SmartResourceScopePermissions::new(kept),
207 ..scope
208 })
209}
210
211fn issued_scope(
216 scope: Scope,
217 has_patient: bool,
218 ehr_launch: bool,
219 has_encounter: bool,
220) -> Option<Scope> {
221 match scope {
222 Scope::SMART(SmartScope::LaunchSystem(_)) => ehr_launch.then_some(scope),
223 Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
224 launch_type: LaunchType::Encounter,
225 })) => has_encounter.then_some(scope),
226 Scope::SMART(SmartScope::Resource(resource))
228 if resource.user == SmartResourceScopeUser::Patient =>
229 {
230 has_patient
231 .then(|| smart_read_only_scopes(resource))
232 .flatten()
233 .map(|resource| Scope::SMART(SmartScope::Resource(resource)))
234 }
235 Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
236 launch_type: LaunchType::Patient,
237 })) => has_patient.then_some(scope),
238 scope => Some(scope),
239 }
240}
241
242fn bind_smart_context(
252 scopes: Scopes,
253 link: Option<String>,
254 launch: Option<&LaunchContext>,
255) -> Result<SmartContext, OIDCError> {
256 let linked_patient = link
257 .as_deref()
258 .and_then(|link| link.strip_prefix("Patient/"))
259 .filter(|id| !id.is_empty())
260 .map(str::to_string);
261
262 let patient = match (launch, linked_patient) {
263 (Some(launch), Some(linked)) if linked != launch.patient => {
264 return Err(OIDCError::new(
265 OIDCErrorCode::InvalidGrant,
266 Some(
267 "The app was launched for a patient other than the signed-in patient."
268 .to_string(),
269 ),
270 None,
271 ));
272 }
273 (Some(launch), _) => Some(launch.patient.clone()),
274 (None, linked) => linked,
275 };
276 let encounter = launch.and_then(|launch| launch.encounter.clone());
277
278 let fhir_user = if scopes.contains_scope(&Scope::SMART(SmartScope::FHIRUser)) {
279 link.map(|reference| FHIRUrl {
280 value: Some(reference),
281 ..Default::default()
282 })
283 } else {
284 None
285 };
286
287 let scopes = Scopes(
288 scopes
289 .0
290 .into_iter()
291 .filter_map(|scope| {
292 issued_scope(
293 scope,
294 patient.is_some(),
295 launch.is_some(),
296 encounter.is_some(),
297 )
298 })
299 .collect(),
300 );
301
302 Ok(SmartContext {
303 fhir_user,
304 patient,
305 encounter,
306 scopes,
307 })
308}
309
310pub(crate) fn smart_style_url(
313 api_uri: &str,
314 tenant: &TenantId,
315 project: &ProjectId,
316) -> Result<String, OIDCError> {
317 let style_path = api_v1_oidc_path(tenant, project).join(
318 style::SmartStylePath
319 .to_string()
320 .strip_prefix('/')
321 .unwrap_or_default(),
322 );
323
324 Url::parse(api_uri)
325 .and_then(|u| u.join(style_path.to_str().unwrap_or_default()))
326 .map(|url| url.to_string())
327 .map_err(|e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None))
328}
329
330async fn create_token_response<Repo: Repository>(
331 config: &ServerConfig,
332 user_agent: &Option<TypedHeader<UserAgent>>,
333 repo: &Repo,
334 client_app: &ClientApplication,
335 grant_type_used: &schemas::token_body::OAuth2TokenBodyGrantType,
336 args: TokenResponseArguments,
337) -> Result<TokenResponse, OIDCError> {
338 let audience = api_fhir_root_url(&config.api_uri, &args.tenant, &args.project)
341 .map_err(|e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None))?;
342
343 let cert_provider = get_certification_provider(config);
344 let encoding_key = cert_provider.encoding_key().map_err(|_e| {
345 OIDCError::new(
346 OIDCErrorCode::ServerError,
347 Some("Failed to create access token. No encoding key available.".to_string()),
348 None,
349 )
350 })?;
351
352 let tenant = TenantModelAdmin::<CreateTenant, _, _, _, _>::read(
353 repo,
354 &TenantId::System,
355 &args.tenant.as_ref().to_string(),
356 )
357 .await
358 .map_err(|_e| {
359 OIDCError::new(
360 OIDCErrorCode::ServerError,
361 Some("Failed to retrieve tenant information.".to_string()),
362 None,
363 )
364 })?
365 .ok_or_else(|| {
366 OIDCError::new(
367 OIDCErrorCode::ServerError,
368 Some("Tenant not found.".to_string()),
369 None,
370 )
371 })?;
372
373 let project_fhir_version = TenantModelAdmin::<CreateProject, _, _, _, _>::read(
377 repo,
378 &args.tenant,
379 &args.project.as_ref().to_string(),
380 )
381 .await
382 .map_err(|_e| {
383 OIDCError::new(
384 OIDCErrorCode::ServerError,
385 Some("Failed to retrieve project.".to_string()),
386 None,
387 )
388 })?
389 .ok_or_else(|| {
390 OIDCError::new(
391 OIDCErrorCode::ServerError,
392 Some("Project not found.".to_string()),
393 None,
394 )
395 })?
396 .fhir_version;
397
398 let issued_at = chrono::Utc::now().timestamp() as usize;
399
400 let mut header = Header::new(Algorithm::RS256);
401 header.kid = Some(encoding_key.kid.clone());
402
403 let launch_granted = args.launch.is_some()
404 && args
405 .scopes
406 .contains_scope(&Scope::SMART(SmartScope::LaunchSystem(LaunchSystemScope)));
407 let encounter = args.encounter.clone().filter(|_| launch_granted);
409
410 let access_token = jsonwebtoken::encode(
411 &header,
412 &UserTokenClaims {
413 sub: AuthorId::new(args.user_id.clone()),
414 exp: issued_at + TOKEN_EXPIRATION,
415 aud: audience.to_string(),
416 scope: args.scopes.clone(),
417 tenant: args.tenant.clone(),
418 subscription_tier: SubscriptionTier::try_from(tenant.subscription_tier).map_err(
419 |e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None),
420 )?,
421 project: Some(args.project.clone()),
422 user_role: args.user_role.clone(),
423 user_id: AuthorId::new(args.user_id.clone()),
424 membership: args.membership.clone(),
425 resource_type: args.user_kind,
426 access_policy_version_ids: args.access_policy_version_ids,
427 fhir_user: args.fhir_user.clone(),
428 patient: args.patient.clone(),
429 encounter: encounter.clone(),
430 client_id: Some(args.client_id.clone()),
431 fhir_version: project_fhir_version,
432 },
433 &encoding_key.encoding_key,
434 )
435 .map_err(|_| {
436 OIDCError::new(
437 OIDCErrorCode::ServerError,
438 Some("Failed to create access token.".to_string()),
439 None,
440 )
441 })?;
442
443 let id_token = if args.scopes.contains_scope(&Scope::OIDC(OIDCScope::OpenId))
448 && *grant_type_used != schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials
449 {
450 let issuer = issuer_url(&config.api_uri, &args.tenant, &args.project)
451 .map_err(|e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None))?;
452 let email = args
454 .email
455 .clone()
456 .filter(|_| args.scopes.contains_scope(&Scope::OIDC(OIDCScope::Email)));
457
458 let id_token = jsonwebtoken::encode(
459 &header,
460 &IdTokenClaims {
461 iss: issuer.to_string(),
462 sub: args.user_id.clone(),
463 aud: args.client_id.clone(),
464 exp: issued_at + TOKEN_EXPIRATION,
465 iat: issued_at,
466 email_verified: email.as_ref().map(|_| args.email_verified),
467 email,
468 fhir_user: args.fhir_user.clone(),
469 tenant: args.tenant.clone(),
470 project: args.project.clone(),
471 user_role: args.user_role.clone(),
472 },
473 &encoding_key.encoding_key,
474 )
475 .map_err(|_| {
476 OIDCError::new(
477 OIDCErrorCode::ServerError,
478 Some("Failed to create id token.".to_string()),
479 None,
480 )
481 })?;
482
483 Some(id_token)
484 } else {
485 None
486 };
487
488 let smart_style_url = if launch_granted {
489 Some(smart_style_url(
490 &config.api_uri,
491 &args.tenant,
492 &args.project,
493 )?)
494 } else {
495 None
496 };
497
498 let mut response = TokenResponse {
499 access_token,
500 id_token,
501 expires_in: TOKEN_EXPIRATION,
502 refresh_token: None,
503 token_type: TokenType::Bearer,
504 scope: String::from(args.scopes.clone()),
505 patient: args
508 .patient
509 .clone()
510 .filter(|_| launch_granted || args.scopes.0.iter().any(is_patient_bound_scope)),
511 encounter,
512 need_patient_banner: launch_granted.then_some(true),
513 smart_style_url,
514 };
515
516 if args.scopes.0
518 .iter()
519 .find(|s| **s == Scope::OIDC(OIDCScope::OfflineAccess))
520 .is_some()
521 && client_app
522 .grantType
523 .iter()
524 .find(|gt| {
525 gt == &&ClientapplicationGrantType::refresh_token()
526 })
527 .is_some()
528 && *grant_type_used != schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials
531 {
532 let existing_refresh_tokens_for_agent =
533 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::search(
534 repo,
535 &args.tenant,
536 &args.project,
537 &AuthorizationCodeSearchClaims {
538 client_id: Some(args.client_id.clone()),
539 user_id: Some(args.user_id.clone()),
540 kind: Some(AuthorizationCodeKind::RefreshToken),
541 code: None,
542 user_agent: user_agent.as_ref().map(|ua| ua.as_str().to_string()),
543 is_expired: None,
544 },
545 )
546 .await
547 .map_err(|_| {
548 OIDCError::new(
549 OIDCErrorCode::ServerError,
550 Some("Failed to retrieve existing refresh tokens.".to_string()),
551 None,
552 )
553 })?;
554
555 for existing_token in existing_refresh_tokens_for_agent {
556 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
557 repo,
558 &args.tenant,
559 &args.project,
560 &existing_token.code,
561 )
562 .await
563 .map_err(|_e| {
564 OIDCError::new(
565 OIDCErrorCode::ServerError,
566 Some("Failed to delete existing refresh token.".to_string()),
567 None,
568 )
569 })?;
570 }
571
572 let refresh_token = ProjectModelAdmin::create(
573 repo,
574 &args.tenant,
575 &args.project,
576 CreateAuthorizationCode {
577 membership: args.membership,
578 user_id: args.user_id,
579 expires_in: Duration::from_secs(REFRESH_TOKEN_EXPIRATION as u64),
580 kind: AuthorizationCodeKind::RefreshToken,
581 client_id: Some(args.client_id),
582 pkce_code_challenge: None,
583 pkce_code_challenge_method: None,
584 redirect_uri: None,
585 meta: Some(sqlx::types::Json(match args.launch {
586 Some(launch) => json!({
589 "user_agent": user_agent.as_ref().map(|ua| ua.to_string()),
590 "launch": launch,
591 }),
592 None => json!({
593 "user_agent": user_agent.as_ref().map(|ua| ua.to_string()),
594 }),
595 })),
596 },
597 )
598 .await
599 .map_err(|_e| {
600 OIDCError::new(
601 OIDCErrorCode::ServerError,
602 Some("Failed to create refresh token.".to_string()),
603 None,
604 )
605 })?;
606
607 response.refresh_token = Some(refresh_token.code);
608 }
609
610 Ok(response)
611}
612
613async fn get_approved_scopes<Repo: Repository>(
614 repo: &Repo,
615 tenant: &TenantId,
616 project: &ProjectId,
617 user_id: UserId,
618 client_id: ClientId,
619) -> Result<Scopes, OIDCError> {
620 let approved_scopes = ProjectModelAdmin::<CreateScope, _, _, _, _>::search(
621 repo,
622 tenant,
623 project,
624 &ScopeSearchClaims {
625 user_: Some(user_id),
626 client: Some(client_id),
627 },
628 )
629 .await
630 .map_err(|_e| {
631 OIDCError::new(
632 OIDCErrorCode::ServerError,
633 Some("Failed to retrieve user's approved scopes.".to_string()),
634 None,
635 )
636 })?
637 .first()
638 .map(|s| s.scope.clone())
639 .unwrap_or_else(Default::default);
640
641 Ok(approved_scopes)
642}
643
644fn validate_client_grant_type(
645 client_app: &ClientApplication,
646 grant_type: &BoundCode<ClientapplicationGrantType>,
647) -> Result<(), OIDCError> {
648 if client_app
649 .grantType
650 .iter()
651 .find(|gt| gt == &grant_type)
652 .is_none()
653 {
654 return Err(OIDCError::new(
655 OIDCErrorCode::AccessDenied,
656 Some("Client application is not authorized for the requested grant type.".to_string()),
657 None,
658 ));
659 }
660
661 Ok(())
662}
663
664fn verify_client(
665 client_app: &ClientApplication,
666 token_request_body: &schemas::token_body::OAuth2TokenBody,
667) -> Result<(), OIDCError> {
668 match token_request_body.grant_type {
670 schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials => {
671 validate_client_grant_type(
672 client_app,
673 &ClientapplicationGrantType::client_credentials(),
674 )?;
675 require_client_secret(client_app)?;
677 }
678 schemas::token_body::OAuth2TokenBodyGrantType::RefreshToken => {
679 validate_client_grant_type(client_app, &ClientapplicationGrantType::refresh_token())?;
680 }
681 schemas::token_body::OAuth2TokenBodyGrantType::AuthorizationCode => {
682 validate_client_grant_type(
683 client_app,
684 &ClientapplicationGrantType::authorization_code(),
685 )?;
686 }
687 }
688
689 verify_client_credentials(
690 client_app,
691 token_request_body.client_id.as_deref(),
692 token_request_body.client_secret.as_deref(),
693 )
694}
695
696pub(crate) fn verify_client_credentials(
700 client_app: &ClientApplication,
701 client_id: Option<&str>,
702 client_secret: Option<&str>,
703) -> Result<(), OIDCError> {
704 if client_app.id.as_deref() != client_id {
705 return Err(OIDCError::new(
706 OIDCErrorCode::InvalidClient,
707 Some("Invalid credentials".to_string()),
708 None,
709 ));
710 }
711
712 if !constant_time::optional_str_eq(
713 client_app.secret.as_ref().and_then(|s| s.value.as_deref()),
714 client_secret,
715 ) {
716 return Err(OIDCError::new(
717 OIDCErrorCode::InvalidClient,
718 Some("Invalid credentials".to_string()),
719 None,
720 ));
721 }
722
723 Ok(())
724}
725
726pub(crate) fn require_client_secret(client_app: &ClientApplication) -> Result<(), OIDCError> {
728 let has_secret = client_app
729 .secret
730 .as_ref()
731 .and_then(|s| s.value.as_deref())
732 .is_some_and(|secret| !secret.is_empty());
733
734 if has_secret {
735 Ok(())
736 } else {
737 Err(OIDCError::new(
738 OIDCErrorCode::InvalidClient,
739 Some("Invalid credentials".to_string()),
740 None,
741 ))
742 }
743}
744
745async fn refresh_token_client_id<Repo: Repository>(
746 repo: &Repo,
747 tenant: &TenantId,
748 project: &ProjectId,
749 refresh_token: &str,
750) -> Result<String, OIDCError> {
751 let invalid = || {
752 OIDCError::new(
753 OIDCErrorCode::InvalidGrant,
754 Some("Invalid refresh token.".to_string()),
755 None,
756 )
757 };
758
759 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::search(
760 repo,
761 tenant,
762 project,
763 &AuthorizationCodeSearchClaims {
764 client_id: None,
765 code: Some(refresh_token.to_string()),
766 kind: Some(AuthorizationCodeKind::RefreshToken),
767 user_id: None,
768 user_agent: None,
769 is_expired: None,
770 },
771 )
772 .await
773 .map_err(|_| invalid())?
774 .pop()
775 .and_then(|code| code.client_id)
776 .ok_or_else(invalid)
777}
778
779async fn search_version_ids<Search: SearchEngine>(
780 search: &Search,
781 tenant: &TenantId,
782 project: &ProjectId,
783 resource_type: ResourceType,
784 parameter: &str,
785 values: Vec<String>,
786) -> Result<Vec<VersionId>, OIDCError> {
787 let result = search
788 .search(
789 &SupportedFHIRVersions::R4,
790 tenant,
791 project,
792 &SearchRequest::Type(FHIRSearchTypeRequest {
793 resource_type,
794 parameters: vec![(parameter.to_string(), values)].into(),
795 }),
796 None,
797 )
798 .await
799 .map_err(|_e| {
800 OIDCError::new(
801 OIDCErrorCode::ServerError,
802 Some("Failed to search for user's access policies.".to_string()),
803 None,
804 )
805 })?;
806
807 Ok(result.entries.into_iter().map(|e| e.version_id).collect())
808}
809
810async fn find_users_access_policy_version_ids<Repo: Repository, Search: SearchEngine>(
816 repo: &Repo,
817 search: &Search,
818 tenant: &TenantId,
819 project: &ProjectId,
820 user_id: &str,
821 user_type: &ResourceType,
822) -> Result<Vec<VersionId>, OIDCError> {
823 let user_reference = format!("{}/{}", user_type.as_ref(), user_id);
824
825 let assignment_version_ids = search_version_ids(
826 search,
827 tenant,
828 project,
829 ResourceType::AccessPolicyV2Assignment,
830 "link",
831 vec![user_reference.clone()],
832 )
833 .await?;
834
835 let assigned_policy_ids = repo
836 .read_by_version_ids(
837 tenant,
838 project,
839 &assignment_version_ids.iter().collect::<Vec<_>>(),
840 haste_repository::fhir::CachePolicy::NoCache,
841 )
842 .await
843 .map_err(|_e| {
844 OIDCError::new(
845 OIDCErrorCode::ServerError,
846 Some("Failed to read user's access policy assignments.".to_string()),
847 None,
848 )
849 })?
850 .into_iter()
851 .filter_map(|resource| match resource {
852 Resource::AccessPolicyV2Assignment(assignment) => assignment
853 .accessPolicy
854 .reference
855 .and_then(|reference| reference.value)
856 .and_then(|reference| {
857 reference
858 .strip_prefix("AccessPolicyV2/")
859 .map(ToString::to_string)
860 }),
861 _ => None,
862 })
863 .collect::<Vec<_>>();
864
865 let mut version_ids = if assigned_policy_ids.is_empty() {
866 vec![]
867 } else {
868 search_version_ids(
869 search,
870 tenant,
871 project,
872 ResourceType::AccessPolicyV2,
873 "_id",
874 assigned_policy_ids,
875 )
876 .await?
877 };
878
879 version_ids.extend(
881 search_version_ids(
882 search,
883 tenant,
884 project,
885 ResourceType::AccessPolicyV2,
886 "link",
887 vec![user_reference],
888 )
889 .await?,
890 );
891
892 let mut seen = std::collections::HashSet::new();
893 version_ids.retain(|version_id| seen.insert(version_id.clone()));
894
895 Ok(version_ids)
896}
897
898async fn membership_link<
900 Repo: Repository + Send + Sync,
901 Search: SearchEngine + Send + Sync,
902 Terminology: FHIRTerminology + Send + Sync,
903>(
904 state: &ServerState<Repo, Search, Terminology>,
905 tenant: &TenantId,
906 project: &ProjectId,
907 membership_id: Option<&str>,
908) -> Result<Option<String>, OIDCError> {
909 let Some(membership_id) = membership_id else {
910 return Ok(None);
911 };
912
913 let response = state
914 .fhir_client
915 .request(
916 Arc::new(crate::fhir_client::ServerCTX::system(
917 tenant.clone(),
918 project.clone(),
919 state.fhir_client.clone(),
920 state.rate_limit.clone(),
921 )),
922 FHIRRequest::Read(FHIRReadRequest {
923 resource_type: ResourceType::Membership,
924 id: membership_id.to_string(),
925 }),
926 )
927 .await
928 .map_err(|_| {
929 OIDCError::new(
930 OIDCErrorCode::ServerError,
931 Some("Failed to retrieve membership resource for its link.".to_string()),
932 None,
933 )
934 })?;
935
936 let membership_link_reference = match response {
937 FHIRResponse::Read(read_response) => {
938 read_response.resource.and_then(|resource| match resource {
939 Resource::Membership(membership) => membership
940 .link
941 .as_ref()
942 .and_then(|reference| reference.reference.as_ref())
943 .and_then(|reference| reference.value.clone()),
944 _ => None,
945 })
946 }
947 _ => None,
948 };
949
950 Ok(membership_link_reference)
951}
952
953#[derive(PartialEq, Eq)]
954pub enum ClientCredentialsMethod {
955 BasicAuth,
956 Body,
957}
958
959pub async fn client_credentials_to_token_response<
960 Repo: Repository + Send + Sync,
961 Search: SearchEngine + Send + Sync,
962 Terminology: FHIRTerminology + Send + Sync,
963>(
964 state: &ServerState<Repo, Search, Terminology>,
965 tenant: &TenantId,
966 project: &ProjectId,
967 user_agent: &Option<TypedHeader<UserAgent>>,
968 token_body: &schemas::token_body::OAuth2TokenBody,
969 method: ClientCredentialsMethod,
970) -> Result<TokenResponse, OIDCError> {
971 let Some(client_id) = &token_body.client_id else {
972 return Err(OIDCError::new(
973 OIDCErrorCode::InvalidRequest,
974 Some("client_id is required for client_credentials grant type.".to_string()),
975 None,
976 ));
977 };
978
979 let client_app =
980 find_client_app(state, tenant.clone(), project.clone(), client_id.clone()).await?;
981
982 verify_client(&client_app, token_body)?;
983
984 if method == ClientCredentialsMethod::BasicAuth {
986 validate_client_grant_type(&client_app, &ClientapplicationGrantType::basic_auth())?;
987 }
988
989 let client_app_scopes = client_app
990 .scope
991 .as_ref()
992 .and_then(|s| s.value.as_deref())
993 .unwrap_or_default();
994
995 let requested = token_body
996 .scope
997 .clone()
998 .unwrap_or_else(|| client_app_scopes.to_string());
999
1000 let requested_scopes = Scopes::try_from(requested.as_str()).map_err(|_| {
1001 OIDCError::new(
1002 OIDCErrorCode::InvalidScope,
1003 Some("Invalid scope requested.".to_string()),
1004 None,
1005 )
1006 })?;
1007
1008 verify_requested_scope_is_subset(
1009 &requested_scopes,
1010 &Scopes::try_from(client_app_scopes).map_err(|_| {
1011 OIDCError::new(
1012 OIDCErrorCode::InvalidScope,
1013 Some("Client application's configured scopes are invalid.".to_string()),
1014 None,
1015 )
1016 })?,
1017 )?;
1018
1019 let response = create_token_response(
1020 state.config.as_ref(),
1021 user_agent,
1022 &*state.repo,
1023 &client_app,
1024 &token_body.grant_type,
1025 TokenResponseArguments {
1026 user_id: client_app.id.clone().unwrap_or_default(),
1027 user_role: UserRole::Member,
1028 email: None,
1029 email_verified: false,
1030 user_kind: AuthorKind::ClientApplication,
1031 client_id: client_app.id.clone().unwrap_or_default(),
1032 scopes: bind_smart_context(requested_scopes, None, None)?.scopes,
1034 tenant: tenant.clone(),
1035 project: project.clone(),
1036 membership: None,
1037 fhir_user: None,
1038 patient: None,
1039 encounter: None,
1040 launch: None,
1041 access_policy_version_ids: find_users_access_policy_version_ids(
1042 state.repo.as_ref(),
1043 state.search.as_ref(),
1044 tenant,
1045 project,
1046 client_id,
1047 &ResourceType::ClientApplication,
1048 )
1049 .await?,
1050 },
1051 )
1052 .await?;
1053
1054 Ok(response)
1055}
1056
1057pub async fn token<
1058 Repo: Repository + Send + Sync,
1059 Search: SearchEngine + Send + Sync,
1060 Terminology: FHIRTerminology + Send + Sync,
1061>(
1062 _: TokenPath,
1063 user_agent: Option<TypedHeader<UserAgent>>,
1064 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
1065 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
1066 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
1067 OAuthTokenBody(token_body): OAuthTokenBody,
1068) -> Result<Response, OIDCError> {
1069 match &token_body.grant_type {
1070 schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials => {
1071 let response = client_credentials_to_token_response(
1072 &*state,
1073 &tenant,
1074 &project,
1075 &user_agent,
1076 &token_body,
1077 ClientCredentialsMethod::Body,
1078 )
1079 .await?;
1080
1081 Ok(Json(response).into_response())
1082 }
1083 schemas::token_body::OAuth2TokenBodyGrantType::RefreshToken => {
1084 let refresh_token = &token_body.refresh_token.as_ref().ok_or_else(|| {
1085 OIDCError::new(
1086 OIDCErrorCode::InvalidRequest,
1087 Some("refresh_token is required for refresh_token grant type.".to_string()),
1088 None,
1089 )
1090 })?;
1091
1092 let client_id = match &token_body.client_id {
1097 Some(client_id) => client_id.clone(),
1098 None => {
1099 refresh_token_client_id(&*state.repo, &tenant, &project, refresh_token).await?
1100 }
1101 };
1102 let token_body = schemas::token_body::OAuth2TokenBody {
1103 client_id: Some(client_id.clone()),
1104 ..token_body.clone()
1105 };
1106 let client_id = &client_id;
1107
1108 let client_app =
1109 find_client_app(&state, tenant.clone(), project.clone(), client_id.clone()).await?;
1110
1111 verify_client(&client_app, &token_body)?;
1112
1113 let code = code_verification::retrieve_and_verify_code(
1114 &*state.repo,
1115 &tenant,
1116 &project,
1117 &client_app,
1118 AuthorizationCodeKind::RefreshToken,
1119 refresh_token,
1120 None,
1121 None,
1122 )
1123 .await
1124 .map_err(|_e| {
1125 OIDCError::new(
1126 OIDCErrorCode::InvalidGrant,
1127 Some("Invalid refresh token.".to_string()),
1128 None,
1129 )
1130 })?;
1131
1132 if code.kind != AuthorizationCodeKind::RefreshToken {
1133 return Err(OIDCError::new(
1134 OIDCErrorCode::InvalidGrant,
1135 Some("Invalid refresh token.".to_string()),
1136 None,
1137 ));
1138 }
1139
1140 if code.is_expired.unwrap_or(true) {
1141 return Err(OIDCError::new(
1142 OIDCErrorCode::InvalidGrant,
1143 Some("Refresh token has expired.".to_string()),
1144 None,
1145 ));
1146 }
1147
1148 let approved_scopes = get_approved_scopes(
1149 &*state.repo,
1150 &tenant,
1151 &project,
1152 UserId::new(code.user_id.clone()),
1153 ClientId::new(client_id.clone()),
1154 )
1155 .await?;
1156
1157 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
1158 &*state.repo,
1159 &tenant,
1160 &project,
1161 refresh_token,
1162 )
1163 .await
1164 .map_err(|_e| {
1165 OIDCError::new(
1166 OIDCErrorCode::ServerError,
1167 Some("Failed to delete used refresh token.".to_string()),
1168 None,
1169 )
1170 })?;
1171
1172 let user =
1173 TenantModelAdmin::<_, User, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
1174 .await
1175 .map_err(|_e| {
1176 OIDCError::new(
1177 OIDCErrorCode::ServerError,
1178 Some("Failed to retrieve user.".to_string()),
1179 None,
1180 )
1181 })?;
1182 let token_user = TokenUser::of(user);
1183
1184 let link =
1185 membership_link(&state, &tenant, &project, code.membership.as_deref()).await?;
1186 let launch = LaunchContext::of(&code);
1189 let SmartContext {
1190 fhir_user,
1191 patient,
1192 encounter,
1193 scopes: approved_scopes,
1194 } = bind_smart_context(approved_scopes, link, launch.as_ref())?;
1195
1196 let response = create_token_response(
1197 state.config.as_ref(),
1198 &user_agent,
1199 &*state.repo,
1200 &client_app,
1201 &token_body.grant_type,
1202 TokenResponseArguments {
1203 user_id: code.user_id,
1204 user_kind: AuthorKind::Membership,
1205 user_role: token_user.role,
1206 email: token_user.email,
1207 email_verified: token_user.email_verified,
1208 client_id: client_id.clone(),
1209 scopes: approved_scopes.clone(),
1210 tenant: tenant.clone(),
1211 project: project.clone(),
1212 access_policy_version_ids: match code.membership.as_ref() {
1213 Some(membership) => {
1214 find_users_access_policy_version_ids(
1215 state.repo.as_ref(),
1216 state.search.as_ref(),
1217 &tenant,
1218 &project,
1219 membership,
1220 &ResourceType::Membership,
1221 )
1222 .await?
1223 }
1224 None => vec![],
1225 },
1226 membership: code.membership.clone(),
1227 fhir_user,
1228 patient,
1229 encounter,
1230 launch,
1231 },
1232 )
1233 .await?;
1234
1235 Ok(Json(response).into_response())
1236 }
1237 schemas::token_body::OAuth2TokenBodyGrantType::AuthorizationCode => {
1238 let Some(client_id) = &token_body.client_id else {
1239 return Err(OIDCError::new(
1240 OIDCErrorCode::InvalidRequest,
1241 Some("client_id is required for authorization_code grant type.".to_string()),
1242 None,
1243 ));
1244 };
1245
1246 let code = token_body.code.as_ref().ok_or_else(|| {
1247 OIDCError::new(
1248 OIDCErrorCode::InvalidRequest,
1249 Some("code is required for authorization_code grant type.".to_string()),
1250 None,
1251 )
1252 })?;
1253 let code_verifier = token_body.code_verifier.as_ref().ok_or_else(|| {
1254 OIDCError::new(
1255 OIDCErrorCode::InvalidRequest,
1256 Some(
1257 "code_verifier is required for authorization_code grant type.".to_string(),
1258 ),
1259 None,
1260 )
1261 })?;
1262 let redirect_uri = token_body.redirect_uri.as_ref().ok_or_else(|| {
1263 OIDCError::new(
1264 OIDCErrorCode::InvalidRequest,
1265 Some("redirect_uri is required for authorization_code grant type.".to_string()),
1266 None,
1267 )
1268 })?;
1269
1270 let client_app =
1271 find_client_app(&state, tenant.clone(), project.clone(), client_id.clone()).await?;
1272
1273 verify_client(&client_app, &token_body)?;
1274
1275 let code = code_verification::retrieve_and_verify_code(
1276 &*state.repo,
1277 &tenant,
1278 &project,
1279 &client_app,
1280 AuthorizationCodeKind::OAuth2CodeGrant,
1281 code,
1282 Some(redirect_uri),
1283 Some(code_verifier),
1284 )
1285 .await
1286 .map_err(|_| {
1287 OIDCError::new(
1288 OIDCErrorCode::InvalidGrant,
1289 Some("Invalid authorization code.".to_string()),
1290 None,
1291 )
1292 })?;
1293
1294 if code.kind != AuthorizationCodeKind::OAuth2CodeGrant {
1295 return Err(OIDCError::new(
1296 OIDCErrorCode::InvalidGrant,
1297 Some("Invalid authorization code.".to_string()),
1298 None,
1299 ));
1300 }
1301
1302 if code.is_expired.unwrap_or(true) {
1303 return Err(OIDCError::new(
1304 OIDCErrorCode::InvalidGrant,
1305 Some("Authorization code has expired.".to_string()),
1306 None,
1307 ));
1308 }
1309
1310 let approved_scopes = get_approved_scopes(
1311 &*state.repo,
1312 &tenant,
1313 &project,
1314 UserId::new(code.user_id.clone()),
1315 ClientId::new(client_id.clone()),
1316 )
1317 .await?;
1318
1319 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
1321 &*state.repo,
1322 &tenant,
1323 &project,
1324 &code.code,
1325 )
1326 .await
1327 .map_err(|_e| {
1328 OIDCError::new(
1329 OIDCErrorCode::ServerError,
1330 Some("Failed to delete used authorization code.".to_string()),
1331 None,
1332 )
1333 })?;
1334
1335 let user =
1336 TenantModelAdmin::<_, User, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
1337 .await
1338 .map_err(|_e| {
1339 OIDCError::new(
1340 OIDCErrorCode::ServerError,
1341 Some("Failed to retrieve user.".to_string()),
1342 None,
1343 )
1344 })?;
1345 let token_user = TokenUser::of(user);
1346
1347 let link =
1348 membership_link(&state, &tenant, &project, code.membership.as_deref()).await?;
1349 let launch = LaunchContext::of(&code);
1351 let SmartContext {
1352 fhir_user,
1353 patient,
1354 encounter,
1355 scopes: approved_scopes,
1356 } = bind_smart_context(approved_scopes, link, launch.as_ref())?;
1357
1358 let response = create_token_response(
1359 state.config.as_ref(),
1360 &user_agent,
1361 &*state.repo,
1362 &client_app,
1363 &token_body.grant_type,
1364 TokenResponseArguments {
1365 user_id: code.user_id,
1366 user_kind: AuthorKind::Membership,
1367 user_role: token_user.role,
1368 email: token_user.email,
1369 email_verified: token_user.email_verified,
1370 client_id: client_id.clone(),
1371 scopes: approved_scopes.clone(),
1372 tenant: tenant.clone(),
1373 project: project.clone(),
1374 access_policy_version_ids: match code.membership.as_ref() {
1375 Some(membership) => {
1376 find_users_access_policy_version_ids(
1377 state.repo.as_ref(),
1378 state.search.as_ref(),
1379 &tenant,
1380 &project,
1381 membership,
1382 &ResourceType::Membership,
1383 )
1384 .await?
1385 }
1386 None => vec![],
1387 },
1388 membership: code.membership.clone(),
1389 fhir_user,
1390 patient,
1391 encounter,
1392 launch,
1393 },
1394 )
1395 .await?;
1396
1397 Ok(Json(response).into_response())
1398 }
1399 }
1400}