login & logout

This commit is contained in:
2026-02-27 23:46:37 +01:00
parent 969401658f
commit b6a211bbcf
3 changed files with 84 additions and 42 deletions

View File

@@ -8,7 +8,7 @@ use serde::Deserialize;
use crate::users::{ use crate::users::{
User, User,
auth::{ auth::{
AuthError, COOKIE_NAME, UserAuthRequired, UserAuthenticate, AuthError, COOKIE_NAME, SessionAuthRequired, SessionAuthenticate, UserAuthRequired,
implementation::authenticate_via_credentials, implementation::authenticate_via_credentials,
}, },
sessions::Session, sessions::Session,
@@ -38,5 +38,8 @@ pub async fn login(Json(creds): Json<LoginForm>) -> Result<Response, AuthError>
} }
pub async fn logout(headers: HeaderMap) -> Result<Response, AuthError> { pub async fn logout(headers: HeaderMap) -> Result<Response, AuthError> {
todo!() let mut s = Session::authenticate(&headers)?.required()?;
s.revoke(Some(&User::get_by_id(s.user_id)?))?;
let cookie = format!("{COOKIE_NAME}=revoking; Path=/; HttpOnly; Max-Age=0");
Ok(([(header::SET_COOKIE, cookie)], "Logged out!").into_response())
} }

View File

@@ -15,8 +15,8 @@ use crate::{
users::{ users::{
User, User,
auth::{ auth::{
AuthError, COOKIE_NAME, TokenSize, UserAuthDummyData, UserAuthRequired, AuthError, COOKIE_NAME, SessionAuthRequired, SessionAuthenticate, TokenSize,
UserAuthenticate, UserPasswordHashing, UserAuthDummyData, UserAuthRequired, UserAuthenticate, UserPasswordHashing,
}, },
sessions::Session, sessions::Session,
}, },
@@ -63,6 +63,14 @@ impl UserAuthRequired for Option<User> {
} }
} }
} }
impl SessionAuthRequired for Option<Session> {
fn required(self) -> Result<Session, AuthError> {
match self {
Self::None => Err(AuthError::AuthRequired),
Self::Some(s) => Ok(s),
}
}
}
impl UserPasswordHashing for User { impl UserPasswordHashing for User {
fn hash_password(passw: &str) -> Result<String, argon2::password_hash::Error> { fn hash_password(passw: &str) -> Result<String, argon2::password_hash::Error> {
@@ -122,49 +130,63 @@ impl<'a> AuthScheme<'a> {
impl UserAuthenticate for User { impl UserAuthenticate for User {
fn authenticate(headers: &HeaderMap) -> Result<Option<User>, AuthError> { fn authenticate(headers: &HeaderMap) -> Result<Option<User>, AuthError> {
let mut auth_values = Vec::new(); let (basic_auth, bearer_auth) = auth_common(&headers);
for auth_header in headers.get_all(AUTHORIZATION).iter() {
if let Ok(s) = auth_header.to_str() {
auth_values.push(s.to_string());
}
}
for cookie_header in headers.get_all(COOKIE).iter() {
if let Ok(cookies) = cookie_header.to_str() {
for cookie in cookies.split(';') {
let cookie = cookie.trim();
if let Some(value) = cookie.strip_prefix(&format!("{}=", COOKIE_NAME)) {
auth_values.push(format!("Bearer {}", value));
}
}
}
}
let mut basic_auth: Option<&str> = None;
let mut bearer_auth: Option<&str> = None;
for header in &auth_values {
let header = header.trim();
match AuthScheme::from_header(header) {
AuthScheme::Basic(creds) => {
if basic_auth.is_none() {
basic_auth = Some(creds);
}
}
AuthScheme::Bearer(token) => {
if bearer_auth.is_none() {
bearer_auth = Some(token);
}
}
AuthScheme::None => {}
}
}
match (basic_auth, bearer_auth) { match (basic_auth, bearer_auth) {
(Some(creds), _) => authenticate_basic(creds), (Some(creds), _) => authenticate_basic(&creds),
(None, Some(token)) => authenticate_bearer(token), (None, Some(token)) => authenticate_bearer(&token),
_ => Ok(None), _ => Ok(None),
} }
} }
} }
impl SessionAuthenticate for Session {
fn authenticate(headers: &HeaderMap) -> Result<Option<Session>, AuthError> {
let (_, bearer_auth) = auth_common(&headers);
if let Some(token) = bearer_auth {
authenticate_bearer_with_session(&token)
} else {
Ok(None)
}
}
}
fn auth_common(headers: &HeaderMap) -> (Option<String>, Option<String>) {
let mut auth_values = Vec::new();
for auth_header in headers.get_all(AUTHORIZATION).iter() {
if let Ok(s) = auth_header.to_str() {
auth_values.push(s.to_string());
}
}
for cookie_header in headers.get_all(COOKIE).iter() {
if let Ok(cookies) = cookie_header.to_str() {
for cookie in cookies.split(';') {
let cookie = cookie.trim();
if let Some(value) = cookie.strip_prefix(&format!("{}=", COOKIE_NAME)) {
auth_values.push(format!("Bearer {}", value));
}
}
}
}
let mut basic_auth: Option<String> = None;
let mut bearer_auth: Option<String> = None;
for header in &auth_values {
let header = header.trim();
match AuthScheme::from_header(header) {
AuthScheme::Basic(creds) => {
if basic_auth.is_none() {
basic_auth = Some(creds.into());
}
}
AuthScheme::Bearer(token) => {
if bearer_auth.is_none() {
bearer_auth = Some(token.into());
}
}
AuthScheme::None => {}
}
}
(basic_auth, bearer_auth)
}
fn authenticate_basic(credentials: &str) -> Result<Option<User>, AuthError> { fn authenticate_basic(credentials: &str) -> Result<Option<User>, AuthError> {
let decoded = BASE64_STANDARD.decode(credentials)?; let decoded = BASE64_STANDARD.decode(credentials)?;
@@ -205,3 +227,11 @@ fn authenticate_bearer(token: &str) -> Result<Option<User>, AuthError> {
s.prolong()?; s.prolong()?;
Ok(Some(User::get_by_id(s.user_id)?)) Ok(Some(User::get_by_id(s.user_id)?))
} }
fn authenticate_bearer_with_session(token: &str) -> Result<Option<Session>, AuthError> {
let mut s = Session::get_by_token(token)?;
if s.is_expired_or_revoked() {
return Err(AuthError::InvalidCredentials);
}
s.prolong()?;
Ok(Some(s))
}

View File

@@ -1,7 +1,10 @@
use axum::http::HeaderMap; use axum::http::HeaderMap;
use rand08::{RngCore, rngs::OsRng}; use rand08::{RngCore, rngs::OsRng};
use crate::users::{User, UserError, sessions::SessionError}; use crate::users::{
User, UserError,
sessions::{Session, SessionError},
};
pub mod implementation; pub mod implementation;
@@ -13,6 +16,12 @@ pub trait UserAuthenticate {
pub trait UserAuthRequired { pub trait UserAuthRequired {
fn required(self) -> Result<User, AuthError>; fn required(self) -> Result<User, AuthError>;
} }
pub trait SessionAuthenticate {
fn authenticate(headers: &HeaderMap) -> Result<Option<Session>, AuthError>;
}
pub trait SessionAuthRequired {
fn required(self) -> Result<Session, AuthError>;
}
pub trait UserPasswordHashing { pub trait UserPasswordHashing {
/// Returns the hashed password as a String /// Returns the hashed password as a String