1use crate::{
2 auth_n::{
3 certificates::get_certification_provider,
4 oidc::{
5 code_verification,
6 error::{OIDCError, OIDCErrorCode},
7 extract::{body::ParsedBody, client_app::find_client_app},
8 routes::scope::verify_requested_scope_is_subset,
9 schemas,
10 },
11 },
12 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
13 services::AppState,
14};
15use axum::{
16 Json,
17 extract::State,
18 response::{IntoResponse, Response},
19};
20use axum_extra::{TypedHeader, extract::Cached, headers::UserAgent, routing::TypedPath};
21use haste_fhir_client::{
22 request::{FHIRSearchTypeRequest, SearchRequest},
23 url::{Parameter, ParsedParameter, ParsedParameters},
24};
25use haste_fhir_model::r4::generated::{
26 resources::{ClientApplication, ResourceType},
27 terminology::ClientapplicationGrantType,
28};
29use haste_fhir_search::SearchEngine;
30use haste_fhir_terminology::FHIRTerminology;
31use haste_jwt::{
32 AuthorId, AuthorKind, ProjectId, TenantId, UserRole, VersionId,
33 claims::{SubscriptionTier, UserTokenClaims},
34 scopes::{OIDCScope, Scope, Scopes},
35};
36use haste_repository::{
37 Repository,
38 admin::{ProjectAuthAdmin, TenantAuthAdmin},
39 types::{
40 SupportedFHIRVersions,
41 authorization_code::{
42 AuthorizationCodeKind, AuthorizationCodeSearchClaims, CreateAuthorizationCode,
43 },
44 scope::{ClientId, CreateScope, ScopeSearchClaims, UserId},
45 tenant::CreateTenant,
46 user::{User, UserRole as RepoUserRole},
47 },
48};
49use jsonwebtoken::{Algorithm, Header};
50use serde::{Deserialize, Serialize};
51use serde_json::json;
52use std::{sync::Arc, time::Duration};
53
54#[derive(TypedPath)]
55#[typed_path("/token")]
56pub struct TokenPath;
57
58#[derive(Serialize, Deserialize, Debug)]
59pub enum TokenType {
60 Bearer,
61}
62
63pub static TOKEN_EXPIRATION: usize = 7200; pub static REFRESH_TOKEN_EXPIRATION: usize = 43200; #[derive(Serialize, Deserialize, Debug)]
67pub struct TokenResponse {
68 pub access_token: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 refresh_token: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub id_token: Option<String>,
73 token_type: TokenType,
74 expires_in: usize,
75}
76
77struct TokenResponseArguments {
78 user_id: String,
79 user_role: UserRole,
80 user_kind: AuthorKind,
81 client_id: String,
82 scopes: Scopes,
83 tenant: TenantId,
84 project: ProjectId,
85 membership: Option<String>,
86 access_policy_version_ids: Vec<VersionId>,
87}
88
89async fn create_token_response<Repo: Repository>(
90 user_agent: &Option<TypedHeader<UserAgent>>,
91 repo: &Repo,
92 client_app: &ClientApplication,
93 grant_type_used: &schemas::token_body::OAuth2TokenBodyGrantType,
94 args: TokenResponseArguments,
95) -> Result<TokenResponse, OIDCError> {
96 let cert_provider = get_certification_provider();
97 let encoding_key = cert_provider.encoding_key().map_err(|_e| {
98 OIDCError::new(
99 OIDCErrorCode::ServerError,
100 Some("Failed to create access token. No encoding key available.".to_string()),
101 None,
102 )
103 })?;
104
105 let tenant = TenantAuthAdmin::<CreateTenant, _, _, _, _>::read(
106 repo,
107 &TenantId::System,
108 &args.tenant.as_ref().to_string(),
109 )
110 .await
111 .map_err(|_e| {
112 OIDCError::new(
113 OIDCErrorCode::ServerError,
114 Some("Failed to retrieve tenant information.".to_string()),
115 None,
116 )
117 })?
118 .ok_or_else(|| {
119 OIDCError::new(
120 OIDCErrorCode::ServerError,
121 Some("Tenant not found.".to_string()),
122 None,
123 )
124 })?;
125
126 let mut header = Header::new(Algorithm::RS256);
127 header.kid = Some(encoding_key.kid.clone());
128
129 let token = jsonwebtoken::encode(
130 &header,
131 &UserTokenClaims {
132 sub: AuthorId::new(args.user_id.clone()),
133 exp: (chrono::Utc::now() + chrono::Duration::seconds(TOKEN_EXPIRATION as i64))
134 .timestamp() as usize,
135 aud: args.client_id.clone(),
136 scope: args.scopes.clone(),
137 tenant: args.tenant.clone(),
138 subscription_tier: SubscriptionTier::try_from(tenant.subscription_tier).map_err(
139 |e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None),
140 )?,
141 project: Some(args.project.clone()),
142 user_role: args.user_role,
143 user_id: AuthorId::new(args.user_id.clone()),
144 membership: args.membership.clone(),
145 resource_type: args.user_kind,
146 access_policy_version_ids: args.access_policy_version_ids,
147 },
148 &encoding_key.encoding_key,
149 )
150 .map_err(|_| {
151 OIDCError::new(
152 OIDCErrorCode::ServerError,
153 Some("Failed to create access token.".to_string()),
154 None,
155 )
156 })?;
157
158 let mut response = TokenResponse {
159 access_token: token.clone(),
160 id_token: None,
161 expires_in: TOKEN_EXPIRATION,
162 refresh_token: None,
163 token_type: TokenType::Bearer,
164 };
165
166 if args.scopes.contains_scope(&Scope::OIDC(OIDCScope::OpenId)) {
167 response.id_token = Some(token);
168 }
169
170 if (&args.scopes.0)
172 .iter()
173 .find(|s| **s == Scope::OIDC(OIDCScope::OfflineAccess))
174 .is_some()
175 && client_app
176 .grantType
177 .iter()
178 .find(|gt| {
179 let discriminator = std::mem::discriminant(gt.as_ref());
180 let offline_discriminator =
181 std::mem::discriminant(&ClientapplicationGrantType::Refresh_token(None));
182 discriminator == offline_discriminator
183 })
184 .is_some()
185 && *grant_type_used != schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials
188 {
189 let existing_refresh_tokens_for_agent =
190 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::search(
191 repo,
192 &args.tenant,
193 &args.project,
194 &AuthorizationCodeSearchClaims {
195 client_id: Some(args.client_id.clone()),
196 user_id: Some(args.user_id.clone()),
197 kind: Some(AuthorizationCodeKind::RefreshToken),
198 code: None,
199 user_agent: user_agent.as_ref().map(|ua| ua.as_str().to_string()),
200 is_expired: None,
201 },
202 )
203 .await
204 .map_err(|_| {
205 OIDCError::new(
206 OIDCErrorCode::ServerError,
207 Some("Failed to retrieve existing refresh tokens.".to_string()),
208 None,
209 )
210 })?;
211
212 for existing_token in existing_refresh_tokens_for_agent {
213 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
214 repo,
215 &args.tenant,
216 &args.project,
217 &existing_token.code,
218 )
219 .await
220 .map_err(|_e| {
221 OIDCError::new(
222 OIDCErrorCode::ServerError,
223 Some("Failed to delete existing refresh token.".to_string()),
224 None,
225 )
226 })?;
227 }
228
229 let refresh_token = ProjectAuthAdmin::create(
230 repo,
231 &args.tenant,
232 &args.project,
233 CreateAuthorizationCode {
234 membership: args.membership,
235 user_id: args.user_id,
236 expires_in: Duration::from_secs(REFRESH_TOKEN_EXPIRATION as u64),
237 kind: AuthorizationCodeKind::RefreshToken,
238 client_id: Some(args.client_id),
239 pkce_code_challenge: None,
240 pkce_code_challenge_method: None,
241 redirect_uri: None,
242 meta: Some(sqlx::types::Json(json!({
243 "user_agent": user_agent.as_ref().map(|ua| ua.to_string()),
244 }))),
245 },
246 )
247 .await
248 .map_err(|_e| {
249 OIDCError::new(
250 OIDCErrorCode::ServerError,
251 Some("Failed to create refresh token.".to_string()),
252 None,
253 )
254 })?;
255
256 response.refresh_token = Some(refresh_token.code);
257 }
258
259 Ok(response)
260}
261
262async fn get_approved_scopes<Repo: Repository>(
263 repo: &Repo,
264 tenant: &TenantId,
265 project: &ProjectId,
266 user_id: UserId,
267 client_id: ClientId,
268) -> Result<Scopes, OIDCError> {
269 let approved_scopes = ProjectAuthAdmin::<CreateScope, _, _, _, _>::search(
270 repo,
271 &tenant,
272 &project,
273 &ScopeSearchClaims {
274 user_: Some(user_id),
275 client: Some(client_id),
276 },
277 )
278 .await
279 .map_err(|_e| {
280 OIDCError::new(
281 OIDCErrorCode::ServerError,
282 Some("Failed to retrieve user's approved scopes.".to_string()),
283 None,
284 )
285 })?
286 .get(0)
287 .map(|s| s.scope.clone())
288 .unwrap_or_else(|| Default::default());
289
290 Ok(approved_scopes)
291}
292
293fn validate_client_grant_type(
294 client_app: &ClientApplication,
295 grant_type: &ClientapplicationGrantType,
296) -> Result<(), OIDCError> {
297 if client_app
298 .grantType
299 .iter()
300 .find(|gt| {
301 let discriminator = std::mem::discriminant(gt.as_ref());
302 let requested_discriminator = std::mem::discriminant(grant_type);
303 discriminator == requested_discriminator
304 })
305 .is_none()
306 {
307 return Err(OIDCError::new(
308 OIDCErrorCode::AccessDenied,
309 Some("Client application is not authorized for the requested grant type.".to_string()),
310 None,
311 ));
312 }
313
314 Ok(())
315}
316
317fn verify_client(
318 client_app: &ClientApplication,
319 token_request_body: &schemas::token_body::OAuth2TokenBody,
320) -> Result<(), OIDCError> {
321 match token_request_body.grant_type {
323 schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials => {
324 validate_client_grant_type(
325 client_app,
326 &ClientapplicationGrantType::Client_credentials(None),
327 )?;
328 }
329 schemas::token_body::OAuth2TokenBodyGrantType::RefreshToken => {
330 validate_client_grant_type(
331 client_app,
332 &ClientapplicationGrantType::Refresh_token(None),
333 )?;
334 }
335 schemas::token_body::OAuth2TokenBodyGrantType::AuthorizationCode => {
336 validate_client_grant_type(
337 client_app,
338 &ClientapplicationGrantType::Authorization_code(None),
339 )?;
340 }
341 }
342
343 if client_app.id.as_ref() != Some(&token_request_body.client_id) {
344 return Err(OIDCError::new(
345 OIDCErrorCode::AccessDenied,
346 Some("Invalid credentials".to_string()),
347 None,
348 ));
349 }
350
351 if client_app
352 .secret
353 .as_ref()
354 .and_then(|s| s.value.as_ref().map(String::as_str))
355 != token_request_body
356 .client_secret
357 .as_ref()
358 .map(String::as_str)
359 {
360 return Err(OIDCError::new(
361 OIDCErrorCode::AccessDenied,
362 Some("Invalid credentials".to_string()),
363 None,
364 ));
365 }
366
367 Ok(())
368}
369
370async fn find_users_access_policy_version_ids<Search: SearchEngine>(
371 search: &Search,
372 tenant: &TenantId,
373 project: &ProjectId,
374 user_id: &str,
375 user_type: &ResourceType,
376) -> Result<Vec<VersionId>, OIDCError> {
377 let access_policies = search
378 .search(
379 &SupportedFHIRVersions::R4,
380 &tenant,
381 &project,
382 &SearchRequest::Type(FHIRSearchTypeRequest {
383 resource_type: ResourceType::AccessPolicyV2,
384 parameters: ParsedParameters::new(vec![ParsedParameter::Resource(Parameter {
385 name: "link".to_string(),
386 value: vec![format!("{}/{}", user_type.as_ref(), user_id)],
387 modifier: None,
388 chains: None,
389 })]),
390 }),
391 None,
392 )
393 .await
394 .map_err(|_e| {
395 OIDCError::new(
396 OIDCErrorCode::ServerError,
397 Some("Failed to search for user's access policies.".to_string()),
398 None,
399 )
400 })?;
401
402 Ok(access_policies
403 .entries
404 .into_iter()
405 .map(|ap| ap.version_id)
406 .collect())
407}
408
409#[derive(PartialEq, Eq)]
410pub enum ClientCredentialsMethod {
411 BasicAuth,
412 Body,
413}
414
415pub async fn client_credentials_to_token_response<
416 Repo: Repository + Send + Sync,
417 Search: SearchEngine + Send + Sync,
418 Terminology: FHIRTerminology + Send + Sync,
419>(
420 state: &AppState<Repo, Search, Terminology>,
421 tenant: &TenantId,
422 project: &ProjectId,
423 user_agent: &Option<TypedHeader<UserAgent>>,
424 token_body: &schemas::token_body::OAuth2TokenBody,
425 method: ClientCredentialsMethod,
426) -> Result<TokenResponse, OIDCError> {
427 let client_id = &token_body.client_id;
428 let client_app =
429 find_client_app(state, tenant.clone(), project.clone(), client_id.clone()).await?;
430
431 verify_client(&client_app, &token_body)?;
432
433 if method == ClientCredentialsMethod::BasicAuth {
435 validate_client_grant_type(&client_app, &ClientapplicationGrantType::Basic_auth(None))?;
436 }
437
438 let client_app_scopes = client_app
439 .scope
440 .as_ref()
441 .and_then(|s| s.value.as_ref().map(String::as_str))
442 .unwrap_or_default();
443
444 let requested_scopes = Scopes::from(
445 token_body
446 .scope
447 .clone()
448 .unwrap_or_else(|| client_app_scopes.to_string()),
449 );
450
451 verify_requested_scope_is_subset(
452 &requested_scopes,
453 &Scopes::try_from(client_app_scopes).map_err(|_| {
454 OIDCError::new(
455 OIDCErrorCode::InvalidScope,
456 Some("Client application's configured scopes are invalid.".to_string()),
457 None,
458 )
459 })?,
460 )?;
461
462 let response = create_token_response(
463 user_agent,
464 &*state.repo,
465 &client_app,
466 &token_body.grant_type,
467 TokenResponseArguments {
468 user_id: client_app.id.clone().unwrap_or_default(),
469 user_role: UserRole::Member,
470 user_kind: AuthorKind::ClientApplication,
471 client_id: client_app.id.clone().unwrap_or_default(),
472 scopes: requested_scopes,
473 tenant: tenant.clone(),
474 project: project.clone(),
475 membership: None,
476 access_policy_version_ids: find_users_access_policy_version_ids(
477 state.search.as_ref(),
478 &tenant,
479 &project,
480 client_id,
481 &ResourceType::ClientApplication,
482 )
483 .await?,
484 },
485 )
486 .await?;
487
488 Ok(response)
489}
490
491pub async fn token<
492 Repo: Repository + Send + Sync,
493 Search: SearchEngine + Send + Sync,
494 Terminology: FHIRTerminology + Send + Sync,
495>(
496 _: TokenPath,
497 user_agent: Option<TypedHeader<UserAgent>>,
498 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
499 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
500 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
501 ParsedBody(token_body): ParsedBody<schemas::token_body::OAuth2TokenBody>,
502) -> Result<Response, OIDCError> {
503 match &token_body.grant_type {
504 schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials => {
505 let response = client_credentials_to_token_response(
506 &*state,
507 &tenant,
508 &project,
509 &user_agent,
510 &token_body,
511 ClientCredentialsMethod::Body,
512 )
513 .await?;
514
515 Ok(Json(response).into_response())
516 }
517 schemas::token_body::OAuth2TokenBodyGrantType::RefreshToken => {
518 let client_id = &token_body.client_id;
519 let refresh_token = &token_body.refresh_token.as_ref().ok_or_else(|| {
520 OIDCError::new(
521 OIDCErrorCode::InvalidRequest,
522 Some("refresh_token is required for refresh_token grant type.".to_string()),
523 token_body.redirect_uri.clone(),
524 )
525 })?;
526
527 let client_app =
528 find_client_app(&state, tenant.clone(), project.clone(), client_id.clone()).await?;
529
530 verify_client(&client_app, &token_body)?;
531
532 let code = code_verification::retrieve_and_verify_code(
533 &*state.repo,
534 &tenant,
535 &project,
536 &client_app,
537 AuthorizationCodeKind::RefreshToken,
538 &refresh_token,
539 None,
540 None,
541 )
542 .await
543 .map_err(|_e| {
544 OIDCError::new(
545 OIDCErrorCode::InvalidGrant,
546 Some("Invalid refresh token.".to_string()),
547 token_body.redirect_uri.clone(),
548 )
549 })?;
550
551 if code.kind != AuthorizationCodeKind::RefreshToken {
552 return Err(OIDCError::new(
553 OIDCErrorCode::InvalidGrant,
554 Some("Invalid refresh token.".to_string()),
555 token_body.redirect_uri.clone(),
556 ));
557 }
558
559 if code.is_expired.unwrap_or(true) {
560 return Err(OIDCError::new(
561 OIDCErrorCode::InvalidGrant,
562 Some("Refresh token has expired.".to_string()),
563 token_body.redirect_uri.clone(),
564 ));
565 }
566
567 let approved_scopes = get_approved_scopes(
568 &*state.repo,
569 &tenant,
570 &project,
571 UserId::new(code.user_id.clone()),
572 ClientId::new(client_id.clone()),
573 )
574 .await?;
575
576 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
577 &*state.repo,
578 &tenant,
579 &project,
580 &refresh_token,
581 )
582 .await
583 .map_err(|_e| {
584 OIDCError::new(
585 OIDCErrorCode::ServerError,
586 Some("Failed to delete used refresh token.".to_string()),
587 token_body.redirect_uri.clone(),
588 )
589 })?;
590
591 let user =
592 TenantAuthAdmin::<_, User, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
593 .await
594 .map_err(|_e| {
595 OIDCError::new(
596 OIDCErrorCode::ServerError,
597 Some("Failed to retrieve user.".to_string()),
598 token_body.redirect_uri.clone(),
599 )
600 })?;
601
602 let response = create_token_response(
603 &user_agent,
604 &*state.repo,
605 &client_app,
606 &token_body.grant_type,
607 TokenResponseArguments {
608 user_id: code.user_id,
609 user_kind: AuthorKind::Membership,
610 user_role: match user.map(|u| u.role) {
611 Some(RepoUserRole::Admin) => UserRole::Admin,
612 Some(RepoUserRole::Member) => UserRole::Member,
613 Some(RepoUserRole::Owner) => UserRole::Owner,
614 None => UserRole::Member,
615 },
616 client_id: client_id.clone(),
617 scopes: approved_scopes.clone(),
618 tenant: tenant.clone(),
619 project: project.clone(),
620 access_policy_version_ids: match code.membership.as_ref() {
621 Some(membership) => {
622 find_users_access_policy_version_ids(
623 state.search.as_ref(),
624 &tenant,
625 &project,
626 &membership,
627 &ResourceType::Membership,
628 )
629 .await?
630 }
631 None => vec![],
632 },
633 membership: code.membership,
634 },
635 )
636 .await?;
637
638 Ok(Json(response).into_response())
639 }
640 schemas::token_body::OAuth2TokenBodyGrantType::AuthorizationCode => {
641 let client_id = &token_body.client_id;
642 let code = token_body.code.as_ref().ok_or_else(|| {
643 OIDCError::new(
644 OIDCErrorCode::InvalidRequest,
645 Some("code is required for authorization_code grant type.".to_string()),
646 None,
647 )
648 })?;
649 let code_verifier = token_body.code_verifier.as_ref().ok_or_else(|| {
650 OIDCError::new(
651 OIDCErrorCode::InvalidRequest,
652 Some(
653 "code_verifier is required for authorization_code grant type.".to_string(),
654 ),
655 None,
656 )
657 })?;
658 let redirect_uri = token_body.redirect_uri.as_ref().ok_or_else(|| {
659 OIDCError::new(
660 OIDCErrorCode::InvalidRequest,
661 Some("redirect_uri is required for authorization_code grant type.".to_string()),
662 None,
663 )
664 })?;
665
666 let client_app =
667 find_client_app(&state, tenant.clone(), project.clone(), client_id.clone()).await?;
668
669 verify_client(&client_app, &token_body)?;
670
671 let code = code_verification::retrieve_and_verify_code(
672 &*state.repo,
673 &tenant,
674 &project,
675 &client_app,
676 AuthorizationCodeKind::OAuth2CodeGrant,
677 &code,
678 Some(&redirect_uri),
679 Some(&code_verifier),
680 )
681 .await
682 .map_err(|_| {
683 OIDCError::new(
684 OIDCErrorCode::AccessDenied,
685 Some("Invalid authorization code.".to_string()),
686 None,
687 )
688 })?;
689
690 if code.kind != AuthorizationCodeKind::OAuth2CodeGrant {
691 return Err(OIDCError::new(
692 OIDCErrorCode::InvalidGrant,
693 Some("Invalid authorization code.".to_string()),
694 None,
695 ));
696 }
697
698 if code.is_expired.unwrap_or(true) {
699 return Err(OIDCError::new(
700 OIDCErrorCode::AccessDenied,
701 Some("Authorization code has expired.".to_string()),
702 None,
703 ));
704 }
705
706 let approved_scopes = get_approved_scopes(
707 &*state.repo,
708 &tenant,
709 &project,
710 UserId::new(code.user_id.clone()),
711 ClientId::new(client_id.clone()),
712 )
713 .await?;
714
715 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
717 &*state.repo,
718 &tenant,
719 &project,
720 &code.code,
721 )
722 .await
723 .map_err(|_e| {
724 OIDCError::new(
725 OIDCErrorCode::ServerError,
726 Some("Failed to delete used authorization code.".to_string()),
727 None,
728 )
729 })?;
730
731 let user =
732 TenantAuthAdmin::<_, User, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
733 .await
734 .map_err(|_e| {
735 OIDCError::new(
736 OIDCErrorCode::ServerError,
737 Some("Failed to retrieve user.".to_string()),
738 None,
739 )
740 })?;
741
742 let response = create_token_response(
743 &user_agent,
744 &*state.repo,
745 &client_app,
746 &token_body.grant_type,
747 TokenResponseArguments {
748 user_id: code.user_id,
749 user_kind: AuthorKind::Membership,
750 user_role: match user.map(|u| u.role) {
751 Some(RepoUserRole::Admin) => UserRole::Admin,
752 Some(RepoUserRole::Member) => UserRole::Member,
753 Some(RepoUserRole::Owner) => UserRole::Owner,
754 None => UserRole::Member,
755 },
756 client_id: client_id.clone(),
757 scopes: approved_scopes.clone(),
758 tenant: tenant.clone(),
759 project: project.clone(),
760 access_policy_version_ids: match code.membership.as_ref() {
761 Some(membership) => {
762 find_users_access_policy_version_ids(
763 state.search.as_ref(),
764 &tenant,
765 &project,
766 &membership,
767 &ResourceType::Membership,
768 )
769 .await?
770 }
771 None => vec![],
772 },
773 membership: code.membership,
774 },
775 )
776 .await?;
777
778 Ok(Json(response).into_response())
779 }
780 }
781}