Skip to main content

haste_server/auth_n/session/
user.rs

1use crate::auth_n::error::AuthError;
2use haste_fhir_model::r4::generated::terminology::IssueType;
3use haste_fhir_operation_error::OperationOutcomeError;
4use haste_repository::{
5    Repository,
6    admin::TenantModelAdmin,
7    types::{
8        mfa::{UserMFACredentialCreate, UserMFASearchClaims},
9        scope::UserId,
10        user::User,
11    },
12};
13use serde::{Deserialize, Serialize};
14use tower_sessions::Session;
15
16static AUTHORIZATION_STATE_KEY: &str = "user_authorization_state";
17
18#[derive(Deserialize, Serialize)]
19pub struct AuthorizationStateCompleted {
20    pub user: User,
21}
22
23#[derive(Deserialize, Serialize)]
24pub enum SessionAuthorizationState {
25    Complete(AuthorizationStateCompleted),
26    MFARequired { user: User },
27    // [TODO] Enforce automatic MFA enrollment for users who have not yet set it up.
28    // This will likely be a per tenant setting.
29    // MFAEnrollmentRequired { user: User },
30}
31
32pub async fn get_completed_authorization_state(
33    session: &Session,
34) -> Result<AuthorizationStateCompleted, OperationOutcomeError> {
35    let authorization_state = get_authorization_state(session).await?;
36
37    match authorization_state {
38        Some(SessionAuthorizationState::Complete(completed_state)) => Ok(completed_state),
39        _ => Err(OperationOutcomeError::error(
40            IssueType::invalid(),
41            "Authorization state is not complete.".to_string(),
42        )),
43    }
44}
45
46pub async fn get_authorization_state(
47    session: &Session,
48) -> Result<Option<SessionAuthorizationState>, OperationOutcomeError> {
49    let authorization_state = session
50        .get::<SessionAuthorizationState>(AUTHORIZATION_STATE_KEY)
51        .await
52        .map_err(|_e| {
53            OperationOutcomeError::fatal(
54                IssueType::exception(),
55                "Session returned an error when retrieving current user.".to_string(),
56            )
57        })?;
58
59    Ok(authorization_state)
60}
61
62pub async fn set_initial_authorization_state<Repo: Repository>(
63    repo: &Repo,
64    session: &Session,
65    user: User,
66) -> Result<(), OperationOutcomeError> {
67    let active_mfa_credentials = TenantModelAdmin::<UserMFACredentialCreate, _, _, _, _>::search(
68        repo,
69        &user.tenant,
70        &UserMFASearchClaims {
71            tenant: user.tenant.clone(),
72            user_id: UserId::new(user.id.clone()),
73            is_active: Some(true),
74        },
75    )
76    .await?;
77
78    let initial_state = if active_mfa_credentials.is_empty() {
79        // No active MFA credentials so can state that state is completed.
80        SessionAuthorizationState::Complete(AuthorizationStateCompleted { user })
81    } else {
82        SessionAuthorizationState::MFARequired { user }
83    };
84
85    session
86        .insert(AUTHORIZATION_STATE_KEY, initial_state)
87        .await
88        .map_err(|e| AuthError::SessionWriteFailed(e).into())
89}
90
91pub async fn set_completed_authorization_state(
92    session: &Session,
93    user: User,
94) -> Result<(), OperationOutcomeError> {
95    session
96        .insert(
97            AUTHORIZATION_STATE_KEY,
98            SessionAuthorizationState::Complete(AuthorizationStateCompleted { user }),
99        )
100        .await
101        .map_err(|e| AuthError::SessionWriteFailed(e).into())
102}
103
104/// Gives the session a new id, keeping its data.
105///
106/// Called whenever the session's authorization level rises: at password or
107/// federated login and when MFA completes. A session id handed to the browser
108/// before login (by an attacker who planted the cookie, or just one that was
109/// observed) then never becomes an authenticated session.
110pub async fn rotate_session_id(session: &Session) -> Result<(), OperationOutcomeError> {
111    session.cycle_id().await.map_err(|_e| {
112        OperationOutcomeError::fatal(
113            IssueType::exception(),
114            "Failed to rotate the session id.".to_string(),
115        )
116    })
117}
118
119pub async fn clear_authorization_state(session: &Session) -> Result<(), OperationOutcomeError> {
120    session
121        .remove::<SessionAuthorizationState>(AUTHORIZATION_STATE_KEY)
122        .await
123        .map_err(|_e| {
124            OperationOutcomeError::fatal(
125                IssueType::exception(),
126                "Failed to clear user from session.".to_string(),
127            )
128        })?;
129
130    Ok(())
131}