haste_server/auth_n/oidc/routes/
token.rs

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; // 2 hours 
63
64#[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 offline means refresh token should be generated.
133    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            // Client credentials grant does not get refresh tokens. Serves no purpose and requires knowing user kind to 
148            // rebuild the token.
149        && *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), // 12 hours.
199                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    // Verify the grant types align
284    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    // Allow basic auth if client app allows grant.
396    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            // Remove the code once valid.
676            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}