Skip to main content

haste_server/auth_n/oidc/
utilities.rs

1use haste_fhir_model::r4::generated::{resources::ClientApplication, terminology::IssueType};
2use haste_fhir_operation_error::OperationOutcomeError;
3use haste_jwt::TenantId;
4use haste_repository::{
5    Repository,
6    admin::TenantModelAdmin,
7    types::{
8        authorization_code::{
9            AuthorizationCodeKind, AuthorizationCodeSearchClaims, CreateAuthorizationCode,
10        },
11        user::{CreateUser, UpdateUser},
12    },
13};
14use regex::Regex;
15
16pub fn is_valid_redirect_url(redirect_url: &str, client: &ClientApplication) -> bool {
17    let k = client.redirectUri.as_ref().and_then(|redirect_uris| {
18        redirect_uris.iter().find(|redirect_pattern| {
19            if let Some(redirect_pattern) = redirect_pattern.value.as_ref()
20                && let Ok(pattern) = Regex::new(&format!(
21                    "^{}$",
22                    regex::escape(redirect_pattern).replace("\\*", "(.+)")
23                ))
24            {
25                pattern.is_match(redirect_url)
26            } else {
27                false
28            }
29        })
30    });
31
32    k.is_some() && !redirect_url.is_empty()
33}
34
35/// Rejects a password that is too easy to guess. The error carries the
36/// feedback to show.
37pub fn check_password_strength(
38    password: &str,
39    user_email: &str,
40) -> Result<(), OperationOutcomeError> {
41    let password_strength = zxcvbn::zxcvbn(password, &[user_email]);
42
43    if u8::from(password_strength.score()) < 3 {
44        let feedback = password_strength
45            .feedback()
46            .map(|f| format!("{}", f))
47            .unwrap_or_default();
48
49        return Err(OperationOutcomeError::fatal(
50            IssueType::security(),
51            feedback,
52        ));
53    }
54
55    Ok(())
56}
57
58pub async fn set_user_password<Repo: Repository>(
59    repo: &Repo,
60    tenant: &TenantId,
61    user_email: &str,
62    user_id: &str,
63    password: &str,
64) -> Result<(), OperationOutcomeError> {
65    check_password_strength(password, user_email)?;
66
67    TenantModelAdmin::<CreateUser, _, _, _, String>::update(
68        repo,
69        tenant,
70        UpdateUser {
71            id: user_id.to_string(),
72            password: Some(password.to_string()),
73            email: None,
74            role: None,
75            method: None,
76            provider_id: None,
77            email_verified: None,
78        },
79    )
80    .await?;
81
82    Ok(())
83}
84
85/// Deletes every refresh token issued to `user_id` in `tenant`, in every
86/// project.
87///
88/// A password reset is how a compromised account is recovered, and a refresh
89/// token issued before it would otherwise let whoever holds it keep minting
90/// access tokens for up to twelve hours after the password changed.
91pub async fn revoke_refresh_tokens<Repo: Repository>(
92    repo: &Repo,
93    tenant: &TenantId,
94    user_id: &str,
95) -> Result<(), OperationOutcomeError> {
96    let refresh_tokens = TenantModelAdmin::<CreateAuthorizationCode, _, _, _, _>::search(
97        repo,
98        tenant,
99        &AuthorizationCodeSearchClaims {
100            client_id: None,
101            code: None,
102            kind: Some(AuthorizationCodeKind::RefreshToken),
103            user_id: Some(user_id.to_string()),
104            user_agent: None,
105            is_expired: None,
106        },
107    )
108    .await?;
109
110    for refresh_token in refresh_tokens {
111        TenantModelAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
112            repo,
113            tenant,
114            &refresh_token.code,
115        )
116        .await?;
117    }
118
119    Ok(())
120}