1pub mod recovery;
9pub mod totp;
10
11use std::marker::PhantomData;
12use std::sync::Arc;
13
14use argon2::{
15 password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString},
16 Argon2, PasswordHash,
17};
18use async_trait::async_trait;
19use axum::extract::{FromRef, FromRequestParts};
20use axum::http::request::Parts;
21use parking_lot::RwLock;
22use serde::{de::DeserializeOwned, Serialize};
23use tower_sessions::Session;
24
25use crate::container::Container;
26use crate::Error;
27
28pub const SESSION_USER_ID_KEY: &str = "_auth.user_id";
29
30#[async_trait]
36pub trait Authenticatable: Send + Sync + Sized + Clone + 'static {
37 type Id: Serialize + DeserializeOwned + Send + Sync + Clone + 'static;
38
39 fn id(&self) -> Self::Id;
41
42 async fn find_by_id(container: &Container, id: &Self::Id) -> Result<Option<Self>, Error>;
44
45 async fn find_by_credentials(
48 container: &Container,
49 identifier: &str,
50 ) -> Result<Option<(Self, String)>, Error>;
51}
52
53#[derive(Default, Clone)]
56pub struct AuthManager {
57 #[allow(dead_code)]
58 inner: Arc<RwLock<AuthInner>>,
59}
60
61#[derive(Default)]
62struct AuthInner {
63 #[allow(dead_code)]
64 hasher_pepper: Option<String>,
65}
66
67impl AuthManager {
68 pub fn new() -> Self {
69 Self::default()
70 }
71
72 pub fn with_pepper(self, pepper: impl Into<String>) -> Self {
73 self.inner.write().hasher_pepper = Some(pepper.into());
74 self
75 }
76}
77
78pub fn hash_password(plain: &str) -> Result<String, Error> {
80 let salt = SaltString::generate(&mut OsRng);
81 let argon2 = Argon2::default();
82 argon2
83 .hash_password(plain.as_bytes(), &salt)
84 .map(|h| h.to_string())
85 .map_err(|e| Error::Internal(format!("password hash failed: {e}")))
86}
87
88pub fn verify_password(plain: &str, hash: &str) -> bool {
90 let Ok(parsed) = PasswordHash::new(hash) else {
91 return false;
92 };
93 Argon2::default()
94 .verify_password(plain.as_bytes(), &parsed)
95 .is_ok()
96}
97
98pub async fn attempt<U: Authenticatable>(
102 container: &Container,
103 identifier: &str,
104 password: &str,
105) -> Result<Option<U>, Error> {
106 let Some((user, hash)) = U::find_by_credentials(container, identifier).await? else {
107 return Ok(None);
108 };
109 if verify_password(password, &hash) {
110 Ok(Some(user))
111 } else {
112 Ok(None)
113 }
114}
115
116pub async fn login<U: Authenticatable>(session: &Session, user: &U) -> Result<(), Error> {
118 let id = user.id();
119 session
120 .insert(SESSION_USER_ID_KEY, id)
121 .await
122 .map_err(|e| Error::Internal(format!("session write failed: {e}")))?;
123 Ok(())
124}
125
126pub async fn logout(session: &Session) -> Result<(), Error> {
128 session
129 .remove::<serde_json::Value>(SESSION_USER_ID_KEY)
130 .await
131 .map_err(|e| Error::Internal(format!("session clear failed: {e}")))?;
132 Ok(())
133}
134
135pub struct Auth<U: Authenticatable>(pub U);
143
144#[async_trait]
145impl<U, S> FromRequestParts<S> for Auth<U>
146where
147 U: Authenticatable,
148 Container: FromRef<S>,
149 S: Send + Sync,
150{
151 type Rejection = Error;
152
153 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
154 let session = Session::from_request_parts(parts, state)
155 .await
156 .map_err(|_| Error::Unauthenticated)?;
157 let id: Option<U::Id> = session
158 .get(SESSION_USER_ID_KEY)
159 .await
160 .map_err(|e| Error::Internal(e.to_string()))?;
161 let id = id.ok_or(Error::Unauthenticated)?;
162 let container = Container::from_ref(state);
163 let user = U::find_by_id(&container, &id)
164 .await?
165 .ok_or(Error::Unauthenticated)?;
166 Ok(Auth(user))
167 }
168}
169
170pub struct OptionalAuth<U: Authenticatable>(pub Option<U>);
174
175#[async_trait]
176impl<U, S> FromRequestParts<S> for OptionalAuth<U>
177where
178 U: Authenticatable,
179 Container: FromRef<S>,
180 S: Send + Sync,
181{
182 type Rejection = Error;
183
184 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
185 let Ok(session) = Session::from_request_parts(parts, state).await else {
186 return Ok(OptionalAuth(None));
187 };
188 let Some(id): Option<U::Id> = session
189 .get(SESSION_USER_ID_KEY)
190 .await
191 .map_err(|e| Error::Internal(e.to_string()))?
192 else {
193 return Ok(OptionalAuth(None));
194 };
195 let container = Container::from_ref(state);
196 let user = U::find_by_id(&container, &id).await?;
197 Ok(OptionalAuth(user))
198 }
199}
200
201pub trait Policy<U, S> {
203 fn check(user: &U, ability: &str, subject: &S) -> bool;
204}
205
206pub fn authorize<P, U, S>(user: &U, ability: &str, subject: &S) -> Result<(), Error>
208where
209 P: Policy<U, S>,
210{
211 if P::check(user, ability, subject) {
212 Ok(())
213 } else {
214 Err(Error::forbidden(ability))
215 }
216}
217
218pub struct WebGuard;
221pub struct ApiGuard;
222
223pub trait Guard: Send + Sync + 'static {}
224impl Guard for WebGuard {}
225impl Guard for ApiGuard {}
226
227pub struct Guarded<U, G>(PhantomData<(U, G)>);