feat: improve authentication flow
authentication now works either with sessionid or basic auth, with basic auth not creating a sessionepisode-actions
parent
2f974fd1ff
commit
648921837b
|
@ -896,6 +896,7 @@ dependencies = [
|
|||
"axum-extra",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cookie",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"libsqlite3-sys",
|
||||
|
|
|
@ -9,6 +9,7 @@ axum = { version = "0.8.1", features = ["macros"] }
|
|||
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
clap = { version = "4.5.30", features = ["derive", "env"] }
|
||||
cookie = "0.18.1"
|
||||
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||
libsqlite3-sys = { version = "0.31.0", features = ["bundled"] }
|
||||
|
|
|
@ -20,6 +20,30 @@ impl From<diesel::result::Error> for gpodder::AuthErr {
|
|||
}
|
||||
|
||||
impl gpodder::AuthRepository for SqliteRepository {
|
||||
fn validate_credentials(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<gpodder::models::User, AuthErr> {
|
||||
if let Some(user) = users::table
|
||||
.select(db::User::as_select())
|
||||
.filter(users::username.eq(username))
|
||||
.first(&mut self.pool.get()?)
|
||||
.optional()?
|
||||
{
|
||||
if user.verify_password(password) {
|
||||
Ok(gpodder::User {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
})
|
||||
} else {
|
||||
Err(gpodder::AuthErr::InvalidPassword)
|
||||
}
|
||||
} else {
|
||||
Err(gpodder::AuthErr::UnknownUser)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_session(&self, session_id: i64) -> Result<gpodder::User, gpodder::AuthErr> {
|
||||
match sessions::dsl::sessions
|
||||
.inner_join(users::table)
|
||||
|
|
|
@ -13,6 +13,10 @@ pub trait AuthRepository {
|
|||
/// Validate the given session ID and return its user.
|
||||
fn validate_session(&self, session_id: i64) -> Result<models::User, AuthErr>;
|
||||
|
||||
/// Validate the credentials, returning the user if the credentials are correct.
|
||||
fn validate_credentials(&self, username: &str, password: &str)
|
||||
-> Result<models::User, AuthErr>;
|
||||
|
||||
/// Create a new session for the given user.
|
||||
fn create_session(
|
||||
&self,
|
||||
|
|
|
@ -44,44 +44,57 @@ pub fn router(ctx: Context) -> Router<Context> {
|
|||
|
||||
/// This middleware accepts
|
||||
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
||||
// SAFETY: this extractor's error type is Infallible
|
||||
let jar: CookieJar = req.extract_parts().await.unwrap();
|
||||
let mut auth_user = None;
|
||||
let mut new_session_id = None;
|
||||
tracing::debug!("{:?}", req.headers());
|
||||
|
||||
// SAFETY: this extractor's error type is Infallible
|
||||
let mut jar: CookieJar = req.extract_parts().await.unwrap();
|
||||
let mut auth_user = None;
|
||||
|
||||
// First try to validate the session
|
||||
if let Some(session_id) = jar
|
||||
.get(SESSION_ID_COOKIE)
|
||||
.and_then(|c| c.value().parse::<i64>().ok())
|
||||
{
|
||||
match tokio::task::spawn_blocking(move || ctx.repo.validate_session(session_id))
|
||||
let ctx_clone = ctx.clone();
|
||||
match tokio::task::spawn_blocking(move || ctx_clone.repo.validate_session(session_id))
|
||||
.await
|
||||
.unwrap()
|
||||
.map_err(AppError::from)
|
||||
{
|
||||
Ok(user) => {
|
||||
auth_user = Some(user);
|
||||
}
|
||||
Err(gpodder::AuthErr::UnknownSession) => {
|
||||
jar = jar.add(
|
||||
Cookie::build((SESSION_ID_COOKIE, String::new()))
|
||||
.max_age(cookie::time::Duration::ZERO),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
return err.into_response();
|
||||
return AppError::from(err).into_response();
|
||||
}
|
||||
};
|
||||
} else if let Ok(auth) = req
|
||||
.extract_parts::<TypedHeader<Authorization<Basic>>>()
|
||||
.await
|
||||
{
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
ctx.repo.create_session(auth.username(), auth.password())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.map_err(AppError::from)
|
||||
}
|
||||
|
||||
// Only if the sessionid wasn't present or valid do we check the credentials.
|
||||
if auth_user.is_none() {
|
||||
if let Ok(auth) = req
|
||||
.extract_parts::<TypedHeader<Authorization<Basic>>>()
|
||||
.await
|
||||
{
|
||||
Ok((session_id, user)) => {
|
||||
auth_user = Some(user);
|
||||
new_session_id = Some(session_id);
|
||||
}
|
||||
Err(err) => {
|
||||
return err.into_response();
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
ctx.repo
|
||||
.validate_credentials(auth.username(), auth.password())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.map_err(AppError::from)
|
||||
{
|
||||
Ok(user) => {
|
||||
auth_user = Some(user);
|
||||
}
|
||||
Err(err) => {
|
||||
return err.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,22 +102,9 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
|
|||
if let Some(user) = auth_user {
|
||||
req.extensions_mut().insert(user);
|
||||
|
||||
let res = next.run(req).await;
|
||||
|
||||
if let Some(session_id) = new_session_id {
|
||||
(
|
||||
jar.add(
|
||||
Cookie::build((SESSION_ID_COOKIE, session_id.to_string()))
|
||||
.expires(Expiration::Session),
|
||||
),
|
||||
res,
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
res
|
||||
}
|
||||
(jar, next.run(req).await).into_response()
|
||||
} else {
|
||||
let mut res = StatusCode::UNAUTHORIZED.into_response();
|
||||
let mut res = (jar, StatusCode::UNAUTHORIZED).into_response();
|
||||
|
||||
// This is what the gpodder.net service returns, and some clients seem to depend on it
|
||||
res.headers_mut().insert(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SubscriptionDelta {
|
||||
pub add: Vec<String>,
|
||||
pub remove: Vec<String>,
|
||||
|
|
Loading…
Reference in New Issue