Skip to main content

haste_server/auth_n/oidc/routes/
token.rs

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; // 2 hours
75pub static REFRESH_TOKEN_EXPIRATION: usize = 43200; // 12 hours
76
77#[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    /// The scopes granted, which SMART lets differ from those requested.
87    #[serde(default)]
88    scope: String,
89    /// SMART launch context: the Patient the `patient/` scopes are bound to.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    patient: Option<String>,
92    /// SMART launch context: the Encounter an EHR launch was started in.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    encounter: Option<String>,
95    /// SMART launch context: whether the app should show which patient it is
96    /// showing. Haste Health opens a launched app on its own, with no patient
97    /// banner around it, so this is true for every EHR launch.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    need_patient_banner: Option<bool>,
100    /// SMART launch context: where the app fetches the EHR's style parameters.
101    #[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    /// The SMART EHR launch the token comes from, kept on its refresh token.
119    launch: Option<LaunchContext>,
120    /// The user's address and whether they verified it, for the id token's
121    /// `email` claims. A client application has neither.
122    email: Option<String>,
123    email_verified: bool,
124}
125
126/// What a token says about the user it is issued to.
127struct TokenUser {
128    role: UserRole,
129    email: Option<String>,
130    email_verified: bool,
131}
132
133impl TokenUser {
134    /// A user that could not be read is issued the least privileged role and
135    /// no address.
136    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/// The context `$launch` saved and authorize copied onto the authorization
157/// code, under `meta.launch`. A refresh token keeps it there too, so a
158/// refreshed token stays bound to the launch.
159#[derive(Serialize, Deserialize, Debug, Clone)]
160struct LaunchContext {
161    patient: String,
162    encounter: Option<String>,
163}
164
165impl LaunchContext {
166    /// The launch context an authorization code or refresh token carries, if
167    /// it came from an EHR launch.
168    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
174/// Whether `scope` needs a patient in context.
175pub(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
187/// What the membership link, and an EHR launch, bind a token to.
188struct SmartContext {
189    fhir_user: Option<FHIRUrl>,
190    patient: Option<String>,
191    encounter: Option<String>,
192    scopes: Scopes,
193}
194
195/// Filters SMART resource scopes to only include read and search permissions.
196fn 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
211/// How a requested `scope` is issued. A patient-bound scope needs a patient
212/// in context and is then read-only; without one it is dropped rather than
213/// issued unbound. `launch` needs an EHR launch and `launch/encounter` an
214/// encounter in context. Anything else is issued as requested.
215fn 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        // For patient-level resource scopes, only issue them if a patient is in context, and make them read-only.
227        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
242/// Binds `scopes` to the resource the membership links to: `fhirUser` names
243/// it, and a linked Patient is the patient in context. An EHR launch's
244/// patient comes first: it is the patient the user launched the app for. The
245/// response's `scope` tells the client what was issued.
246///
247/// # Errors
248///
249/// A user who is a patient may only launch an app for themselves, so a
250/// launch for another patient is refused.
251fn 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
310/// Where an EHR-launched app fetches the project's style parameters (SMART
311/// `smart_style_url`).
312pub(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    // Per SMART on FHIR, `aud` identifies the FHIR resource server the token is
339    // valid for, binding the token to this tenant/project's FHIR endpoint.
340    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    // Carried in the token so that a request never has to look the project up
374    // to know which FHIR version it is working in. A project's version cannot
375    // change, so the claim cannot go stale within a token's lifetime.
376    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    // Returned only with the launch, as in the token response below.
408    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    // An id token describes the signed-in user to the client that asked for
444    // the login (OpenID Connect Core §2), so unlike the access token it is
445    // addressed to that client and not to the FHIR server. A client credentials
446    // grant has no user to describe.
447    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        // The email claims go with the `email` scope (OpenID Connect Core §5.4).
453        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        // An EHR launch returns its context with the `launch` scope; otherwise
506        // the patient is only returned for the scopes it is bound to.
507        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 offline means refresh token should be generated.
517    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            // Client credentials grant does not get refresh tokens. Serves no purpose and requires knowing user kind to 
529            // rebuild the token.
530        && *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                    // The launch goes with the refresh token, so the token it
587                    // is exchanged for keeps the same patient and encounter.
588                    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    // Verify the grant types align
669    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            // Enforce requirement of a client_secret.
676            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
696/// Checks that `client_id` names `client_app` and `client_secret` matches its
697/// stored secret. Two missing secrets match, so a confidential caller must
698/// also pass [`require_client_secret`].
699pub(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
726/// Refuses a confidential grant for a client that has no secret stored.
727pub(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
810/// Version ids of the access policies that apply to `user_type/user_id`.
811///
812/// Policies are assigned through `AccessPolicyV2Assignment` resources. Policies
813/// that still name the user in the deprecated `AccessPolicyV2.target` are
814/// included too, so existing policies keep applying until they are migrated.
815async 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    // Deprecated: AccessPolicyV2.target.link.
880    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
898/// The resource the membership links to (`Patient/123`), if any.
899async 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    // Allow basic auth if client app allows grant.
985    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            // A client has no patient in context.
1033            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            // A public client need not send client_id when refreshing (RFC 6749
1093            // section 6, and fhirclient does not by default): the refresh
1094            // token names the client it was issued to. A confidential client
1095            // still has to authenticate, which verify_client checks.
1096            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            // The launch the refresh token was issued for, so the new token
1187            // keeps its patient and encounter.
1188            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            // Remove the code once valid.
1320            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            // An EHR launch's patient and encounter, saved on the code by authorize.
1350            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}