use std::time::Duration;
use actix_web::{
dev::ServiceRequest, HttpRequest
};
use crate::{
app::{App, AppTypes},
hashing,
maybe_auth::{maybe_auth_from_request, Auth, MaybeAuth},
secret::Secret,
token_actions::AuthTokenAction,
tokens,
users::{UserID, UserState},
};
#[cfg_attr(feature = "diesel", derive(diesel::prelude::QueryableByName))]
pub struct SessionData<A: AppTypes> {
#[cfg_attr(feature = "diesel", diesel(embed))]
pub user: A::User,
#[cfg_attr(feature = "diesel", diesel(embed))]
pub user_state: UserState,
#[cfg_attr(feature = "diesel", diesel(deserialize_as = String), diesel(sql_type = diesel::sql_types::Text))]
pub token_hash: Secret,
#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Timestamp))]
pub expires: A::DateTime,
}
pub enum LoginOutcome<A: App> {
Success(Auth<A>),
IncorrectPassword,
NoSuchUser,
UserHasNoPassword,
}
pub async fn login<A: App>(
app: &mut A,
user_identifier: &str,
password: Secret,
request: &HttpRequest,
) -> Result<LoginOutcome<A>, A::Error> {
let Some(user_data) = app
.get_user_data_by_identifier(user_identifier)
.await
.map_err(Into::into)?
else {
return Ok(LoginOutcome::NoSuchUser);
};
if !user_data.password_hash.exists() {
return Ok(LoginOutcome::UserHasNoPassword);
}
let result = hashing::check_password(&user_data.password_hash, &password)?;
if !result {
return Ok(LoginOutcome::IncorrectPassword);
}
let (session_id, session_token) = begin_session_for_user(app, &user_data.user)
.await?;
AuthTokenAction::Issue(session_token)
.insert_into_request(request);
let user_id = user_data.user.id();
log::debug!("Successful password login for user #{user_id}");
user_data.state.require_ready(user_id)?;
Ok(LoginOutcome::Success(Auth {
user: user_data.user,
user_state: user_data.state,
session_id,
_deny_public_constructor: (),
}))
}
impl<A: App> MaybeAuth<A> {
pub async fn logout(self, app: &mut A, request: &HttpRequest) -> Result<(), A::Error> {
if let MaybeAuth::Authenticated(auth) = self {
auth.logout(app, request)
.await?;
}
Ok(())
}
}
impl<A: App> Auth<A> {
pub async fn logout(self, app: &mut A, request: &HttpRequest) -> Result<(), A::Error> {
log::debug!("Logging out user #{}", self.user.id());
app.delete_session_by_id(self.session_id)
.await
.map_err(Into::into)?;
AuthTokenAction::Revoke
.insert_into_request(request);
Ok(())
}
}
pub(crate) async fn on_successful_challenge<A: App>(
app: &mut A,
user: &A::User,
request: &HttpRequest,
) -> Result<(), A::Error> {
let maybe_auth = maybe_auth_from_request::<A>(request);
let new_session_token = match maybe_auth {
MaybeAuth::Authenticated(auth) if auth.user.id() == user.id() => {
renew_by_id(app, auth.session_id)
.await?
}
_ => {
begin_session_for_user(app, user)
.await?
.1
},
};
AuthTokenAction::Issue(new_session_token)
.insert_into_request(request);
Ok(())
}
pub(crate) async fn authenticate_by_session_token<A: App>(
app: &mut A,
request: &ServiceRequest,
) -> Result<MaybeAuth<A>, A::Error> {
let revoke_cookie = || {
AuthTokenAction::Revoke
.insert_into_request(request);
Ok(MaybeAuth::Unauthenticated)
};
let Some(cookie) = request.cookie(app.session_token_cookie_name()) else {
log::debug!("Request has no session cookie");
return Ok(MaybeAuth::Unauthenticated);
};
let cookie_value = Secret(cookie.value().to_string());
drop(cookie);
let Some((session_id, session_token)) = tokens::unpack(cookie_value) else {
log::info!("Invalid session token format");
return revoke_cookie();
};
log::debug!("Request has cookie claiming session #{}", session_id);
let Some(session) = app.get_session_by_id(session_id)
.await
.map_err(Into::into)?
else {
log::debug!("No such session #{}", session_id);
return revoke_cookie();
};
if session.expires <= app.time_now() {
log::debug!("Session #{} has expired; revoking", session_id);
app.delete_session_by_id(session_id)
.await
.map_err(Into::into)?;
return revoke_cookie();
}
if !hashing::check_fast_hash(&session_token, &session.token_hash) {
log::info!("Invalid session token for session #{}", session_id);
return revoke_cookie();
}
if should_renew(app, session.expires) {
let token = renew_by_id(app, session_id)
.await?;
AuthTokenAction::Issue(token)
.insert_into_request(request);
}
Ok(MaybeAuth::Authenticated(Auth {
user: session.user,
user_state: session.user_state,
session_id,
_deny_public_constructor: (),
}))
}
async fn begin_session_for_user<A: App>(app: &mut A, user: &A::User) -> Result<(A::ID, Secret), A::Error> {
let (session_token, hash) = hashing::generate_session_token_and_hash();
let session_id = app.insert_session(user, hash, expiry_time(app))
.await
.map_err(Into::into)?;
log::debug!("Beginning session #{} for user #{}", session_id, user.id());
Ok((session_id, tokens::pack(session_id, session_token)))
}
async fn renew_by_id<A: App>(app: &mut A, session_id: A::ID) -> Result<Secret, A::Error> {
log::debug!("Renewing session #{}", session_id);
let (session_token, hash) = hashing::generate_session_token_and_hash();
app.update_session_by_id(session_id, hash, expiry_time(app))
.await
.map_err(Into::into)?;
Ok(tokens::pack(session_id, session_token))
}
fn expiry_time<A: App>(app: &A) -> A::DateTime {
let duration = Duration::from_secs(3600 * app.session_expire_after_hours());
app.time_now() + duration
}
fn should_renew<A: App>(app: &mut A, expires: A::DateTime) -> bool {
let renewal_period_hours = app.session_expire_after_hours() - app.session_renew_after_hours();
let renewal_period = Duration::from_secs(3600 * renewal_period_hours as u64);
app.time_now() + renewal_period >= expires
}