Compare commits

..

No commits in common. "f586d157b1926b9d42f2e9fdfca043f91ca07884" and "c46bd34e168728875df040a8208326fd7420e565" have entirely different histories.

20 changed files with 480 additions and 1327 deletions

1169
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

@ -26,4 +26,4 @@ openidconnect.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true
pretty_assertions.workspace = true pretty_assertions.workspace = true
sealed_test.workspace = true sealed_test.workspace = true

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, config::ConfigClap, 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,17 +26,12 @@ 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, oauth,
introspection, introspection,
@ -66,11 +56,8 @@ impl AuthService {
})) }))
} }
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(),
}))
} }
} }
@ -89,107 +76,18 @@ pub struct ZitadelAuthService {
config: ConfigClap, 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 mut headers = HeaderMap::new();
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((
headers,
Url::parse(&return_url)
.context("failed to parse login_authorized zitadel return url")?,
))
}
async fn login_token(&self, _user: &str, password: &str) -> anyhow::Result<IdToken> {
self.introspection.get_id_token(password).await
}
async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User> {
match self.session.get_user(cookie).await? {
Some(u) => Ok(u),
None => Err(anyhow::anyhow!("failed to find user")),
}
}
}
pub struct NoopAuthService {
session: SessionService,
config: ConfigClap,
}
#[async_trait]
impl Auth for NoopAuthService {
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, Url)> {
let url = Url::parse(&format!(
"{}/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(
&self,
_code: &str,
_state: &str,
_app_session_cookie: Option<String>,
) -> 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 cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie_value);
@ -202,15 +100,37 @@ impl Auth for NoopAuthService {
.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<String> {
async fn login_token(&self, _user: &str, _password: &str) -> anyhow::Result<IdToken> { self.introspection.get_id_token(password).await
todo!()
} }
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 {}
#[async_trait]
impl Auth for NoopAuthService {
async fn login(&self) -> anyhow::Result<Url> {
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<String> {
todo!()
}
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::headers::authorization::Basic;
use axum::headers::{Authorization, Cookie};
use axum::http::request::Parts; use axum::http::request::Parts;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{ErrorResponse, IntoResponse, Redirect}; use axum::response::{ErrorResponse, IntoResponse, Redirect};
use axum::routing::get; use axum::routing::get;
use axum::{async_trait, Json, RequestPartsExt, Router}; use axum::{async_trait, Json, RequestPartsExt, Router, TypedHeader};
use axum_extra::extract::CookieJar;
use axum_extra::headers::authorization::Basic;
use axum_extra::headers::{Authorization, Cookie};
use axum_extra::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()?;
@ -126,11 +122,7 @@ where
})?; })?;
return Ok(UserFromSession { return Ok(UserFromSession {
user: User { user: User { id: token },
id: token.sub,
email: token.email,
name: token.name,
},
}); });
} }
@ -153,6 +145,8 @@ where
) )
})?; })?;
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: