use std::{ops::Deref, sync::Arc}; use anyhow::Context; use async_trait::async_trait; use axum::http::{header::SET_COOKIE, HeaderMap}; use oauth2::url::Url; use crate::{ introspection::IntrospectionService, oauth::{OAuth, ZitadelConfig}, session::{SessionService, User}, AuthClap, AuthEngine, }; #[async_trait] pub trait Auth { async fn login(&self) -> anyhow::Result; async fn login_token(&self, user: &str, password: &str) -> anyhow::Result; async fn login_authorized(&self, code: &str, state: &str) -> anyhow::Result<(HeaderMap, Url)>; async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result; } #[derive(Clone)] pub struct AuthService(Arc); impl AuthService { pub async fn new(config: &AuthClap, session: SessionService) -> anyhow::Result { match config.engine { AuthEngine::Noop => Ok(Self::new_noop()), AuthEngine::Zitadel => { let oauth: OAuth = ZitadelConfig::try_from(config.zitadel.clone())?.into(); let introspection: IntrospectionService = IntrospectionService::new_zitadel(config).await?; Ok(Self::new_zitadel(oauth, introspection, session)) } } } pub fn new_zitadel( oauth: OAuth, introspection: IntrospectionService, session: SessionService, ) -> Self { Self(Arc::new(ZitadelAuthService { oauth, introspection, session, })) } pub fn new_noop() -> Self { Self(Arc::new(NoopAuthService {})) } } impl Deref for AuthService { type Target = Arc; fn deref(&self) -> &Self::Target { &self.0 } } pub struct ZitadelAuthService { oauth: OAuth, introspection: IntrospectionService, session: SessionService, } pub static COOKIE_NAME: &str = "SESSION"; #[async_trait] impl Auth for ZitadelAuthService { async fn login(&self) -> anyhow::Result { let authorize_url = self.oauth.authorize_url().await?; Ok(authorize_url) } async fn login_authorized(&self, code: &str, _state: &str) -> anyhow::Result<(HeaderMap, Url)> { let token = self.oauth.exchange(code).await?; let user_id = self.introspection.get_id_token(token.as_str()).await?; let cookie_value = self.session.insert_user("user", user_id.as_str()).await?; let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie_value); let mut headers = HeaderMap::new(); headers.insert(SET_COOKIE, cookie.parse().unwrap()); Ok(( headers, Url::parse("http://localhost:3000/dash/home") .context("failed to parse login_authorized zitadel return url")?, )) } async fn login_token(&self, _user: &str, password: &str) -> anyhow::Result { self.introspection.get_id_token(password).await } async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result { match self.session.get_user(cookie).await? { Some(u) => Ok(User { id: u }), None => Err(anyhow::anyhow!("failed to find user")), } } } pub struct NoopAuthService {} #[async_trait] impl Auth for NoopAuthService { async fn login(&self) -> anyhow::Result { todo!() } async fn login_authorized( &self, _code: &str, _state: &str, ) -> anyhow::Result<(HeaderMap, Url)> { todo!() } async fn login_token(&self, _user: &str, _password: &str) -> anyhow::Result { todo!() } async fn get_user_from_session(&self, _cookie: &str) -> anyhow::Result { todo!() } }