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,16 +44,47 @@ 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()
 | 
			
		||||
        {
 | 
			
		||||
            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
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .map_err(AppError::from)
 | 
			
		||||
| 
						 | 
				
			
			@ -64,24 +95,6 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
 | 
			
		|||
                Err(err) => {
 | 
			
		||||
                    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 {
 | 
			
		||||
        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()
 | 
			
		||||
        (jar, next.run(req).await).into_response()
 | 
			
		||||
    } else {
 | 
			
		||||
            res
 | 
			
		||||
        }
 | 
			
		||||
    } 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