use alibi_core::utils::cookie_utils::{
create_session_cookie_with_max_age, create_session_like_cookie, related_cookie_name,
sign_cookie_value, verify_cookie_value,
};
use alibi_core::{
AuthAccount, AuthConfig, AuthContext, AuthError, AuthResponse, AuthResult, AuthSchema,
AuthSession, AuthUser, CreateVerification, UpdateUser,
};
use chrono::{Duration, Utc};
use serde_json::Value;
pub(crate) fn dont_remember_preference(
req: &impl alibi_core::session::SessionRequest,
config: &AuthConfig,
) -> bool {
crate::helpers::get_cookie(req, &related_cookie_name(config, "dont_remember"))
.and_then(|value| verify_cookie_value(&value, config.current_secret()))
.is_some_and(|value| !value.is_empty())
}
pub(crate) fn with_session_cookies(
response: AuthResponse,
config: &AuthConfig,
token: &str,
dont_remember: bool,
) -> AuthResult<AuthResponse> {
let mut response = response;
for (name, value) in session_cookie_headers(config, token, dont_remember)? {
response.headers.append(name, value);
}
Ok(response)
}
pub(crate) fn session_cookie_headers(
config: &AuthConfig,
token: &str,
dont_remember: bool,
) -> AuthResult<alibi_core::Headers> {
let max_age = (!dont_remember).then(|| config.session.expires_in.num_seconds());
let mut headers = alibi_core::Headers::new();
headers.append(
"Set-Cookie",
create_session_cookie_with_max_age(Some(token), max_age, config)?,
);
if dont_remember {
headers.append(
"Set-Cookie",
create_session_like_cookie(
&related_cookie_name(config, "dont_remember"),
&sign_cookie_value("true", config.current_secret()),
None,
config,
)?,
);
}
Ok(headers)
}
pub(crate) async fn session_response<S: AuthSchema>(
ctx: &AuthContext<S>,
req: &impl alibi_core::session::SessionRequest,
user: alibi_core::AdapterRecord<S::User>,
) -> AuthResult<(Value, AuthResponse)> {
session_response_with_remember(ctx, req, user, None).await
}
pub(crate) async fn session_response_with_remember<S: AuthSchema>(
ctx: &AuthContext<S>,
req: &impl alibi_core::session::SessionRequest,
user: alibi_core::AdapterRecord<S::User>,
remember_me: Option<bool>,
) -> AuthResult<(Value, AuthResponse)> {
let dont_remember = remember_me.map_or_else(
|| dont_remember_preference(req, &ctx.config),
|remember| !remember,
);
let mut config = (*ctx.config).clone();
if remember_me == Some(false) {
config.session.expires_in = Duration::days(1);
}
let issuing_context = AuthContext {
config: std::sync::Arc::new(config),
database: std::sync::Arc::clone(&ctx.database),
email_provider: ctx.email_provider.clone(),
metadata: ctx.metadata.clone(),
extensions: ctx.extensions.clone(),
};
let meta = if req.endpoint_call().is_none() {
req.physical_request()
.map(alibi_core::RequestMeta::from_request)
} else {
None
}
.unwrap_or_else(|| alibi_core::RequestMeta {
ip_address: Some(
ctx.config
.advanced
.ip_address
.resolve_ip(req.session_headers())
.unwrap_or_default(),
),
user_agent: Some(
req.session_headers()
.get("user-agent")
.cloned()
.unwrap_or_default(),
),
});
if req.endpoint_call().is_some() {
alibi_core::session::cookie_cache::runtime::set_issuance_preference(req, dont_remember);
}
let issued = crate::helpers::issue_selected_user_session_record(
&issuing_context,
user,
meta.ip_address,
meta.user_agent,
)
.await
.map_err(crate::helpers::SessionIssueError::into_auth_error)?;
let token = issued.session.token();
let user = serde_json::to_value(ctx.user_view(&issued.user))?;
let session = serde_json::to_value(ctx.session_view(&issued.session))?;
let payload = serde_json::json!({"token":token,"user":user,"session":session});
let response = with_session_cookies(
AuthResponse::json(200, &serde_json::json!({"token":token,"user":user}))?,
&ctx.config,
token,
dont_remember,
)?;
Ok((payload, response))
}
pub(crate) async fn revoke_unproven_access<S: AuthSchema>(
ctx: &AuthContext<S>,
user_id: &str,
) -> AuthResult<Option<alibi_core::AdapterRecord<S::User>>> {
let identifier = format!("revoke-unproven-account-access:{user_id}");
let reserved = ctx
.verifications()
.reserve(CreateVerification {
identifier: identifier.clone(),
value: user_id.to_owned(),
expires_at: Utc::now() + Duration::seconds(5),
})
.await;
let reserved = match reserved {
Ok(value) => value,
Err(AuthError::Internal(message))
if message
== "reserveVerificationValue requires database-backed verification storage. Set verification.storeInDatabase to true for flows that reserve verification values." =>
{
true
}
Err(error) => return Err(error),
};
if !reserved {
for _ in 0..8 {
if ctx.verifications().find(&identifier).await?.is_none() {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
}
return ctx.database.get_user_by_id_record(user_id).await;
}
let result = async {
let Some(user) = ctx.database.get_user_by_id_record(user_id).await? else {
return Ok(None);
};
if user.email_verified() {
return Ok(Some(user));
}
for account in ctx.database.get_user_accounts(user_id).await? {
ctx.database.delete_account(&account.id()).await?;
}
ctx.database.delete_user_sessions(user_id).await?;
ctx.database
.update_user_record(
user_id,
UpdateUser {
email_verified: Some(true),
..Default::default()
},
)
.await
.map(Some)
}
.await;
_ = ctx.verifications().delete(&identifier).await;
result
}