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