Skip to main content

haste_jwt/
claims.rs

1use crate::{
2    AuthorId, AuthorKind, ProjectId, SupportedFHIRVersions, TenantId, UserRole, VersionId,
3    scopes::Scopes,
4};
5use derivative::Derivative;
6use haste_fhir_model::r4::generated::{terminology::IssueType, types::FHIRUrl};
7use haste_fhir_operation_error::OperationOutcomeError;
8use serde::{Deserialize, Serialize};
9
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
11pub enum SubscriptionTier {
12    #[serde(rename = "free")]
13    Free,
14    #[serde(rename = "professional")]
15    Professional,
16    #[serde(rename = "team")]
17    Team,
18    #[serde(rename = "unlimited")]
19    Unlimited,
20}
21
22impl From<SubscriptionTier> for String {
23    fn from(tier: SubscriptionTier) -> Self {
24        match tier {
25            SubscriptionTier::Free => "free",
26            SubscriptionTier::Professional => "professional",
27            SubscriptionTier::Team => "team",
28            SubscriptionTier::Unlimited => "unlimited",
29        }
30        .to_string()
31    }
32}
33
34impl TryFrom<String> for SubscriptionTier {
35    type Error = OperationOutcomeError;
36    fn try_from(value: String) -> Result<Self, Self::Error> {
37        match value.as_str() {
38            "free" => Ok(SubscriptionTier::Free),
39            "professional" => Ok(SubscriptionTier::Professional),
40            "team" => Ok(SubscriptionTier::Team),
41            "unlimited" => Ok(SubscriptionTier::Unlimited),
42            _ => Err(OperationOutcomeError::error(
43                IssueType::invalid(),
44                format!("Invalid subscription tier: '{value}'"),
45            )),
46        }
47    }
48}
49
50#[derive(Derivative, Serialize, Deserialize, Clone)]
51#[derivative(Debug = "transparent")]
52pub struct UserTokenClaims {
53    pub sub: AuthorId,
54    #[derivative(Debug = "ignore")]
55    pub exp: usize,
56    #[derivative(Debug = "ignore")]
57    pub aud: String,
58    #[derivative(Debug = "ignore")]
59    pub scope: Scopes,
60
61    /// Smart on fhir claims.
62    #[derivative(Debug = "ignore")]
63    #[serde(rename = "fhirUser")]
64    pub fhir_user: Option<FHIRUrl>,
65    /// The Patient (an id in the token's project) that `patient/` scopes are
66    /// bound to. Issued only when the user is that patient.
67    #[serde(
68        rename = "https://haste.health/patient",
69        default,
70        skip_serializing_if = "Option::is_none"
71    )]
72    #[derivative(Debug = "ignore")]
73    pub patient: Option<String>,
74    /// The Encounter of the SMART EHR launch the token was issued for, when
75    /// the token response returned one.
76    #[serde(
77        rename = "https://haste.health/encounter",
78        default,
79        skip_serializing_if = "Option::is_none"
80    )]
81    #[derivative(Debug = "ignore")]
82    pub encounter: Option<String>,
83    /// The client the token was issued to (RFC 9068 ยง2.2). Tokens issued
84    /// before the claim existed, and the server's own system context, have
85    /// none.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    #[derivative(Debug = "ignore")]
88    pub client_id: Option<String>,
89
90    // Haste Health claims.
91    #[serde(rename = "https://haste.health/tenant")]
92    #[derivative(Debug = "ignore")]
93    pub tenant: TenantId,
94    #[serde(rename = "https://haste.health/subscription_tier")]
95    #[derivative(Debug = "ignore")]
96    pub subscription_tier: SubscriptionTier,
97    #[serde(rename = "https://haste.health/project")]
98    #[derivative(Debug = "ignore")]
99    pub project: Option<ProjectId>,
100    #[serde(rename = "https://haste.health/user_role")]
101    #[derivative(Debug = "ignore")]
102    pub user_role: UserRole,
103    #[serde(rename = "https://haste.health/user_id")]
104    #[derivative(Debug = "ignore")]
105    pub user_id: AuthorId,
106    #[serde(rename = "https://haste.health/resource_type")]
107    #[derivative(Debug = "ignore")]
108    pub resource_type: AuthorKind,
109    #[serde(rename = "https://haste.health/access_policies")]
110    #[derivative(Debug = "ignore")]
111    pub access_policy_version_ids: Vec<VersionId>,
112    #[serde(rename = "https://haste.health/membership")]
113    #[derivative(Debug = "ignore")]
114    pub membership: Option<String>,
115    /// The FHIR version of the project this token was issued for, so a request
116    /// does not have to look the project up to know it. Required: a token
117    /// without it is from before the claim existed, and is rejected rather than
118    /// guessed at.
119    #[serde(rename = "https://haste.health/fhir_version")]
120    #[derivative(Debug = "ignore")]
121    pub fhir_version: SupportedFHIRVersions,
122}
123
124/// The id token issued with an access token when `openid` is granted (`OpenID
125/// Connect Core`). It tells the client that requested the sign-in who signed
126/// in, so unlike the access token its audience is that client and not the FHIR
127/// server. A Haste Health server using this one as an identity provider
128/// verifies it on federated login.
129#[derive(Serialize, Deserialize, Clone)]
130pub struct IdTokenClaims {
131    pub iss: String,
132    pub sub: String,
133    pub aud: String,
134    pub exp: usize,
135    pub iat: usize,
136    /// Issued with the `email` scope.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub email: Option<String>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub email_verified: Option<bool>,
141    /// Issued with the `fhirUser` scope (SMART App Launch).
142    #[serde(rename = "fhirUser", default, skip_serializing_if = "Option::is_none")]
143    pub fhir_user: Option<FHIRUrl>,
144    // Where the user signed in, for clients that show it.
145    #[serde(rename = "https://haste.health/tenant")]
146    pub tenant: TenantId,
147    #[serde(rename = "https://haste.health/project")]
148    pub project: ProjectId,
149    #[serde(rename = "https://haste.health/user_role")]
150    pub user_role: UserRole,
151}