Compare commits

..

No commits in common. "main" and "feat/with-config" have entirely different histories.

22 changed files with 741 additions and 1561 deletions

View File

@ -1,2 +0,0 @@
kind: template
load: cuddle-rust-lib-plan.yaml

34
.woodpecker/test.yml Normal file
View File

@ -0,0 +1,34 @@
when:
- event: [pull_request, tag]
- event: push
branch:
- main
variables:
- &rust_image 'rustlang/rust:nightly'
steps:
build:
image: *rust_image
group: ci
commands:
- "cargo build"
test:
image: *rust_image
group: ci
commands:
- "cargo test"
lint:
image: *rust_image
group: ci
commands:
- "cargo clippy"
fmt:
image: *rust_image
group: ci
commands:
- "cargo fmt --all --check"

1582
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,40 +5,34 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
nefarious-login = { path = "crates/nefarious-login" } nefarious-login = { path = "crates/nefarious-login" }
anyhow = { version = "1" } anyhow = { version = "1.0.75" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = { version = "0.1", features = ["log"] } tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
clap = { version = "4", features = ["derive", "env"] } clap = {version = "4.4.7", features = ["derive", "env"]}
async-trait = { version = "0.1", features = [] } async-trait = {version = "0.1.74", features = []}
axum = { version = "0.7.1", features = [ axum = {version = "0.6.20", features = []}
"macros", axum-extra = {version = "0.8.0", features = ["cookie", "cookie-private"]}
] } axum-sessions = {version = "0.6.1", features = []}
axum-extra = { version = "0.9.0", features = [ async-sqlx-session = {version = "0.4.0", features = ["pg"]}
"cookie",
"cookie-private",
"typed-header",
] }
axum-sessions = { version = "0.6.1", features = [] }
async-sqlx-session = { version = "0.4.0", features = ["pg"] }
serde = { version = "1", features = ["derive"] } serde = {version = "1.0", features = ["derive"]}
serde_json = { version = "1" } serde_json = {version = "1.0.108"}
uuid = {version = "1.5.0", features = ["v4"]} uuid = {version = "1.5.0", features = []}
sqlx = { version = "0.7", features = [ sqlx = { version = "0.7.2", features = [
"runtime-tokio", "runtime-tokio-rustls",
"postgres", "postgres",
"migrate", "migrate",
] } ] }
zitadel = { version = "3.4", features = ["axum"] } zitadel = { version = "3.4.29", features = ["axum"] }
tower = "0.4" tower = "0.4.13"
tower-http = { version = "0.4", features = ["cors", "trace"] } tower-http = { version = "0.4.4", features = ["cors", "trace"] }
oauth2 = "4.4.2" oauth2 = "4.4.2"
openidconnect = "3.4" openidconnect = "3.4.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
sealed_test = "1.0.0" sealed_test = "1.0.0"

View File

@ -6,22 +6,17 @@ use axum::http::{header::SET_COOKIE, HeaderMap};
use oauth2::url::Url; use oauth2::url::Url;
use crate::{ use crate::{
introspection::{IdToken, IntrospectionService}, introspection::IntrospectionService,
login::{auth_clap::AuthEngine, config::ConfigClap, AuthClap}, login::{auth_clap::AuthEngine, AuthClap},
oauth::{zitadel::ZitadelConfig, OAuth}, oauth::{zitadel::ZitadelConfig, OAuth},
session::{AppSession, SessionService, User}, session::{SessionService, User},
}; };
#[async_trait] #[async_trait]
pub trait Auth { pub trait Auth {
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, Url)>; async fn login(&self) -> anyhow::Result<Url>;
async fn login_token(&self, user: &str, password: &str) -> anyhow::Result<IdToken>; async fn login_token(&self, user: &str, password: &str) -> anyhow::Result<String>;
async fn login_authorized( async fn login_authorized(&self, code: &str, state: &str) -> anyhow::Result<(HeaderMap, Url)>;
&self,
code: &str,
state: &str,
app_session_cookie: Option<String>,
) -> anyhow::Result<(HeaderMap, Url)>;
async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User>; async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User>;
} }
@ -31,23 +26,14 @@ pub struct AuthService(Arc<dyn Auth + Send + Sync + 'static>);
impl AuthService { impl AuthService {
pub async fn new(config: &AuthClap) -> anyhow::Result<Self> { pub async fn new(config: &AuthClap) -> anyhow::Result<Self> {
match config.engine { match config.engine {
AuthEngine::Noop => { AuthEngine::Noop => Ok(Self::new_noop()),
let session = SessionService::new(config).await?;
Ok(Self::new_noop(session, &config.config))
}
AuthEngine::Zitadel => { AuthEngine::Zitadel => {
let session = SessionService::new(config).await?; let session = SessionService::new(config).await?;
let oauth: OAuth = ZitadelConfig::try_from(config.zitadel.clone())?.into(); let oauth: OAuth = ZitadelConfig::try_from(config.zitadel.clone())?.into();
let introspection: IntrospectionService = let introspection: IntrospectionService =
IntrospectionService::new_zitadel(config).await?; IntrospectionService::new_zitadel(config).await?;
Ok(Self::new_zitadel( Ok(Self::new_zitadel(oauth, introspection, session))
oauth,
introspection,
session,
&config.config,
))
} }
} }
} }
@ -56,21 +42,16 @@ impl AuthService {
oauth: OAuth, oauth: OAuth,
introspection: IntrospectionService, introspection: IntrospectionService,
session: SessionService, session: SessionService,
config: &ConfigClap,
) -> Self { ) -> Self {
Self(Arc::new(ZitadelAuthService { Self(Arc::new(ZitadelAuthService {
oauth, oauth,
introspection, introspection,
session, session,
config: config.clone(),
})) }))
} }
pub fn new_noop(session: SessionService, config: &ConfigClap) -> Self { pub fn new_noop() -> Self {
Self(Arc::new(NoopAuthService { Self(Arc::new(NoopAuthService {}))
session,
config: config.clone(),
}))
} }
} }
@ -86,131 +67,63 @@ pub struct ZitadelAuthService {
oauth: OAuth, oauth: OAuth,
introspection: IntrospectionService, introspection: IntrospectionService,
session: SessionService, session: SessionService,
config: ConfigClap,
} }
pub static COOKIE_NAME: &str = "SESSION"; pub static COOKIE_NAME: &str = "SESSION";
pub static COOKIE_APP_SESSION_NAME: &str = "APP_SESSION";
#[async_trait] #[async_trait]
impl Auth for ZitadelAuthService { impl Auth for ZitadelAuthService {
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, Url)> { async fn login(&self) -> anyhow::Result<Url> {
let mut headers = HeaderMap::new();
if let Some(return_url) = return_url.clone() {
let cookie_value = self.session.insert(AppSession { return_url }).await?;
let cookie = format!(
"{}={}; SameSite=Lax; Path=/",
COOKIE_APP_SESSION_NAME, cookie_value
);
headers.insert(SET_COOKIE, cookie.parse().unwrap());
}
let authorize_url = self.oauth.authorize_url().await?; let authorize_url = self.oauth.authorize_url().await?;
Ok((headers, authorize_url)) Ok(authorize_url)
} }
async fn login_authorized( async fn login_authorized(&self, code: &str, _state: &str) -> anyhow::Result<(HeaderMap, Url)> {
&self,
code: &str,
_state: &str,
app_session_cookie: Option<String>,
) -> anyhow::Result<(HeaderMap, Url)> {
let token = self.oauth.exchange(code).await?; let token = self.oauth.exchange(code).await?;
let id_token = self.introspection.get_id_token(token.as_str()).await?; let user_id = self.introspection.get_id_token(token.as_str()).await?;
let cookie_value = self.session.insert_user("user", id_token).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 cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie_value);
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, cookie.parse().unwrap()); headers.insert(SET_COOKIE, cookie.parse().unwrap());
let mut return_url = self.config.return_url.clone();
if let Some(cookie) = app_session_cookie {
if let Some(session) = self.session.get(&cookie).await? {
if session.return_url.starts_with('/') {
let mut url = Url::parse(&return_url)?;
url.set_path(&session.return_url);
return_url = url.to_string();
} else {
return_url = session.return_url;
}
}
}
Ok(( Ok((
headers, headers,
Url::parse(&return_url) Url::parse("http://localhost:3001/authed")
.context("failed to parse login_authorized zitadel return url")?, .context("failed to parse login_authorized zitadel return url")?,
)) ))
} }
async fn login_token(&self, _user: &str, password: &str) -> anyhow::Result<IdToken> { async fn login_token(&self, _user: &str, password: &str) -> anyhow::Result<String> {
self.introspection.get_id_token(password).await self.introspection.get_id_token(password).await
} }
async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User> { async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User> {
match self.session.get_user(cookie).await? { match self.session.get_user(cookie).await? {
Some(u) => Ok(u), Some(u) => Ok(User { id: u }),
None => Err(anyhow::anyhow!("failed to find user")), None => Err(anyhow::anyhow!("failed to find user")),
} }
} }
} }
pub struct NoopAuthService { pub struct NoopAuthService {}
session: SessionService,
config: ConfigClap,
}
#[async_trait] #[async_trait]
impl Auth for NoopAuthService { impl Auth for NoopAuthService {
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, Url)> { async fn login(&self) -> anyhow::Result<Url> {
let url = Url::parse(&format!( todo!()
"{}/auth/authorized?code=noop&state=noop",
self.config
.return_url
.rsplit_once('/')
.map(|(a, _)| a)
.unwrap()
))
.unwrap();
Ok((HeaderMap::new(), url))
} }
async fn login_authorized( async fn login_authorized(
&self, &self,
_code: &str, _code: &str,
_state: &str, _state: &str,
_app_session_cookie: Option<String>,
) -> anyhow::Result<(HeaderMap, Url)> { ) -> anyhow::Result<(HeaderMap, Url)> {
let cookie_value = self
.session
.insert_user(
"user",
IdToken {
sub: uuid::Uuid::new_v4().to_string(),
email: format!("{}@kjuulh.io", uuid::Uuid::new_v4()),
name: uuid::Uuid::new_v4().to_string(),
},
)
.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(&self.config.return_url)
.context("failed to parse login_authorized zitadel return url")?,
))
}
async fn login_token(&self, _user: &str, _password: &str) -> anyhow::Result<IdToken> {
todo!() todo!()
} }
async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User> { async fn login_token(&self, _user: &str, _password: &str) -> anyhow::Result<String> {
match self.session.get_user(cookie).await? { todo!()
Some(u) => Ok(u),
None => Err(anyhow::anyhow!("failed to find user")),
} }
async fn get_user_from_session(&self, _cookie: &str) -> anyhow::Result<User> {
todo!()
} }
} }

View File

@ -1,24 +1,25 @@
use std::fmt::Display; use std::fmt::Display;
use axum::extract::{FromRef, FromRequestParts, Query, State}; use axum::extract::{FromRef, FromRequestParts, Query, State};
use axum::http::request::Parts;
use axum::http::{HeaderMap, StatusCode, Uri};
use axum::response::{ErrorResponse, IntoResponse, Redirect, Response};
use axum::routing::get;
use axum::{async_trait, Json, RequestPartsExt, Router};
use axum_extra::extract::CookieJar; use axum::headers::authorization::Basic;
use axum_extra::headers::authorization::Basic; use axum::headers::{Authorization, Cookie};
use axum_extra::headers::{Authorization, Cookie}; use axum::http::request::Parts;
use axum_extra::TypedHeader; use axum::http::StatusCode;
use axum::response::{ErrorResponse, IntoResponse, Redirect};
use axum::routing::get;
use axum::{async_trait, Json, RequestPartsExt, Router, TypedHeader};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use crate::auth::{AuthService, COOKIE_APP_SESSION_NAME}; use crate::auth::AuthService;
use crate::session::User; use crate::session::User;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ZitadelAuthParams { pub struct ZitadelAuthParams {
#[allow(dead_code)]
return_url: Option<String>, return_url: Option<String>,
} }
@ -51,9 +52,9 @@ where
pub async fn zitadel_auth( pub async fn zitadel_auth(
State(auth_service): State<AuthService>, State(auth_service): State<AuthService>,
) -> Result<impl IntoResponse, ErrorResponse> { ) -> Result<impl IntoResponse, ErrorResponse> {
let (headers, url) = auth_service.login(None).await.into_response()?; let url = auth_service.login().await.into_response()?;
Ok((headers, Redirect::to(url.as_ref()))) Ok(Redirect::to(url.as_ref()))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -66,14 +67,9 @@ pub struct AuthRequest {
pub async fn login_authorized( pub async fn login_authorized(
Query(query): Query<AuthRequest>, Query(query): Query<AuthRequest>,
State(auth_service): State<AuthService>, State(auth_service): State<AuthService>,
cookie_jar: CookieJar,
) -> Result<impl IntoResponse, ErrorResponse> { ) -> Result<impl IntoResponse, ErrorResponse> {
let cookie_value = cookie_jar
.get(COOKIE_APP_SESSION_NAME)
.map(|c| c.value().to_string());
let (headers, url) = auth_service let (headers, url) = auth_service
.login_authorized(&query.code, &query.state, cookie_value) .login_authorized(&query.code, &query.state)
.await .await
.into_response()?; .into_response()?;
@ -97,21 +93,13 @@ pub struct UserFromSession {
pub static COOKIE_NAME: &str = "SESSION"; pub static COOKIE_NAME: &str = "SESSION";
pub struct AuthRedirect((HeaderMap, String));
impl IntoResponse for AuthRedirect {
fn into_response(self) -> Response {
(self.0 .0, Redirect::temporary(&self.0 .1.as_str())).into_response()
}
}
#[async_trait] #[async_trait]
impl<S> FromRequestParts<S> for UserFromSession impl<S> FromRequestParts<S> for UserFromSession
where where
AuthService: FromRef<S>, AuthService: FromRef<S>,
S: Send + Sync, S: Send + Sync,
{ {
type Rejection = AuthRedirect; type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let auth_service = AuthService::from_ref(state); let auth_service = AuthService::from_ref(state);
@ -122,58 +110,43 @@ where
let basic: Option<TypedHeader<Authorization<Basic>>> = parts.extract().await.unwrap(); let basic: Option<TypedHeader<Authorization<Basic>>> = parts.extract().await.unwrap();
if let Some(basic) = basic { if let Some(basic) = basic {
let token = match auth_service let token = auth_service
.login_token(basic.username(), basic.password()) .login_token(basic.username(), basic.password())
.await .await
.into_response() .into_response()
{ .map_err(|_| {
Ok(login) => login, (
Err(e) => { StatusCode::INTERNAL_SERVER_ERROR,
tracing::info!("did not find a basic login token, will trigger login"); "could not get token from basic",
let (headers, url) = auth_service )
.login(Some(parts.uri.to_string())) })?;
.await
.expect("to be able to request login");
return Err(AuthRedirect((headers, url.to_string())));
}
};
return Ok(UserFromSession { return Ok(UserFromSession {
user: User { user: User { id: token },
id: token.sub,
email: token.email,
name: token.name,
},
}); });
} }
tracing::info!("did not find a cookie, will trigger login"); return Err(anyhow::anyhow!("No session was found"))
let (headers, url) = auth_service .into_response()
.login(Some(parts.uri.to_string())) .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "did not find a cookie"))?;
.await
.expect("to be able to request login");
return Err(AuthRedirect((headers, url.to_string())));
} }
let session_cookie = session_cookie.unwrap(); let session_cookie = session_cookie.unwrap();
// continue to decode the session cookie // continue to decode the session cookie
let user = match auth_service let user = auth_service
.get_user_from_session(session_cookie) .get_user_from_session(session_cookie)
.await .await
.into_response() .into_response()
{ .map_err(|_| {
Ok(user) => user, (
Err(_) => { StatusCode::INTERNAL_SERVER_ERROR,
tracing::info!("could not get user from session, will trigger login"); "failed to decode session cookie",
let (headers, url) = auth_service )
.login(Some(parts.uri.to_string())) })?;
.await
.expect("to be able to request login");
return Err(AuthRedirect((headers, url.to_string())));
}
};
Ok(UserFromSession { user }) Ok(UserFromSession {
user: User { id: user.id },
})
} }
} }

View File

@ -4,7 +4,6 @@ use async_trait::async_trait;
use axum::extract::FromRef; use axum::extract::FromRef;
use oauth2::TokenIntrospectionResponse; use oauth2::TokenIntrospectionResponse;
use openidconnect::IntrospectionUrl; use openidconnect::IntrospectionUrl;
use serde::{Deserialize, Serialize};
use zitadel::{ use zitadel::{
axum::introspection::IntrospectionStateBuilderError, axum::introspection::IntrospectionStateBuilderError,
credentials::Application, credentials::Application,
@ -16,17 +15,10 @@ use zitadel::{
use crate::login::AuthClap; use crate::login::AuthClap;
#[derive(Clone, Serialize, Deserialize)]
pub struct IdToken {
pub sub: String,
pub email: String,
pub name: String,
}
#[async_trait] #[async_trait]
pub trait Introspection { pub trait Introspection {
async fn get_user(&self) -> anyhow::Result<()>; async fn get_user(&self) -> anyhow::Result<()>;
async fn get_id_token(&self, token: &str) -> anyhow::Result<IdToken>; async fn get_id_token(&self, token: &str) -> anyhow::Result<String>;
} }
pub struct IntrospectionService(Arc<dyn Introspection + Send + Sync + 'static>); pub struct IntrospectionService(Arc<dyn Introspection + Send + Sync + 'static>);
@ -69,7 +61,7 @@ impl Introspection for ZitadelIntrospection {
async fn get_user(&self) -> anyhow::Result<()> { async fn get_user(&self) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
async fn get_id_token(&self, token: &str) -> anyhow::Result<IdToken> { async fn get_id_token(&self, token: &str) -> anyhow::Result<String> {
let config = &self.state.config; let config = &self.state.config;
let res = introspect( let res = introspect(
&config.introspection_uri, &config.introspection_uri,
@ -79,21 +71,10 @@ impl Introspection for ZitadelIntrospection {
) )
.await?; .await?;
let sub = res Ok(res
.sub() .sub()
.ok_or(anyhow::anyhow!("could not find a userid (sub) in token"))? .ok_or(anyhow::anyhow!("could not find a userid (sub) in token"))?
.to_string(); .to_string())
let extra = res.extra_fields();
let email = extra.email.clone().ok_or(anyhow::anyhow!(
"could not find a email (scope email) in token"
))?;
let name = extra.name.clone().ok_or(anyhow::anyhow!(
"could not find a name (scope profile) in token"
))?;
Ok(IdToken { sub, email, name })
} }
} }

View File

@ -15,7 +15,7 @@ pub struct AuthConfigFile {
zitadel: Option<ZitadelClap>, zitadel: Option<ZitadelClap>,
} }
#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[group(requires_all = ["client_id", "client_secret", "redirect_url", "authority_url"])] #[group(requires_all = ["client_id", "client_secret", "redirect_url", "authority_url"])]
pub struct ZitadelClap { pub struct ZitadelClap {
#[arg(env = "ZITADEL_CLIENT_ID", long = "zitadel-client-id")] #[arg(env = "ZITADEL_CLIENT_ID", long = "zitadel-client-id")]

View File

@ -105,15 +105,12 @@ impl OAuthClient for ZitadelOAuthClient {
Ok(()) Ok(())
} }
async fn authorize_url(&self) -> anyhow::Result<Url> { async fn authorize_url(&self) -> anyhow::Result<Url> {
let req = self let (auth_url, _csrf_token) = self
.client .client
.authorize_url(CsrfToken::new_random) .authorize_url(CsrfToken::new_random)
.add_scope(Scope::new("identify".to_string())) .add_scope(Scope::new("identify".to_string()))
.add_scope(Scope::new("openid".to_string())) .add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string())) .url();
.add_scope(Scope::new("profile".to_string()));
let (auth_url, _csrf_token) = req.url();
Ok(auth_url) Ok(auth_url)
} }

View File

@ -11,7 +11,7 @@ use async_trait::async_trait;
use axum_sessions::async_session::{Session as AxumSession, SessionStore as AxumSessionStore}; use axum_sessions::async_session::{Session as AxumSession, SessionStore as AxumSessionStore};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{introspection::IdToken, login::AuthClap}; use crate::login::AuthClap;
#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionClap { pub struct SessionClap {
@ -19,7 +19,7 @@ pub struct SessionClap {
pub postgresql: PostgresqlSessionClap, pub postgresql: PostgresqlSessionClap,
} }
#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PostgresqlSessionClap { pub struct PostgresqlSessionClap {
#[arg(env = "SESSION_POSTGRES_CONN", long = "session-postgres-conn")] #[arg(env = "SESSION_POSTGRES_CONN", long = "session-postgres-conn")]
pub conn: Option<String>, pub conn: Option<String>,
@ -27,17 +27,16 @@ pub struct PostgresqlSessionClap {
#[async_trait] #[async_trait]
pub trait Session { pub trait Session {
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String>; async fn insert_user(&self, id: &str, user_id: &str) -> anyhow::Result<String>;
async fn insert_user(&self, id: &str, id_token: IdToken) -> anyhow::Result<String>; async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<String>>;
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<User>>;
async fn get(&self, cookie: &str) -> anyhow::Result<Option<AppSession>>;
} }
pub struct SessionService(Arc<dyn Session + Send + Sync + 'static>); pub struct SessionService(Arc<dyn Session + Send + Sync + 'static>);
impl SessionService { impl SessionService {
pub async fn new(config: &AuthClap) -> anyhow::Result<Self> { pub async fn new(config: &AuthClap) -> anyhow::Result<Self> {
match config.session_backend { match config.session_backend {
SessionBackend::InMemory => Ok(Self(Arc::new(InMemorySessionService::default()))), SessionBackend::InMemory => Ok(Self(Arc::new(InMemorySessionService {}))),
SessionBackend::Postgresql => { SessionBackend::Postgresql => {
let postgres_session = PostgresSessionStore::new( let postgres_session = PostgresSessionStore::new(
config config
@ -49,8 +48,6 @@ impl SessionService {
) )
.await?; .await?;
postgres_session.migrate().await?;
Ok(Self(Arc::new(PostgresSessionService { Ok(Self(Arc::new(PostgresSessionService {
store: postgres_session, store: postgres_session,
}))) })))
@ -74,38 +71,16 @@ pub struct PostgresSessionService {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct User { pub struct User {
pub id: String, pub id: String,
pub email: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AppSession {
pub return_url: String,
} }
#[async_trait] #[async_trait]
impl Session for PostgresSessionService { impl Session for PostgresSessionService {
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String> { async fn insert_user(&self, _id: &str, user_id: &str) -> anyhow::Result<String> {
let mut session = AxumSession::new();
session.insert("app_session", app_session)?;
let cookie = self
.store
.store_session(session)
.await?
.ok_or(anyhow::anyhow!("failed to store app session"))?;
Ok(cookie)
}
async fn insert_user(&self, _id: &str, id_token: IdToken) -> anyhow::Result<String> {
let mut session = AxumSession::new(); let mut session = AxumSession::new();
session.insert( session.insert(
"user", "user",
User { User {
id: id_token.sub, id: user_id.to_string(),
email: id_token.email,
name: id_token.name,
}, },
)?; )?;
@ -117,14 +92,14 @@ impl Session for PostgresSessionService {
Ok(cookie) Ok(cookie)
} }
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<User>> { async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<String>> {
if let Some(session) = self.store.load_session(cookie.to_string()).await.unwrap() { if let Some(session) = self.store.load_session(cookie.to_string()).await.unwrap() {
if let Some(user) = session.get::<User>("user") { if let Some(user) = session.get::<User>("user") {
tracing::debug!( tracing::debug!(
"UserFromSession: session decoded success, user_id={:?}", "UserFromSession: session decoded success, user_id={:?}",
user.id user.id
); );
Ok(Some(user)) Ok(Some(user.id))
} else { } else {
Ok(None) Ok(None)
} }
@ -136,50 +111,17 @@ impl Session for PostgresSessionService {
Err(anyhow::anyhow!("No session found for cookie")) Err(anyhow::anyhow!("No session found for cookie"))
} }
} }
async fn get(&self, cookie: &str) -> anyhow::Result<Option<AppSession>> {
let Some(session) = self.store.load_session(cookie.to_string()).await? else {
return Ok(None);
};
let Some(session) = session.get::<AppSession>("app_session") else {
anyhow::bail!("failed to deserialize app_session from cookie");
};
Ok(Some(session))
}
} }
#[derive(Default)] pub struct InMemorySessionService {}
pub struct InMemorySessionService {
store: std::sync::Mutex<std::collections::BTreeMap<String, User>>,
}
#[async_trait] #[async_trait]
impl Session for InMemorySessionService { impl Session for InMemorySessionService {
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String> { async fn insert_user(&self, _id: &str, _user_id: &str) -> anyhow::Result<String> {
todo!() todo!()
} }
async fn insert_user(&self, _id: &str, id_token: IdToken) -> anyhow::Result<String> {
let user = User {
id: id_token.sub,
email: id_token.email,
name: id_token.name,
};
let id = uuid::Uuid::new_v4(); async fn get_user(&self, _cookie: &str) -> anyhow::Result<Option<String>> {
self.store.lock().unwrap().insert(id.to_string(), user);
Ok(id.to_string())
}
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<User>> {
let user = self.store.lock().unwrap().get(cookie).cloned();
Ok(user)
}
async fn get(&self, cookie: &str) -> anyhow::Result<Option<AppSession>> {
todo!() todo!()
} }
} }

View File

@ -5,9 +5,3 @@ base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars: vars:
service: "nefarious-login" service: "nefarious-login"
registry: kasperhermansen registry: kasperhermansen
scripts:
local_up:
type: shell
local_down:
type: shell

View File

@ -1,4 +1,4 @@
use std::{net::SocketAddr, str::FromStr}; use std::net::SocketAddr;
use axum::{ use axum::{
extract::{FromRef, State}, extract::{FromRef, State},
@ -60,10 +60,13 @@ async fn main() -> anyhow::Result<()> {
.with_state(state) .with_state(state)
.nest("/auth", AuthController::new_router(auth_service).await?); .nest("/auth", AuthController::new_router(auth_service).await?);
let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?; let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
let listener = tokio::net::TcpListener::bind(&addr).await?; println!("listening on: {addr}");
println!("open browser at: http://localhost:3001/auth/zitadel");
axum::serve(listener, app).await?; axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
Ok(()) Ok(())
} }

View File

@ -1,4 +1,4 @@
use std::{net::SocketAddr, str::FromStr}; use std::net::SocketAddr;
use axum::{ use axum::{
extract::{FromRef, State}, extract::{FromRef, State},
@ -58,11 +58,10 @@ async fn main() -> anyhow::Result<()> {
let addr = SocketAddr::from(([127, 0, 0, 1], 3001)); let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
println!("listening on: {addr}"); println!("listening on: {addr}");
println!("open browser at: http://localhost:3001/auth/zitadel"); println!("open browser at: http://localhost:3001/auth/zitadel");
axum::Server::bind(&addr)
let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?; .serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await?; .await
.unwrap();
axum::serve(listener, app).await?;
Ok(()) Ok(())
} }

View File

@ -1,16 +0,0 @@
[package]
name = "custom_redirect"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nefarious-login.workspace = true
tokio.workspace = true
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
tracing-subscriber.workspace = true

View File

@ -1,94 +0,0 @@
use std::{net::SocketAddr, str::FromStr};
use axum::{
extract::{FromRef, State},
response::{IntoResponse, Redirect},
routing::get,
Router,
};
use nefarious_login::{
auth::AuthService,
axum::{AuthController, UserFromSession},
login::{
auth_clap::{AuthEngine, ZitadelClap},
config::ConfigClap,
AuthClap,
},
session::{PostgresqlSessionClap, SessionBackend},
};
use tracing_subscriber::EnvFilter;
#[derive(Clone)]
struct AppState {
auth: AuthService,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let auth = AuthClap {
engine: AuthEngine::Zitadel,
session_backend: SessionBackend::Postgresql,
zitadel: ZitadelClap {
authority_url: Some("https://personal-wxuujs.zitadel.cloud".into()),
client_id: Some("237412977047895154@nefarious-test".into()),
client_secret: Some(
"rWwDi8gjNOyuMFKoOjNSlhjcVZ1B25wDh6HsDL27f0g2Hb0xGbvEf0WXFY2akOlL".into(),
),
redirect_url: Some("http://localhost:3001/auth/authorized".into()),
},
session: nefarious_login::session::SessionClap {
postgresql: PostgresqlSessionClap {
conn: Some("postgres://nefarious-test:somenotverysecurepassword@localhost:5432/nefarious-test".into()),
},
},
config: ConfigClap { return_url: "http://localhost:3001/authed".into() } // this normally has /authed
};
let auth_service = AuthService::new(&auth).await?;
let state = AppState {
auth: auth_service.clone(),
};
let app = Router::new()
.route("/unauthed", get(unauthed))
.route("/authed", get(authed))
.route("/login", get(login))
.with_state(state)
.nest("/auth", AuthController::new_router(auth_service).await?);
let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?;
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
impl FromRef<AppState> for AuthService {
fn from_ref(input: &AppState) -> Self {
input.auth.clone()
}
}
async fn login(State(auth_service): State<AuthService>) -> impl IntoResponse {
let (headers, url) = auth_service.login(Some("/authed".into())).await.unwrap();
(headers, Redirect::to(url.as_ref()))
}
async fn unauthed() -> String {
"Hello Unauthorized User".into()
}
#[axum::debug_handler()]
async fn authed(
user: UserFromSession,
State(_auth_service): State<AuthService>,
) -> impl IntoResponse {
format!("Hello authorized user: {:?}", user.user.id)
}

View File

@ -1,16 +0,0 @@
[package]
name = "noop"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nefarious-login.workspace = true
tokio.workspace = true
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
tracing-subscriber.workspace = true

View File

@ -1,84 +0,0 @@
use std::{net::SocketAddr, str::FromStr};
use axum::{
extract::{FromRef, State},
response::IntoResponse,
routing::get,
Router,
};
use nefarious_login::{
auth::AuthService,
axum::{AuthController, UserFromSession},
login::{
auth_clap::{AuthEngine, ZitadelClap},
config::ConfigClap,
AuthClap,
},
session::{PostgresqlSessionClap, SessionBackend},
};
use tracing_subscriber::EnvFilter;
#[derive(Clone)]
struct AppState {
auth: AuthService,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let auth = AuthClap {
engine: AuthEngine::Noop,
session_backend: SessionBackend::InMemory,
zitadel: ZitadelClap {
..Default::default()
},
session: nefarious_login::session::SessionClap {
postgresql: PostgresqlSessionClap {
..Default::default()
},
},
config: ConfigClap {
return_url: "http://localhost:3000/authed".into(),
},
};
let auth_service = AuthService::new(&auth).await?;
let state = AppState {
auth: auth_service.clone(),
};
let app = Router::new()
.route("/unauthed", get(unauthed))
.route("/authed", get(authed))
.with_state(state)
.nest("/auth", AuthController::new_router(auth_service).await?);
let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?;
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
impl FromRef<AppState> for AuthService {
fn from_ref(input: &AppState) -> Self {
input.auth.clone()
}
}
async fn unauthed() -> String {
"Hello Unauthorized User".into()
}
#[axum::debug_handler()]
async fn authed(
user: UserFromSession,
State(_auth_service): State<AuthService>,
) -> impl IntoResponse {
format!("Hello authorized user: {:?}", user.user.id)
}

View File

@ -1,7 +0,0 @@
#!/bin/bash
set -e
cuddle render_template --template-file $TMP/docker-compose.local_up.yml.tmpl --dest $TMP/docker-compose.local_up.yml
docker compose -f $TMP/docker-compose.local_up.yml down -v

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -e
cuddle render_template --template-file $TMP/docker-compose.local_up.yml.tmpl --dest $TMP/docker-compose.local_up.yml
docker compose -f $TMP/docker-compose.local_up.yml up -d --remove-orphans --build
sleep 3

View File

@ -1,7 +0,0 @@
target/
.git/
.cuddle/
scripts/
cuddle.yaml
local.sh
README.md

View File

@ -1,17 +0,0 @@
version: '3.7'
services:
db:
image: bitnami/postgresql:16
restart: always
environment:
- POSTGRESQL_USERNAME=nefarious-test
- POSTGRESQL_DATABASE=nefarious-test
- POSTGRESQL_PASSWORD=somenotverysecurepassword
ports:
- 5432:5432
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: