Skip to main content

alibi_api/plugins/password_management/
set_password.rs

1//! Trusted server-only passwordless-to-credential operation.
2use super::super::email_password::EmailPasswordConfig;
3use alibi_core::{
4    AuthAccount, AuthContext, AuthError, AuthRequest, AuthResult, AuthSchema, AuthUser,
5    CreateAccount, UpdateAccount,
6};
7
8/// Set the authenticated user's password when no password is currently set.
9///
10/// This is a trusted server API, with no public authentication route and no target
11/// user selector. It re-reads the signed-cookie session from physical storage;
12/// cached or hook-provided virtual sessions cannot authorize credential changes.
13/// The initialized email/password policy and custom hasher apply. Existing
14/// sessions remain active, and the configured hasher runs even when an existing
15/// password will subsequently cause rejection.
16///
17/// # Errors
18/// Returns an authentication error for absent or invalid authoritative sessions,
19/// password-bound errors, `PASSWORD_ALREADY_SET`, or a storage/hasher error.
20pub async fn set_password<S: AuthSchema>(
21    request: &AuthRequest,
22    new_password: &str,
23    context: &AuthContext<S>,
24) -> AuthResult<()> {
25    let (user, _) = context
26        .require_authoritative_session(request)
27        .await
28        .map_err(|error| {
29            if matches!(error, AuthError::Unauthenticated) {
30                AuthError::Upstream {
31                    status: 401,
32                    code: "UNAUTHORIZED",
33                    message: "Unauthorized",
34                }
35            } else {
36                error
37            }
38        })?;
39    let policy = context.extensions.get::<EmailPasswordConfig>();
40    let (minimum, maximum) = crate::plugins::email_password::password_length_limits(context);
41    let length = new_password.encode_utf16().count();
42    if length < minimum {
43        return Err(AuthError::Upstream {
44            status: 400,
45            code: "PASSWORD_TOO_SHORT",
46            message: "Password too short",
47        });
48    }
49    if length > maximum {
50        return Err(AuthError::Upstream {
51            status: 400,
52            code: "PASSWORD_TOO_LONG",
53            message: "Password too long",
54        });
55    }
56    let user_id = user.id();
57    let credential = context
58        .database
59        .get_user_accounts(user_id.as_ref())
60        .await?
61        .into_iter()
62        .find(|account| account.provider_id() == "credential" && account.account_id() == user_id);
63    let hasher = policy
64        .as_ref()
65        .and_then(|policy| policy.password_hasher.as_ref());
66    let hash_context = alibi_core::PasswordHashContext {
67        // The pathless endpoint's handler context uses the published virtual
68        // identity. It does not identify a public URL or register a route.
69        path: Some("virtual:".into()),
70        request: alibi_core::hooks::current_request_hook_context(),
71    };
72    let password = context
73        .hash_password_with_context(hasher, new_password, Some(&hash_context))
74        .await?;
75    if let Some(account) = credential {
76        if account
77            .password()
78            .is_some_and(|password| !password.is_empty())
79        {
80            return Err(AuthError::Upstream {
81                status: 400,
82                code: "PASSWORD_ALREADY_SET",
83                message: "User already has a password set",
84            });
85        }
86        drop(
87            context
88                .database
89                .update_account_record(
90                    account.id().as_ref(),
91                    UpdateAccount {
92                        password: Some(password),
93                        ..Default::default()
94                    },
95                )
96                .await?,
97        );
98    } else {
99        drop(
100            context
101                .database
102                .create_account_record(CreateAccount {
103                    additional_fields: Default::default(),
104                    user_id: user.id().into_owned(),
105                    account_id: user.id().into_owned(),
106                    provider_id: "credential".to_owned(),
107                    access_token: None,
108                    refresh_token: None,
109                    id_token: None,
110                    access_token_expires_at: None,
111                    refresh_token_expires_at: None,
112                    scope: None,
113                    password: Some(password),
114                })
115                .await?,
116        );
117    }
118    Ok(())
119}