From e6084a7f4e9a2207fda62c5b7c055774379e27d9 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sun, 20 Aug 2023 14:08:40 +0200 Subject: [PATCH] feat(auth): add authentication integration Signed-off-by: kjuulh --- Cargo.lock | 8 +- como_api/src/controllers/auth.rs | 174 +++++++++++-------------- como_api/src/router.rs | 28 +--- como_api/src/zitadel/mod.rs | 89 ------------- como_auth/Cargo.toml | 10 +- como_auth/src/auth.rs | 107 +++++++++++++++ como_auth/src/introspection.rs | 33 ++++- como_auth/src/lib.rs | 41 +++++- como_auth/src/oauth.rs | 32 +++++ como_auth/src/session.rs | 108 +++++++++++++++ como_infrastructure/Cargo.toml | 2 +- como_infrastructure/src/configs/mod.rs | 4 + como_infrastructure/src/register.rs | 6 + 13 files changed, 409 insertions(+), 233 deletions(-) create mode 100644 como_auth/src/auth.rs create mode 100644 como_auth/src/session.rs diff --git a/Cargo.lock b/Cargo.lock index 909be40..db19eed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,24 +896,17 @@ name = "como_auth" version = "0.1.0" dependencies = [ "anyhow", - "async-graphql", - "async-graphql-axum", "async-sqlx-session", "async-trait", "axum", "axum-extra", "axum-sessions", "clap", - "como_core", - "como_domain", - "como_gql", - "como_infrastructure", "oauth2", "openidconnect", "pretty_assertions", "sealed_test", "serde", - "serde_json", "sqlx 0.6.3", "tokio", "tower", @@ -985,6 +978,7 @@ dependencies = [ "axum", "chrono", "clap", + "como_auth", "como_core", "como_domain", "rand_core", diff --git a/como_api/src/controllers/auth.rs b/como_api/src/controllers/auth.rs index 12b0088..5bf6a1c 100644 --- a/como_api/src/controllers/auth.rs +++ b/como_api/src/controllers/auth.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; +use std::fmt::Display; use crate::router::AppState; -use crate::zitadel::{IntrospectionConfig, IntrospectionState}; use async_sqlx_session::PostgresSessionStore; use axum::extract::{FromRef, FromRequestParts, Query, State}; @@ -10,9 +10,9 @@ use axum::headers::{Authorization, Cookie}; use axum::http::request::Parts; use axum::http::StatusCode; use axum::http::{header::SET_COOKIE, HeaderMap}; -use axum::response::{IntoResponse, Redirect}; +use axum::response::{ErrorResponse, IntoResponse, Redirect}; use axum::routing::get; -use axum::{async_trait, RequestPartsExt, Router, TypedHeader}; +use axum::{async_trait, Json, RequestPartsExt, Router, TypedHeader}; use axum_sessions::async_session::{Session, SessionStore}; use como_domain::users::User; use como_infrastructure::register::ServiceRegister; @@ -20,6 +20,7 @@ use oauth2::basic::BasicClient; use oauth2::{reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope, TokenResponse}; use oauth2::{RedirectUrl, TokenIntrospectionResponse}; use serde::Deserialize; +use serde_json::json; use zitadel::oidc::introspection::introspect; #[derive(Debug, Deserialize)] @@ -27,17 +28,39 @@ pub struct ZitadelAuthParams { return_url: Option, } -pub async fn zitadel_auth(State(client): State) -> impl IntoResponse { - let (auth_url, _csrf_token) = client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("identify".to_string())) - .add_scope(Scope::new("openid".to_string())) - .url(); - - Redirect::to(auth_url.as_ref()) +trait AnyhowExtensions +where + E: Display, +{ + fn into_response(self) -> Result; +} +impl AnyhowExtensions for anyhow::Result +where + E: Display, +{ + fn into_response(self) -> Result { + match self { + Ok(o) => Ok(o), + Err(e) => { + tracing::error!("failed with anyhow error: {}", e); + Err(ErrorResponse::from(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "status": "something", + })), + ))) + } + } + } } -pub static COOKIE_NAME: &str = "SESSION"; +pub async fn zitadel_auth( + State(services): State, +) -> Result { + let url = services.auth_service.login().await.into_response()?; + + Ok(Redirect::to(&url.to_string())) +} #[derive(Debug, Deserialize)] #[allow(dead_code)] @@ -48,44 +71,15 @@ pub struct AuthRequest { pub async fn login_authorized( Query(query): Query, - State(store): State, - State(introspection_state): State, -) -> impl IntoResponse { - let token = oauth_client - .exchange_code(AuthorizationCode::new(query.code.clone())) - .request_async(async_http_client) + State(services): State, +) -> Result { + let (headers, url) = services + .auth_service + .login_authorized(&query.code, &query.state) .await - .unwrap(); + .into_response()?; - let config = IntrospectionConfig::from_ref(&introspection_state); - - let res = introspect( - &config.introspection_uri, - &config.authority, - &config.authentication, - token.access_token().secret(), - ) - .await - .unwrap(); - - let mut session = Session::new(); - session - .insert( - "user", - User { - id: res.sub().unwrap().into(), - }, - ) - .unwrap(); - - let cookie = store.store_session(session).await.unwrap().unwrap(); - - let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie); - - let mut headers = HeaderMap::new(); - headers.insert(SET_COOKIE, cookie.parse().unwrap()); - - (headers, Redirect::to("http://localhost:3000/dash/home")) + Ok((headers, Redirect::to(url.as_str()))) } pub struct AuthController; @@ -106,79 +100,61 @@ pub struct UserFromSession { pub user: User, } +pub static COOKIE_NAME: &str = "SESSION"; + #[async_trait] impl FromRequestParts for UserFromSession where - PostgresSessionStore: FromRef, - IntrospectionState: FromRef, + ServiceRegister: FromRef, S: Send + Sync, { type Rejection = (StatusCode, &'static str); async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let store = PostgresSessionStore::from_ref(state); + let services = ServiceRegister::from_ref(state); let cookie: Option> = parts.extract().await.unwrap(); - let session_cookie = cookie.as_ref().and_then(|cookie| cookie.get(COOKIE_NAME)); if let None = session_cookie { - let introspection_state = IntrospectionState::from_ref(state); + // let introspection_state = IntrospectionState::from_ref(state); - let basic: Option>> = parts.extract().await.unwrap(); + // let basic: Option>> = parts.extract().await.unwrap(); - if let Some(basic) = basic { - let config = IntrospectionConfig::from_ref(&introspection_state); + // if let Some(basic) = basic { + // let config = IntrospectionConfig::from_ref(&introspection_state); - let res = introspect( - &config.introspection_uri, - &config.authority, - &config.authentication, - basic.password(), - ) - .await - .unwrap(); + // let res = introspect( + // &config.introspection_uri, + // &config.authority, + // &config.authentication, + // basic.password(), + // ) + // .await + // .unwrap(); - return Ok(UserFromSession { - user: User { - id: res.sub().unwrap().into(), - }, - }); - } + // return Ok(UserFromSession { + // user: User { + // id: res.sub().unwrap().into(), + // }, + // }); + // } + todo!() - return Err((StatusCode::UNAUTHORIZED, "No session was found")); + //return Err(anyhow::anyhow!("No session was found")).into_response(); } let session_cookie = session_cookie.unwrap(); - tracing::debug!( - "UserFromSession: got session cookie from user agent, {}={}", - COOKIE_NAME, - session_cookie - ); // continue to decode the session cookie - let user = - if let Some(session) = store.load_session(session_cookie.to_owned()).await.unwrap() { - if let Some(user) = session.get::("user") { - tracing::debug!( - "UserFromSession: session decoded success, user_id={:?}", - user.id - ); - user - } else { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "No `user_id` found in session", - )); - } - } else { - tracing::debug!( - "UserIdFromSession: err session not exists in store, {}={}", - COOKIE_NAME, - session_cookie - ); - return Err((StatusCode::BAD_REQUEST, "No session found for cookie")); - }; + let user = services + .auth_service + .get_user_from_session(session_cookie) + .await + .into_response() + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed with error"))?; - Ok(UserFromSession { user }) + Ok(UserFromSession { + user: User { id: user.id }, + }) } } diff --git a/como_api/src/router.rs b/como_api/src/router.rs index b8abc49..1c9693b 100644 --- a/como_api/src/router.rs +++ b/como_api/src/router.rs @@ -1,18 +1,15 @@ use std::env; use anyhow::Context; -use async_sqlx_session::PostgresSessionStore; use axum::extract::FromRef; use axum::http::{HeaderValue, Method}; use axum::Router; use como_infrastructure::register::ServiceRegister; -use oauth2::basic::BasicClient; use tower::ServiceBuilder; use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::controllers::auth::AuthController; use crate::controllers::graphql::GraphQLController; -use crate::zitadel::{IntrospectionState, IntrospectionStateBuilder}; pub struct Api; @@ -22,18 +19,8 @@ impl Api { cors_origin: &str, service_register: ServiceRegister, ) -> anyhow::Result<()> { - let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!"); - let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!"); - let zitadel_url = env::var("ZITADEL_URL").expect("missing ZITADEL_URL"); - - let is = IntrospectionStateBuilder::new(&zitadel_url) - .with_basic_auth(&client_id, &client_secret) - .build() - .await?; - let app_state = AppState { - store: service_register.session_store.clone(), - introspection_state: is, + service_register: service_register.clone(), }; let router = Router::new() @@ -76,18 +63,11 @@ impl Api { #[derive(Clone)] pub struct AppState { - introspection_state: IntrospectionState, - store: PostgresSessionStore, + service_register: ServiceRegister, } -impl FromRef for PostgresSessionStore { - fn from_ref(state: &AppState) -> Self { - state.store.clone() - } -} - -impl FromRef for IntrospectionState { +impl FromRef for ServiceRegister { fn from_ref(input: &AppState) -> Self { - input.introspection_state.clone() + input.service_register.clone() } } diff --git a/como_api/src/zitadel/mod.rs b/como_api/src/zitadel/mod.rs index 5089d31..8b13789 100644 --- a/como_api/src/zitadel/mod.rs +++ b/como_api/src/zitadel/mod.rs @@ -1,90 +1 @@ -pub mod client; -use axum::extract::FromRef; -use openidconnect::IntrospectionUrl; -use zitadel::{ - axum::introspection::IntrospectionStateBuilderError, - credentials::Application, - oidc::{discovery::discover, introspection::AuthorityAuthentication}, -}; - -#[derive(Clone, Debug)] -pub struct IntrospectionState { - pub(crate) config: IntrospectionConfig, -} - -/// Configuration that must be inject into the axum application state. Used by the -/// [IntrospectionStateBuilder](super::IntrospectionStateBuilder). This struct is also used to create the [IntrospectionState](IntrospectionState) -#[derive(Debug, Clone)] -pub struct IntrospectionConfig { - pub(crate) authority: String, - pub(crate) authentication: AuthorityAuthentication, - pub(crate) introspection_uri: IntrospectionUrl, -} - -impl FromRef for IntrospectionConfig { - fn from_ref(input: &IntrospectionState) -> Self { - input.config.clone() - } -} - -pub struct IntrospectionStateBuilder { - authority: String, - authentication: Option, -} - -/// Builder for [IntrospectionConfig] -impl IntrospectionStateBuilder { - pub fn new(authority: &str) -> Self { - Self { - authority: authority.to_string(), - authentication: None, - } - } - - pub fn with_basic_auth( - &mut self, - client_id: &str, - client_secret: &str, - ) -> &mut IntrospectionStateBuilder { - self.authentication = Some(AuthorityAuthentication::Basic { - client_id: client_id.to_string(), - client_secret: client_secret.to_string(), - }); - - self - } - - pub fn with_jwt_profile(&mut self, application: Application) -> &mut IntrospectionStateBuilder { - self.authentication = Some(AuthorityAuthentication::JWTProfile { application }); - - self - } - - pub async fn build(&mut self) -> Result { - if self.authentication.is_none() { - return Err(IntrospectionStateBuilderError::NoAuthSchema); - } - - let metadata = discover(&self.authority) - .await - .map_err(|source| IntrospectionStateBuilderError::Discovery { source })?; - - let introspection_uri = metadata - .additional_metadata() - .introspection_endpoint - .clone(); - - if introspection_uri.is_none() { - return Err(IntrospectionStateBuilderError::NoIntrospectionUrl); - } - - Ok(IntrospectionState { - config: IntrospectionConfig { - authority: self.authority.clone(), - introspection_uri: introspection_uri.unwrap(), - authentication: self.authentication.as_ref().unwrap().clone(), - }, - }) - } -} diff --git a/como_auth/Cargo.toml b/como_auth/Cargo.toml index c6ef12c..e28d61b 100644 --- a/como_auth/Cargo.toml +++ b/como_auth/Cargo.toml @@ -6,21 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -como_gql.workspace = true -como_core.workspace = true -como_domain.workspace = true -como_infrastructure.workspace = true - clap.workspace = true async-trait.workspace = true -async-graphql.workspace = true -async-graphql-axum.workspace = true axum.workspace = true axum-extra.workspace = true axum-sessions.workspace = true serde.workspace = true -serde_json.workspace = true -tokio.workspace = true uuid.workspace = true sqlx.workspace = true anyhow.workspace = true @@ -34,5 +25,6 @@ oauth2 = "4.4.0" openidconnect = "3.0.0" [dev-dependencies] +tokio.workspace = true pretty_assertions.workspace = true sealed_test.workspace = true diff --git a/como_auth/src/auth.rs b/como_auth/src/auth.rs new file mode 100644 index 0000000..4e8b3ce --- /dev/null +++ b/como_auth/src/auth.rs @@ -0,0 +1,107 @@ +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, + session::{SessionService, User}, + AuthClap, AuthEngine, +}; + +#[async_trait] +pub trait Auth { + async fn login(&self) -> 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) -> anyhow::Result { + match config.engine { + AuthEngine::Noop => Ok(Self::new_noop()), + AuthEngine::Zitadel => Ok(Self::new_zitadel()), + } + } + + pub fn new_zitadel() -> Self { + todo!() + //Self(Arc::new(ZitadelAuthService {})) + } + + 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 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 get_user_from_session(&self, cookie: &str) -> anyhow::Result { + todo!() + } +} diff --git a/como_auth/src/introspection.rs b/como_auth/src/introspection.rs index de91ecc..43ec02b 100644 --- a/como_auth/src/introspection.rs +++ b/como_auth/src/introspection.rs @@ -1,12 +1,16 @@ -use std::sync::Arc; +use std::{ops::Deref, sync::Arc}; use async_trait::async_trait; use axum::extract::FromRef; +use oauth2::TokenIntrospectionResponse; use openidconnect::IntrospectionUrl; use zitadel::{ axum::introspection::IntrospectionStateBuilderError, credentials::Application, - oidc::{discovery::discover, introspection::AuthorityAuthentication}, + oidc::{ + discovery::discover, + introspection::{introspect, AuthorityAuthentication}, + }, }; use crate::AuthClap; @@ -14,10 +18,10 @@ use crate::AuthClap; #[async_trait] pub trait Introspection { async fn get_user(&self) -> anyhow::Result<()>; + async fn get_id_token(&self, token: &str) -> anyhow::Result; } pub struct IntrospectionService(Arc); - impl IntrospectionService { pub async fn new_zitadel(config: &AuthClap) -> anyhow::Result { let res = IntrospectionStateBuilder::new(&config.zitadel.authority_url.clone().unwrap()) @@ -34,6 +38,14 @@ impl IntrospectionService { } } +impl Deref for IntrospectionService { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub struct ZitadelIntrospection { state: IntrospectionState, } @@ -49,6 +61,21 @@ impl Introspection for ZitadelIntrospection { async fn get_user(&self) -> anyhow::Result<()> { Ok(()) } + async fn get_id_token(&self, token: &str) -> anyhow::Result { + let config = &self.state.config; + let res = introspect( + &config.introspection_uri, + &config.authority, + &config.authentication, + token, + ) + .await?; + + Ok(res + .sub() + .ok_or(anyhow::anyhow!("could not find a userid (sub) in token"))? + .to_string()) + } } #[derive(Clone, Debug)] diff --git a/como_auth/src/lib.rs b/como_auth/src/lib.rs index 439903f..ee3cd76 100644 --- a/como_auth/src/lib.rs +++ b/como_auth/src/lib.rs @@ -1,8 +1,13 @@ use oauth::{OAuth, ZitadelConfig}; use serde::{Deserialize, Serialize}; +mod auth; mod introspection; mod oauth; +mod session; + +pub use auth::{Auth, AuthService}; +use session::SessionClap; #[derive(clap::ValueEnum, Clone, PartialEq, Eq, Debug)] pub enum AuthEngine { @@ -10,6 +15,12 @@ pub enum AuthEngine { Zitadel, } +#[derive(clap::ValueEnum, Clone, PartialEq, Eq, Debug)] +pub enum SessionBackend { + InMemory, + Postgresql, +} + #[derive(clap::Args, Clone, PartialEq, Eq, Debug)] pub struct AuthClap { #[arg( @@ -22,8 +33,21 @@ pub struct AuthClap { ] pub engine: AuthEngine, + #[arg( + env = "SESSION_BACKEND", + long = "session-backend", + requires_ifs = [ + ( "postgresql", "PostgresqlSessionClap" ) + ], + default_value = "in-memory" ) + ] + pub session_backend: SessionBackend, + #[clap(flatten)] pub zitadel: ZitadelClap, + + #[clap(flatten)] + pub session: SessionClap, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -105,7 +129,10 @@ impl AuthClap { #[cfg(test)] mod test { - use crate::{AuthClap, AuthEngine, ZitadelClap}; + use crate::{ + session::{PostgresqlSessionClap, SessionClap}, + AuthClap, AuthEngine, SessionBackend, ZitadelClap, + }; use clap::Parser; use pretty_assertions::assert_eq; @@ -141,6 +168,10 @@ mod test { token_url: None, authority_url: None, }, + session_backend: SessionBackend::InMemory, + session: SessionClap { + postgresql: PostgresqlSessionClap { conn: None } + } } } ); @@ -163,6 +194,10 @@ mod test { token_url: None, authority_url: None, }, + session_backend: crate::SessionBackend::InMemory, + session: crate::SessionClap { + postgresql: PostgresqlSessionClap { conn: None } + } } } ); @@ -195,6 +230,10 @@ mod test { token_url: Some("https://something".into()), authority_url: Some("https://something".into()), }, + session_backend: crate::SessionBackend::InMemory, + session: crate::SessionClap { + postgresql: PostgresqlSessionClap { conn: None } + } }, } ); diff --git a/como_auth/src/oauth.rs b/como_auth/src/oauth.rs index a3babf3..bd2e9c1 100644 --- a/como_auth/src/oauth.rs +++ b/como_auth/src/oauth.rs @@ -1,5 +1,8 @@ use async_trait::async_trait; +use oauth2::reqwest::async_http_client; +use oauth2::url::Url; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; +use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse}; use std::ops::Deref; use std::sync::Arc; @@ -8,6 +11,8 @@ use crate::ZitadelClap; #[async_trait] pub trait OAuthClient { async fn get_token(&self) -> anyhow::Result<()>; + async fn authorize_url(&self) -> anyhow::Result; + async fn exchange(&self, code: &str) -> anyhow::Result; } pub struct OAuth(Arc); @@ -43,6 +48,13 @@ impl OAuthClient for NoopOAuthClient { async fn get_token(&self) -> anyhow::Result<()> { Ok(()) } + async fn authorize_url(&self) -> anyhow::Result { + Ok(Url::parse("http://localhost:3000/auth/zitadel").unwrap()) + } + + async fn exchange(&self, code: &str) -> anyhow::Result { + Ok(String::new()) + } } // -- Zitadel @@ -138,6 +150,26 @@ impl OAuthClient for ZitadelOAuthClient { async fn get_token(&self) -> anyhow::Result<()> { Ok(()) } + async fn authorize_url(&self) -> anyhow::Result { + let (auth_url, _csrf_token) = self + .client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("identify".to_string())) + .add_scope(Scope::new("openid".to_string())) + .url(); + + Ok(auth_url) + } + + async fn exchange(&self, code: &str) -> anyhow::Result { + let token = self + .client + .exchange_code(AuthorizationCode::new(code.to_string())) + .request_async(async_http_client) + .await?; + + Ok(token.access_token().secret().clone()) + } } #[cfg(test)] diff --git a/como_auth/src/session.rs b/como_auth/src/session.rs new file mode 100644 index 0000000..dce6a72 --- /dev/null +++ b/como_auth/src/session.rs @@ -0,0 +1,108 @@ +use std::{ops::Deref, sync::Arc}; + +use async_sqlx_session::PostgresSessionStore; +use async_trait::async_trait; +use axum_sessions::async_session::{Session as AxumSession, SessionStore as AxumSessionStore}; +use serde::{Deserialize, Serialize}; + +use crate::{AuthClap, SessionBackend}; + +#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SessionClap { + #[clap(flatten)] + pub postgresql: PostgresqlSessionClap, +} + +#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PostgresqlSessionClap { + #[arg(env = "SESSION_POSTGRES_CONN", long = "session-postgres-conn")] + pub conn: Option, +} + +#[async_trait] +pub trait Session { + async fn insert_user(&self, id: &str, user_id: &str) -> anyhow::Result; + async fn get_user(&self, cookie: &str) -> anyhow::Result>; +} + +pub struct SessionService(Arc); +impl SessionService { + pub async fn new(config: &AuthClap) -> anyhow::Result { + match config.session_backend { + SessionBackend::InMemory => Ok(Self(Arc::new(InMemorySessionService {}))), + SessionBackend::Postgresql => { + Ok(Self(Arc::new(PostgresSessionService { store: todo!() }))) + } + } + } +} + +impl Deref for SessionService { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct PostgresSessionService { + store: PostgresSessionStore, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct User { + pub id: String, +} + +#[async_trait] +impl Session for PostgresSessionService { + async fn insert_user(&self, id: &str, user_id: &str) -> anyhow::Result { + let mut session = AxumSession::new(); + session.insert( + "user", + User { + id: user_id.to_string(), + }, + )?; + + let cookie = self + .store + .store_session(session) + .await? + .ok_or(anyhow::anyhow!("failed to store session"))?; + + Ok(cookie) + } + async fn get_user(&self, cookie: &str) -> anyhow::Result> { + if let Some(session) = self.store.load_session(cookie.to_string()).await.unwrap() { + if let Some(user) = session.get::("user") { + tracing::debug!( + "UserFromSession: session decoded success, user_id={:?}", + user.id + ); + Ok(Some(user.id)) + } else { + Ok(None) + } + } else { + tracing::debug!( + "UserIdFromSession: err session not exists in store, {}", + cookie + ); + Err(anyhow::anyhow!("No session found for cookie")) + } + } +} + +pub struct InMemorySessionService {} + +#[async_trait] +impl Session for InMemorySessionService { + async fn insert_user(&self, id: &str, user_id: &str) -> anyhow::Result { + todo!() + } + + async fn get_user(&self, cookie: &str) -> anyhow::Result> { + todo!() + } +} diff --git a/como_infrastructure/Cargo.toml b/como_infrastructure/Cargo.toml index a5302d1..a0ddc0b 100644 --- a/como_infrastructure/Cargo.toml +++ b/como_infrastructure/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] como_core.workspace = true como_domain.workspace = true - +como_auth.workspace = true axum.workspace = true async-trait.workspace = true diff --git a/como_infrastructure/src/configs/mod.rs b/como_infrastructure/src/configs/mod.rs index 42ebf3a..03981b0 100644 --- a/como_infrastructure/src/configs/mod.rs +++ b/como_infrastructure/src/configs/mod.rs @@ -1,4 +1,5 @@ use clap::ValueEnum; +use como_auth::AuthClap; #[derive(clap::Parser)] pub struct AppConfig { @@ -18,6 +19,9 @@ pub struct AppConfig { pub seed: bool, #[clap(long, env)] pub cors_origin: String, + + #[clap(flatten)] + pub auth: AuthClap, } #[derive(Clone, Debug, ValueEnum)] diff --git a/como_infrastructure/src/register.rs b/como_infrastructure/src/register.rs index 1811523..7af9805 100644 --- a/como_infrastructure/src/register.rs +++ b/como_infrastructure/src/register.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_sqlx_session::PostgresSessionStore; +use como_auth::AuthService; use como_core::{items::DynItemService, projects::DynProjectService, users::DynUserService}; use tracing::log::info; @@ -20,12 +21,15 @@ pub struct ServiceRegister { pub project_service: DynProjectService, pub user_service: DynUserService, pub session_store: PostgresSessionStore, + pub auth_service: AuthService, } impl ServiceRegister { pub async fn new(pool: ConnectionPool, config: Arc) -> anyhow::Result { info!("creating services"); + let auth = AuthService::new(&config.auth).await?; + let s = match config.database_type { DatabaseType::Postgres => { let item_service = @@ -42,6 +46,7 @@ impl ServiceRegister { user_service, project_service, session_store: store, + auth_service: auth, } } DatabaseType::InMemory => { @@ -57,6 +62,7 @@ impl ServiceRegister { user_service, project_service, session_store: store, + auth_service: auth, } } };