#[cfg(feature = "db")]
pub mod db;
use std::any::Any;
use std::borrow::Cow;
use std::sync::{Arc, Mutex, MutexGuard};
use async_trait::async_trait;
use chrono::{DateTime, FixedOffset};
use cot_core::error::impl_into_cot_error;
use derive_more::with_trait::Debug;
#[cfg(test)]
use mockall::automock;
use password_auth::VerifyError;
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use thiserror::Error;
use crate::config::SecretKey;
#[cfg(feature = "db")]
use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue};
use crate::request::{Request, RequestExt};
use crate::session::Session;
const ERROR_PREFIX: &str = "failed to authenticate user:";
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AuthError {
#[error("{ERROR_PREFIX} password hash is invalid")]
PasswordHashInvalid,
#[error("{ERROR_PREFIX} error while accessing the session object")]
SessionAccess(#[from] tower_sessions::session::Error),
#[error("{ERROR_PREFIX} error while accessing the user object")]
UserBackend(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("{ERROR_PREFIX} tried to authenticate with an unsupported credentials type")]
CredentialsTypeNotSupported,
#[error("{ERROR_PREFIX} tried to get a user by an unsupported user ID type")]
UserIdTypeNotSupported,
}
impl_into_cot_error!(AuthError, UNAUTHORIZED);
impl AuthError {
pub fn backend_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::UserBackend(Box::new(error))
}
}
pub type Result<T> = std::result::Result<T, AuthError>;
#[cfg_attr(test, automock)]
pub trait User {
fn id(&self) -> Option<UserId> {
None
}
#[expect(clippy::elidable_lifetime_names)]
fn username<'a>(&'a self) -> Option<Cow<'a, str>> {
None
}
fn is_active(&self) -> bool {
false
}
fn is_authenticated(&self) -> bool {
false
}
fn last_login(&self) -> Option<DateTime<FixedOffset>> {
None
}
fn joined(&self) -> Option<DateTime<FixedOffset>> {
None
}
#[expect(unused_variables)]
fn session_auth_hash(&self, secret_key: &SecretKey) -> Option<SessionAuthHash> {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserId {
Int(i64),
String(String),
}
impl UserId {
#[must_use]
pub fn as_int(&self) -> Option<i64> {
match self {
Self::Int(id) => Some(*id),
Self::String(_) => None,
}
}
#[must_use]
pub fn as_string(&self) -> Option<&str> {
match self {
Self::Int(_) => None,
Self::String(id) => Some(id),
}
}
}
#[repr(transparent)]
struct UserWrapper(Arc<dyn User + Send + Sync>);
impl Debug for UserWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Arc<dyn User + Send + Sync>")
.field("id", &self.0.id())
.field("username", &self.0.username())
.field("is_active", &self.0.is_active())
.field("is_authenticated", &self.0.is_authenticated())
.field("last_login", &self.0.last_login())
.field("joined", &self.0.joined())
.finish()
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct AnonymousUser;
impl PartialEq for AnonymousUser {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl User for AnonymousUser {}
#[repr(transparent)]
#[derive(Clone)]
pub struct SessionAuthHash(Box<[u8]>);
impl SessionAuthHash {
#[must_use]
pub fn new(hash: &[u8]) -> Self {
Self(Box::from(hash))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[must_use]
pub fn into_bytes(self) -> Box<[u8]> {
self.0
}
}
impl From<&[u8]> for SessionAuthHash {
fn from(hash: &[u8]) -> Self {
Self::new(hash)
}
}
impl PartialEq for SessionAuthHash {
fn eq(&self, other: &Self) -> bool {
self.0.ct_eq(&other.0).into()
}
}
impl Eq for SessionAuthHash {}
impl Debug for SessionAuthHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SessionAuthHash")
.field(&"**********")
.finish()
}
}
#[repr(transparent)]
#[derive(Clone)]
pub struct PasswordHash(String);
impl PasswordHash {
pub fn new<T: Into<String>>(hash: T) -> Result<Self> {
let hash = hash.into();
if hash.len() > MAX_PASSWORD_HASH_LENGTH as usize {
return Err(AuthError::PasswordHashInvalid);
}
password_auth::is_hash_obsolete(&hash).map_err(|_| AuthError::PasswordHashInvalid)?;
Ok(Self(hash))
}
#[must_use]
pub fn from_password(password: &crate::common_types::Password) -> Self {
let hash = password_auth::generate_hash(password.as_str());
if hash.len() > MAX_PASSWORD_HASH_LENGTH as usize {
unreachable!("password hash should never exceed {MAX_PASSWORD_HASH_LENGTH} bytes");
}
Self(hash)
}
pub fn verify(&self, password: &crate::common_types::Password) -> PasswordVerificationResult {
const VALID_ERROR_STR: &str = "password hash should always be valid if created with `PasswordHash::new` or `PasswordHash::from_password`";
match password_auth::verify_password(password.as_str(), &self.0) {
Ok(()) => {
let Ok(is_obsolete) = password_auth::is_hash_obsolete(&self.0) else {
unreachable!("{VALID_ERROR_STR}");
};
if is_obsolete {
PasswordVerificationResult::OkObsolete(PasswordHash::from_password(password))
} else {
PasswordVerificationResult::Ok
}
}
Err(error) => match error {
VerifyError::PasswordInvalid => PasswordVerificationResult::Invalid,
VerifyError::Parse(_) => unreachable!("{VALID_ERROR_STR}"),
},
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl TryFrom<String> for PasswordHash {
type Error = AuthError;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Debug, Clone)]
#[must_use]
pub enum PasswordVerificationResult {
Ok,
OkObsolete(PasswordHash),
Invalid,
}
impl Debug for PasswordHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("PasswordHash")
.field(&format!("{}**********", &self.0[..10]))
.finish()
}
}
const MAX_PASSWORD_HASH_LENGTH: u32 = 128;
#[cfg(feature = "db")]
impl DatabaseField for PasswordHash {
const TYPE: ColumnType = ColumnType::String(MAX_PASSWORD_HASH_LENGTH);
}
#[cfg(feature = "db")]
impl FromDbValue for PasswordHash {
#[cfg(feature = "sqlite")]
fn from_sqlite(value: crate::db::impl_sqlite::SqliteValueRef<'_>) -> cot::db::Result<Self> {
PasswordHash::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
#[cfg(feature = "postgres")]
fn from_postgres(
value: crate::db::impl_postgres::PostgresValueRef<'_>,
) -> cot::db::Result<Self> {
PasswordHash::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
#[cfg(feature = "mysql")]
fn from_mysql(value: crate::db::impl_mysql::MySqlValueRef<'_>) -> crate::db::Result<Self>
where
Self: Sized,
{
PasswordHash::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
}
#[cfg(feature = "db")]
impl ToDbValue for PasswordHash {
fn to_db_value(&self) -> DbValue {
self.0.clone().into()
}
}
#[derive(Debug, Clone)]
pub struct Auth {
inner: Arc<AuthInner>,
}
impl Auth {
#[cfg(all(feature = "db", feature = "test"))]
pub(crate) async fn new(
session: Session,
backend: Arc<dyn AuthBackend>,
secret_key: SecretKey,
fallback_secret_keys: &[SecretKey],
) -> cot::Result<Self> {
let inner = AuthInner::new(session, backend, secret_key, fallback_secret_keys).await?;
Ok(Self {
inner: Arc::new(inner),
})
}
pub(crate) async fn from_request(request: &mut Request) -> cot::Result<Self> {
let inner = AuthInner::from_request(request).await?;
Ok(Self {
inner: Arc::new(inner),
})
}
#[must_use]
pub fn user(&self) -> Arc<dyn User + Send + Sync> {
self.inner.user()
}
pub async fn authenticate(
&self,
credentials: &(dyn Any + Send + Sync),
) -> Result<Option<Box<dyn User + Send + Sync>>> {
self.inner.authenticate(credentials).await
}
pub async fn login(&self, user: Box<dyn User + Send + Sync + 'static>) -> Result<()> {
self.inner.login(user).await
}
pub async fn logout(&self) -> Result<()> {
self.inner.logout().await
}
}
#[derive(Debug)]
struct AuthInner {
session: Session,
#[debug("..")]
backend: Arc<dyn AuthBackend>,
secret_key: SecretKey,
#[debug("..")]
user: Mutex<UserWrapper>,
}
impl AuthInner {
async fn new(
session: Session,
backend: Arc<dyn AuthBackend>,
secret_key: SecretKey,
fallback_secret_keys: &[SecretKey],
) -> cot::Result<Self> {
#[expect(trivial_casts)] let user = get_user_with_saved_id(&session, &*backend, &secret_key, fallback_secret_keys)
.await?
.map_or_else(
|| Arc::new(AnonymousUser) as Arc<dyn User + Send + Sync>,
Arc::from,
);
Ok(Self {
session,
backend,
secret_key,
user: Mutex::new(UserWrapper(user)),
})
}
async fn from_request(request: &mut Request) -> cot::Result<Self> {
let config = request.context().config();
let session = Session::from_request(request).clone();
let backend = request.context().auth_backend().clone();
let secret_key = config.secret_key.clone();
Self::new(session, backend, secret_key, &config.fallback_secret_keys).await
}
fn user(&self) -> Arc<dyn User + Send + Sync> {
Arc::clone(&self.user_lock().0)
}
async fn authenticate(
&self,
credentials: &(dyn Any + Send + Sync),
) -> Result<Option<Box<dyn User + Send + Sync>>> {
self.backend.authenticate(credentials).await
}
async fn login(&self, user: Box<dyn User + Send + Sync + 'static>) -> Result<()> {
self.session.cycle_id().await?;
if let Some(user_id) = user.id() {
self.session.insert(USER_ID_SESSION_KEY, user_id).await?;
}
let secret_key = &self.secret_key;
if let Some(session_auth_hash) = user.session_auth_hash(secret_key) {
self.session
.insert(SESSION_HASH_SESSION_KEY, session_auth_hash.as_bytes())
.await?;
}
*self.user_lock() = UserWrapper(Arc::from(user));
Ok(())
}
async fn logout(&self) -> Result<()> {
self.session.flush().await?;
*self.user_lock() = UserWrapper(Arc::new(AnonymousUser));
Ok(())
}
fn user_lock(&self) -> MutexGuard<'_, UserWrapper> {
self.user.lock().unwrap_or_else(|poison_error| {
self.user.clear_poison();
poison_error.into_inner()
})
}
}
const USER_ID_SESSION_KEY: &str = "__cot_auth_user_id";
const SESSION_HASH_SESSION_KEY: &str = "__cot_auth_session_hash";
async fn get_user_with_saved_id(
session: &Session,
auth_backend: &dyn AuthBackend,
secret_key: &SecretKey,
fallback_secret_keys: &[SecretKey],
) -> Result<Option<Box<dyn User + Send + Sync>>> {
let Some(user_id) = session.get::<UserId>(USER_ID_SESSION_KEY).await? else {
return Ok(None);
};
let Some(user) = auth_backend.get_by_id(user_id).await? else {
return Ok(None);
};
if session_auth_hash_valid(&*user, session, secret_key, fallback_secret_keys).await? {
Ok(Some(user))
} else {
Ok(None)
}
}
async fn session_auth_hash_valid(
user: &(dyn User + Send + Sync),
session: &Session,
secret_key: &SecretKey,
fallback_secret_keys: &[SecretKey],
) -> Result<bool> {
let Some(user_hash) = user.session_auth_hash(secret_key) else {
return Ok(true);
};
let stored_hash = session
.get::<Vec<u8>>(SESSION_HASH_SESSION_KEY)
.await?
.expect("Session hash should be present in the session object");
let stored_hash = SessionAuthHash::new(&stored_hash);
if user_hash == stored_hash {
return Ok(true);
}
for fallback_key in fallback_secret_keys {
let user_hash_fallback = user
.session_auth_hash(fallback_key)
.expect("User should have a session hash for each secret key");
if user_hash_fallback == stored_hash {
session
.insert(SESSION_HASH_SESSION_KEY, user_hash.as_bytes())
.await?;
return Ok(true);
}
}
Ok(false)
}
#[async_trait]
pub trait AuthBackend: Send + Sync {
async fn authenticate(
&self,
credentials: &(dyn Any + Send + Sync),
) -> Result<Option<Box<dyn User + Send + Sync>>>;
async fn get_by_id(&self, id: UserId) -> Result<Option<Box<dyn User + Send + Sync>>>;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct NoAuthBackend;
#[async_trait]
impl AuthBackend for NoAuthBackend {
async fn authenticate(
&self,
_credentials: &(dyn Any + Send + Sync),
) -> Result<Option<Box<dyn User + Send + Sync>>> {
Ok(None)
}
async fn get_by_id(&self, _id: UserId) -> Result<Option<Box<dyn User + Send + Sync>>> {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use mockall::predicate::eq;
use super::*;
use crate::common_types::Password;
use crate::config::ProjectConfig;
use crate::test::TestRequestBuilder;
struct MockAuthBackend<F> {
return_user: F,
}
#[async_trait]
impl<F: Fn() -> MockUser + Send + Sync + 'static> AuthBackend for MockAuthBackend<F> {
async fn authenticate(
&self,
_credentials: &(dyn Any + Send + Sync),
) -> Result<Option<Box<dyn User + Send + Sync>>> {
Ok(Some(Box::new((self.return_user)())))
}
async fn get_by_id(&self, _id: UserId) -> Result<Option<Box<dyn User + Send + Sync>>> {
Ok(Some(Box::new((self.return_user)())))
}
}
const TEST_KEY_1: &[u8] = b"key1";
const TEST_KEY_2: &[u8] = b"key2";
const TEST_KEY_3: &[u8] = b"key3";
fn test_request<T: Fn() -> MockUser + Send + Sync + 'static>(return_user: T) -> Request {
test_request_with_auth_backend(MockAuthBackend { return_user })
}
fn test_request_with_auth_backend<T: AuthBackend + 'static>(auth_backend: T) -> Request {
TestRequestBuilder::get("/")
.with_session()
.config(test_project_config(SecretKey::new(TEST_KEY_1), vec![]))
.auth_backend(auth_backend)
.build()
}
fn test_request_with_auth_config_and_session<T: AuthBackend + 'static>(
auth_backend: T,
config: ProjectConfig,
session_source: &Request,
) -> Request {
TestRequestBuilder::get("/")
.with_session_from(session_source)
.config(config)
.auth_backend(auth_backend)
.build()
}
fn test_project_config(secret_key: SecretKey, fallback_keys: Vec<SecretKey>) -> ProjectConfig {
ProjectConfig::builder()
.secret_key(secret_key)
.fallback_secret_keys(fallback_keys)
.clone()
.build()
}
#[test]
fn anonymous_user() {
let anonymous_user = AnonymousUser;
assert_eq!(anonymous_user.id(), None);
assert_eq!(anonymous_user.username(), None);
assert!(!anonymous_user.is_active());
assert!(!anonymous_user.is_authenticated());
assert_eq!(anonymous_user.last_login(), None);
assert_eq!(anonymous_user.joined(), None);
assert_eq!(
anonymous_user.session_auth_hash(&SecretKey::new(b"key")),
None
);
let anonymous_user2 = AnonymousUser;
assert_eq!(anonymous_user, anonymous_user2);
}
#[test]
#[cfg_attr(miri, ignore)]
fn password_hash() {
let password = Password::new("password".to_string());
let hash = PasswordHash::from_password(&password);
match hash.verify(&password) {
PasswordVerificationResult::Ok => {}
_ => panic!("Password hash verification failed"),
}
}
#[test]
fn session_auth_hash_debug() {
let hash = SessionAuthHash::from([1, 2, 3].as_ref());
assert_eq!(format!("{hash:?}"), "SessionAuthHash(\"**********\")");
}
const TEST_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$QAAI3EMU1eTLT9NzzBhQjg$khq4zuHsEyk9trGjuqMBFYnTbpqkmn0wXGxFn1nkPBc";
#[test]
#[cfg_attr(miri, ignore)]
fn password_hash_debug() {
let hash = PasswordHash::new(TEST_PASSWORD_HASH).unwrap();
assert_eq!(
format!("{hash:?}"),
"PasswordHash(\"$argon2id$**********\")"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn password_hash_verify() {
let password = Password::new("password");
let hash = PasswordHash::from_password(&password);
match hash.verify(&password) {
PasswordVerificationResult::Ok => {}
_ => panic!("Password hash verification failed"),
}
let wrong_password = Password::new("wrongpassword");
match hash.verify(&wrong_password) {
PasswordVerificationResult::Invalid => {}
_ => panic!("Password hash verification failed"),
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn password_hash_str() {
let hash = PasswordHash::new(TEST_PASSWORD_HASH).unwrap();
assert_eq!(hash.as_str(), TEST_PASSWORD_HASH);
assert_eq!(hash.into_string(), TEST_PASSWORD_HASH);
let hash = PasswordHash::try_from(TEST_PASSWORD_HASH.to_string()).unwrap();
assert_eq!(hash.as_str(), TEST_PASSWORD_HASH);
assert_eq!(hash.into_string(), TEST_PASSWORD_HASH);
}
#[cot::test]
async fn user_anonymous() {
let mut request = test_request_with_auth_backend(NoAuthBackend {});
let auth = Auth::from_request(&mut request).await.unwrap();
let user = auth.user();
assert!(!user.is_authenticated());
assert!(!user.is_active());
}
#[cot::test]
async fn user() {
let mut request = test_request(|| {
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(UserId::Int(1));
mock_user.expect_session_auth_hash().return_const(None);
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser")));
mock_user
});
Session::from_request(&request)
.insert(USER_ID_SESSION_KEY, UserId::Int(1))
.await
.unwrap();
let auth = Auth::from_request(&mut request).await.unwrap();
assert_eq!(auth.user().username(), Some(Cow::from("mockuser")));
}
#[cot::test]
async fn authenticate() {
let mut request = test_request(|| {
let mut mock_user = MockUser::new();
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser")));
mock_user
});
let auth = Auth::from_request(&mut request).await.unwrap();
let credentials: &(dyn Any + Send + Sync) = &();
let user = auth.authenticate(credentials).await.unwrap().unwrap();
assert_eq!(user.username(), Some(Cow::from("mockuser")));
}
#[cot::test]
async fn login_logout() {
let mut request = test_request(MockUser::new);
let session = Session::from_request(&request).clone();
let auth = Auth::from_request(&mut request).await.unwrap();
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(UserId::Int(1));
mock_user.expect_session_auth_hash().return_const(None);
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser")));
auth.login(Box::new(mock_user)).await.unwrap();
assert_eq!(auth.user().username(), Some(Cow::from("mockuser")));
assert!(!session.is_empty().await);
auth.logout().await.unwrap();
assert!(auth.user().username().is_none());
assert!(session.is_empty().await);
}
#[cot::test]
async fn login_cycle_id() {
let mut request = test_request(MockUser::new);
let session = Session::from_request(&request).clone();
let auth = Auth::from_request(&mut request).await.unwrap();
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(UserId::Int(1));
mock_user.expect_session_auth_hash().return_const(None);
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser_1")));
auth.login(Box::new(mock_user)).await.unwrap();
session.save().await.unwrap();
let id_1 = session.id();
assert!(id_1.is_some());
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(UserId::Int(2));
mock_user.expect_session_auth_hash().return_const(None);
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser_2")));
auth.login(Box::new(mock_user)).await.unwrap();
session.save().await.unwrap();
let id_2 = session.id();
assert!(id_2.is_some());
assert!(id_1 != id_2);
}
#[cot::test]
async fn logout_on_invalid_user_id_in_session() {
let mut request = test_request_with_auth_backend(NoAuthBackend {});
Session::from_request(&request)
.insert(USER_ID_SESSION_KEY, UserId::Int(1))
.await
.unwrap();
let auth = Auth::from_request(&mut request).await.unwrap();
let user = auth.user();
assert_eq!(user.username(), None);
assert!(!user.is_authenticated());
}
#[cot::test]
async fn logout_on_session_hash_change() {
let session_auth_hash = Arc::new(Mutex::new(SessionAuthHash::new(&[1, 2, 3])));
let session_auth_hash_clone = Arc::clone(&session_auth_hash);
let create_user = move || {
let session_auth_hash_clone = Arc::clone(&session_auth_hash_clone);
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(UserId::Int(1));
mock_user
.expect_session_auth_hash()
.returning(move |_| Some(session_auth_hash_clone.lock().unwrap().clone()));
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser")));
mock_user
};
let mut request = test_request(create_user.clone());
let auth = Auth::from_request(&mut request).await.unwrap();
auth.login(Box::new(create_user())).await.unwrap();
assert_eq!(auth.user().username(), Some(Cow::from("mockuser")));
let auth = Auth::from_request(&mut request).await.unwrap();
assert_eq!(auth.user().username(), Some(Cow::from("mockuser")));
*session_auth_hash.lock().unwrap() = SessionAuthHash::new(&[4, 5, 6]);
let auth = Auth::from_request(&mut request).await.unwrap();
let user = auth.user();
assert!(!user.is_authenticated());
assert_eq!(user.username(), None);
}
#[cot::test]
async fn user_secret_key_change() {
let create_user = move || {
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(UserId::Int(1));
mock_user
.expect_session_auth_hash()
.with(eq(SecretKey::new(TEST_KEY_1)))
.returning(move |_| Some(SessionAuthHash::new(&[1, 2, 3])));
mock_user
.expect_session_auth_hash()
.with(eq(SecretKey::new(TEST_KEY_2)))
.returning(move |_| Some(SessionAuthHash::new(&[4, 5, 6])));
mock_user
.expect_session_auth_hash()
.with(eq(SecretKey::new(TEST_KEY_3)))
.returning(move |_| Some(SessionAuthHash::new(&[7, 8, 9])));
mock_user
.expect_username()
.return_const(Some(Cow::from("mockuser")));
mock_user
};
let mut request = test_request(create_user);
let auth = Auth::from_request(&mut request).await.unwrap();
auth.login(Box::new(create_user())).await.unwrap();
let user = auth.user();
assert_eq!(user.username(), Some(Cow::from("mockuser")));
let replace_keys = move |request: &mut Request, secret_key, fallback_keys| {
let auth_backend = MockAuthBackend {
return_user: create_user,
};
let new_config = test_project_config(secret_key, fallback_keys);
*request = test_request_with_auth_config_and_session(auth_backend, new_config, request);
};
replace_keys(
&mut request,
SecretKey::new(TEST_KEY_2),
vec![SecretKey::new(TEST_KEY_1)],
);
let auth = Auth::from_request(&mut request).await.unwrap();
let user = auth.user();
assert_eq!(user.username(), Some(Cow::from("mockuser")));
replace_keys(&mut request, SecretKey::new(TEST_KEY_2), vec![]);
let auth = Auth::from_request(&mut request).await.unwrap();
let user = auth.user();
assert_eq!(user.username(), Some(Cow::from("mockuser")));
replace_keys(&mut request, SecretKey::new(TEST_KEY_3), vec![]);
let auth = Auth::from_request(&mut request).await.unwrap();
let user = auth.user();
assert_eq!(user.username(), None);
assert!(!user.is_authenticated());
}
#[test]
fn user_wrapper_with_anonymous_user() {
let anon_user = AnonymousUser;
let user_wrapper = UserWrapper(Arc::new(anon_user));
let debug_output = format!("{user_wrapper:?}");
assert!(debug_output.contains("Arc<dyn User + Send + Sync>"));
assert!(debug_output.contains("id: None"));
assert!(debug_output.contains("username: None"));
assert!(debug_output.contains("is_active: false"));
assert!(debug_output.contains("is_authenticated: false"));
assert!(debug_output.contains("last_login: None"));
assert!(debug_output.contains("joined: None"));
}
#[test]
fn test_user_wrapper_debug() {
let mut mock_user = MockUser::new();
mock_user.expect_id().return_const(Some(UserId::Int(42)));
mock_user
.expect_username()
.return_const(Some(Cow::from("test_user")));
mock_user.expect_is_active().return_const(true);
mock_user.expect_is_authenticated().return_const(true);
let now = DateTime::parse_from_rfc3339("2023-01-01T12:00:00+00:00").unwrap();
mock_user.expect_last_login().return_const(Some(now));
mock_user.expect_joined().return_const(Some(now));
let user_wrapper = UserWrapper(Arc::new(mock_user));
let debug_output = format!("{user_wrapper:?}");
assert!(debug_output.contains("Arc<dyn User + Send + Sync>"));
assert!(debug_output.contains("id: Some(Int(42))"));
assert!(debug_output.contains("username: Some(\"test_user\")"));
assert!(debug_output.contains("is_active: true"));
assert!(debug_output.contains("is_authenticated: true"));
assert!(debug_output.contains("2023-01-01T12:00:00+00:00"));
}
}