Skip to main content

alibi_plugins/email_password/
endpoint.rs

1//! Public credential sign-in uses logical input and the shared session issuer.
2use super::types::SignInCoreResult;
3use super::{EmailPasswordPlugin, SignInEmailRequest, SignInEmailResponse, sign_in_core};
4use crate::authentication_helpers::RequestBody;
5use alibi_core::endpoint::{
6    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
7};
8use alibi_core::utils::json::JsValue;
9use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, HttpMethod, RequestMeta};
10
11pub(super) fn definitions(username_enabled: bool) -> Vec<EndpointDefinition> {
12    let mut definitions = vec![
13        crate::endpoint::definition(
14            "signInEmail",
15            "signInEmail",
16            Some("/sign-in/email"),
17            HttpMethod::Post,
18        ),
19        crate::endpoint::definition(
20            "signUpEmail",
21            "signUpWithEmailAndPassword",
22            Some("/sign-up/email"),
23            HttpMethod::Post,
24        ),
25    ];
26    if username_enabled {
27        definitions.push(crate::endpoint::definition(
28            "signInUsername",
29            "signInUsername",
30            Some("/sign-in/username"),
31            HttpMethod::Post,
32        ));
33        definitions.push(crate::endpoint::definition(
34            "isUsernameAvailable",
35            "isUsernameAvailable",
36            Some("/is-username-available"),
37            HttpMethod::Post,
38        ));
39    }
40    definitions
41}
42pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
43    if call.operation_id() == "signInUsername" {
44        return Ok(EndpointInput {
45            body: Some(crate::endpoint::validate_fields(
46                call.body(),
47                "body",
48                &[
49                    crate::authentication_helpers::JsonField::string("username", true),
50                    crate::authentication_helpers::JsonField::string("password", true),
51                    crate::authentication_helpers::JsonField {
52                        name: "rememberMe",
53                        kind: crate::authentication_helpers::JsonFieldKind::Boolean,
54                        required: false,
55                    },
56                    crate::authentication_helpers::JsonField::string("callbackURL", false),
57                ],
58            )?),
59            query: call.query().cloned(),
60        });
61    }
62    if call.operation_id() == "isUsernameAvailable" {
63        return Ok(EndpointInput {
64            body: Some(crate::endpoint::validate_fields(
65                call.body(),
66                "body",
67                &[crate::authentication_helpers::JsonField::string(
68                    "username", true,
69                )],
70            )?),
71            query: call.query().cloned(),
72        });
73    }
74    if call.operation_id() == "signUpWithEmailAndPassword" {
75        _ = crate::endpoint::validate_fields(
76            call.body(),
77            "body",
78            super::SignUpEmailRequest::FIELDS,
79        )?;
80        return Ok(EndpointInput {
81            body: call.body().cloned(),
82            query: call.query().cloned(),
83        });
84    }
85    let mut body =
86        crate::endpoint::validate_fields(call.body(), "body", SignInEmailRequest::FIELDS)?;
87    if let JsValue::Object(body) = &mut body {
88        _ = body
89            .entry("rememberMe".into())
90            .or_insert(JsValue::Bool(true));
91    }
92    Ok(EndpointInput {
93        body: Some(body),
94        query: call.query().cloned(),
95    })
96}
97impl EmailPasswordPlugin {
98    /// Sign in using the installed username policy and credential service.
99    ///
100    /// # Errors
101    /// Returns an error if the input cannot be serialized.
102    pub fn sign_in_username_endpoint(
103        input: &super::SignInUsernameRequest,
104    ) -> AuthResult<ServerEndpoint<SignInEmailResponse>> {
105        ServerEndpoint::new("email-password", "signInUsername").with_body(input)
106    }
107
108    /// Check a guest-supplied username through the installed username policy.
109    #[must_use]
110    pub fn is_username_available_endpoint(
111        username: impl Into<String>,
112    ) -> ServerEndpoint<super::IsUsernameAvailableResponse> {
113        ServerEndpoint::new("email-password", "isUsernameAvailable").with_body_value(
114            JsValue::Object(
115                [("username".into(), JsValue::String(username.into()))]
116                    .into_iter()
117                    .collect(),
118            ),
119        )
120    }
121
122    /// Sign in through the installed credential service and server hooks.
123    pub fn sign_in_email_endpoint(
124        body: &SignInEmailRequest,
125    ) -> AuthResult<ServerEndpoint<SignInEmailResponse>> {
126        ServerEndpoint::new("email-password", "signInEmail").with_body(body)
127    }
128    /// Create a credential owner through the installed sign-up service and hooks.
129    pub fn sign_up_email_endpoint(
130        body: &super::SignUpEmailRequest,
131    ) -> AuthResult<ServerEndpoint<super::SignUpEmailResponse<alibi_core::field_policy::FieldOutput>>>
132    {
133        ServerEndpoint::new("email-password", "signUpEmail").with_body(body)
134    }
135    pub(super) async fn call_sign_in_endpoint(
136        &self,
137        call: &EndpointCall,
138        ctx: &AuthContext<impl AuthSchema>,
139    ) -> AuthResult<EndpointResponse> {
140        if call.operation_id() == "signInUsername" {
141            let body: super::SignInUsernameRequest = call.body_as()?;
142            alibi_core::session::cookie_cache::runtime::set_issuance_preference(
143                call,
144                body.remember_me == Some(false),
145            );
146            let headers = call.headers().cloned().unwrap_or_default();
147            let meta = RequestMeta {
148                ip_address: Some(
149                    ctx.config
150                        .advanced
151                        .ip_address
152                        .resolve_ip(&headers)
153                        .unwrap_or_default(),
154                ),
155                user_agent: Some(headers.get("user-agent").cloned().unwrap_or_default()),
156            };
157            let output = self.username_sign_in_core(call, ctx, body, &meta).await?;
158            let mut response = if output.status >= 400 {
159                EndpointResponse::error(crate::endpoint::error_response(output.clone()))
160            } else {
161                EndpointResponse::value(alibi_core::utils::json::from_slice(&output.body)?)
162            };
163            response.set_status(Some(output.status));
164            response.merge_headers(output.headers);
165            return Ok(response);
166        }
167        if call.operation_id() == "isUsernameAvailable" {
168            let output = self
169                .username_availability_core(ctx, call.body_as()?)
170                .await?;
171            let mut response = if output.status >= 400 {
172                EndpointResponse::error(crate::endpoint::error_response(output.clone()))
173            } else {
174                EndpointResponse::value(alibi_core::utils::json::from_slice(&output.body)?)
175            };
176            response.set_status(Some(output.status));
177            response.merge_headers(output.headers);
178            return Ok(response);
179        }
180        if call.operation_id() == "signUpWithEmailAndPassword" {
181            return self.call_signup_endpoint(call, ctx).await;
182        }
183        let body: SignInEmailRequest = call.body_as()?;
184        if let Some(request) = call.request() {
185            alibi_core::middleware::CsrfMiddleware::new(
186                alibi_core::middleware::CsrfConfig::new(),
187                ctx.config.clone(),
188            )
189            .check_login_origin(request)?;
190            if let Some(url) = body.callback_url.as_deref().filter(|url| !url.is_empty())
191                && !ctx.config.current_origin_check_disabled()
192                && !ctx.config.is_redirect_target_trusted(url)
193            {
194                return Err(AuthError::forbidden("Invalid callbackURL"));
195            }
196        }
197        alibi_core::session::cookie_cache::runtime::set_issuance_preference(
198            call,
199            body.remember_me == Some(false),
200        );
201        let headers = call.headers().cloned().unwrap_or_default();
202        let meta = RequestMeta {
203            ip_address: Some(
204                ctx.config
205                    .advanced
206                    .ip_address
207                    .resolve_ip(&headers)
208                    .unwrap_or_default(),
209            ),
210            user_agent: Some(headers.get("user-agent").cloned().unwrap_or_default()),
211        };
212        match Box::pin(sign_in_core(
213            call,
214            &body,
215            &self.config,
216            self.email_verification.as_deref(),
217            &meta,
218            ctx,
219        ))
220        .await?
221        {
222            SignInCoreResult::Success {
223                response,
224                token,
225                set_cookie_headers,
226            } => {
227                let mut output = EndpointResponse::json(&response)?;
228                output.merge_headers(crate::authentication_helpers::session_cookie_headers(
229                    &ctx.config,
230                    &token,
231                    body.remember_me == Some(false),
232                )?);
233                if let Some(url) = body.callback_url.as_deref().filter(|url| !url.is_empty()) {
234                    output = output.with_header("location", url);
235                }
236                for cookie in set_cookie_headers {
237                    output = output.with_header("set-cookie", cookie);
238                }
239                Ok(output)
240            }
241            SignInCoreResult::TwoFactorRedirect {
242                response,
243                set_cookie_headers,
244            } => {
245                let mut output = EndpointResponse::json(&response)?;
246                for cookie in set_cookie_headers {
247                    output = output.with_header("set-cookie", cookie);
248                }
249                Ok(output)
250            }
251        }
252    }
253    async fn call_signup_endpoint(
254        &self,
255        call: &EndpointCall,
256        ctx: &AuthContext<impl AuthSchema>,
257    ) -> AuthResult<EndpointResponse> {
258        let raw = call.body().cloned().unwrap_or(JsValue::Null);
259        let mut parsed = raw.clone();
260        if !self.config.enable_username
261            && let JsValue::Object(body) = &mut parsed
262        {
263            _ = body.shift_remove("username");
264            _ = body.shift_remove("displayUsername");
265        }
266        let mut body: super::SignUpEmailRequest = alibi_core::utils::json::from_value(parsed)?;
267        if let Some(request) = call.request() {
268            alibi_core::middleware::CsrfMiddleware::new(
269                alibi_core::middleware::CsrfConfig::new(),
270                ctx.config.clone(),
271            )
272            .check_login_origin(request)?;
273            if let Some(url) = body.callback_url.as_deref().filter(|url| !url.is_empty())
274                && !ctx.config.current_origin_check_disabled()
275                && !ctx.config.is_redirect_target_trusted(url)
276            {
277                return Err(AuthError::forbidden("Invalid callbackURL"));
278            }
279        }
280        if let Some(response) = self.prepare_signup_input(&mut body, raw, call, ctx).await? {
281            return Err(crate::endpoint::error_response(response));
282        }
283        alibi_core::session::cookie_cache::runtime::set_issuance_preference(
284            call,
285            body.remember_me == Some(false),
286        );
287        let headers = call.headers().cloned().unwrap_or_default();
288        let meta = RequestMeta {
289            ip_address: Some(
290                ctx.config
291                    .advanced
292                    .ip_address
293                    .resolve_ip(&headers)
294                    .unwrap_or_default(),
295            ),
296            user_agent: Some(headers.get("user-agent").cloned().unwrap_or_default()),
297        };
298        let (value, cookies) = Box::pin(super::sign_up_core(
299            call.request(),
300            &body,
301            &self.config,
302            &meta,
303            ctx,
304        ))
305        .await?;
306        let mut response = EndpointResponse::json(&value)?;
307        for cookie in cookies.into_iter().flatten() {
308            response = response.with_header("set-cookie", cookie);
309        }
310        Ok(response)
311    }
312}