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",
|
"axum-extra",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"cookie",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
|
|
|
@ -9,6 +9,7 @@ axum = { version = "0.8.1", features = ["macros"] }
|
||||||
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
clap = { version = "4.5.30", features = ["derive", "env"] }
|
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 = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||||
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||||
libsqlite3-sys = { version = "0.31.0", features = ["bundled"] }
|
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 {
|
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> {
|
fn validate_session(&self, session_id: i64) -> Result<gpodder::User, gpodder::AuthErr> {
|
||||||
match sessions::dsl::sessions
|
match sessions::dsl::sessions
|
||||||
.inner_join(users::table)
|
.inner_join(users::table)
|
||||||
|
|
|
@ -13,6 +13,10 @@ pub trait AuthRepository {
|
||||||
/// Validate the given session ID and return its user.
|
/// Validate the given session ID and return its user.
|
||||||
fn validate_session(&self, session_id: i64) -> Result<models::User, AuthErr>;
|
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.
|
/// Create a new session for the given user.
|
||||||
fn create_session(
|
fn create_session(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -44,16 +44,47 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
|
|
||||||
/// This middleware accepts
|
/// This middleware accepts
|
||||||
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
|
||||||
// SAFETY: this extractor's error type is Infallible
|
tracing::debug!("{:?}", req.headers());
|
||||||
let jar: CookieJar = req.extract_parts().await.unwrap();
|
|
||||||
let mut auth_user = None;
|
|
||||||
let mut new_session_id = None;
|
|
||||||
|
|
||||||
|
// 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
|
if let Some(session_id) = jar
|
||||||
.get(SESSION_ID_COOKIE)
|
.get(SESSION_ID_COOKIE)
|
||||||
.and_then(|c| c.value().parse::<i64>().ok())
|
.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()
|
||||||
|
{
|
||||||
|
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 AppError::from(err).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
ctx.repo
|
||||||
|
.validate_credentials(auth.username(), auth.password())
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map_err(AppError::from)
|
.map_err(AppError::from)
|
||||||
|
@ -64,24 +95,6 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return err.into_response();
|
return 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)
|
|
||||||
{
|
|
||||||
Ok((session_id, user)) => {
|
|
||||||
auth_user = Some(user);
|
|
||||||
new_session_id = Some(session_id);
|
|
||||||
}
|
|
||||||
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 {
|
if let Some(user) = auth_user {
|
||||||
req.extensions_mut().insert(user);
|
req.extensions_mut().insert(user);
|
||||||
|
|
||||||
let res = next.run(req).await;
|
(jar, next.run(req).await).into_response()
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
res
|
let mut res = (jar, StatusCode::UNAUTHORIZED).into_response();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut res = StatusCode::UNAUTHORIZED.into_response();
|
|
||||||
|
|
||||||
// This is what the gpodder.net service returns, and some clients seem to depend on it
|
// This is what the gpodder.net service returns, and some clients seem to depend on it
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct SubscriptionDelta {
|
pub struct SubscriptionDelta {
|
||||||
pub add: Vec<String>,
|
pub add: Vec<String>,
|
||||||
pub remove: Vec<String>,
|
pub remove: Vec<String>,
|
||||||
|
|
Loading…
Reference in New Issue