Compare commits
18 Commits
feat/with-
...
main
Author | SHA1 | Date | |
---|---|---|---|
63c29999bf | |||
a470122745 | |||
d32d343695 | |||
251d3922bf | |||
1b8924e7f6 | |||
f586d157b1 | |||
66080374b0 | |||
92e435e080 | |||
9510b8fc42 | |||
6bf847a24a | |||
d4a162876a | |||
835cd32fb1 | |||
9102ec4c1e | |||
d746ef0dd7 | |||
70915aec65 | |||
74362f3b1c | |||
21fc2587d4 | |||
c46bd34e16 |
2
.drone.yml
Normal file
2
.drone.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
kind: template
|
||||||
|
load: cuddle-rust-lib-plan.yaml
|
@ -1,34 +0,0 @@
|
|||||||
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"
|
|
||||||
|
|
1602
Cargo.lock
generated
1602
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@ -5,34 +5,40 @@ resolver = "2"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
nefarious-login = { path = "crates/nefarious-login" }
|
nefarious-login = { path = "crates/nefarious-login" }
|
||||||
|
|
||||||
anyhow = { version = "1.0.75" }
|
anyhow = { version = "1" }
|
||||||
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.17", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
clap = {version = "4.4.7", features = ["derive", "env"]}
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
async-trait = {version = "0.1.74", features = []}
|
async-trait = { version = "0.1", features = [] }
|
||||||
|
|
||||||
axum = {version = "0.6.20", features = []}
|
axum = { version = "0.7.1", features = [
|
||||||
axum-extra = {version = "0.8.0", features = ["cookie", "cookie-private"]}
|
"macros",
|
||||||
|
] }
|
||||||
|
axum-extra = { version = "0.9.0", features = [
|
||||||
|
"cookie",
|
||||||
|
"cookie-private",
|
||||||
|
"typed-header",
|
||||||
|
] }
|
||||||
axum-sessions = { version = "0.6.1", features = [] }
|
axum-sessions = { version = "0.6.1", features = [] }
|
||||||
async-sqlx-session = { version = "0.4.0", features = ["pg"] }
|
async-sqlx-session = { version = "0.4.0", features = ["pg"] }
|
||||||
|
|
||||||
serde = {version = "1.0", features = ["derive"]}
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = {version = "1.0.108"}
|
serde_json = { version = "1" }
|
||||||
|
|
||||||
uuid = {version = "1.5.0", features = []}
|
uuid = {version = "1.5.0", features = ["v4"]}
|
||||||
sqlx = { version = "0.7.2", features = [
|
sqlx = { version = "0.7", features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio",
|
||||||
"postgres",
|
"postgres",
|
||||||
"migrate",
|
"migrate",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
zitadel = { version = "3.4.29", features = ["axum"] }
|
zitadel = { version = "3.4", features = ["axum"] }
|
||||||
tower = "0.4.13"
|
tower = "0.4"
|
||||||
tower-http = { version = "0.4.4", features = ["cors", "trace"] }
|
tower-http = { version = "0.4", features = ["cors", "trace"] }
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
openidconnect = "3.4.0"
|
openidconnect = "3.4"
|
||||||
|
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
sealed_test = "1.0.0"
|
sealed_test = "1.0.0"
|
||||||
|
@ -6,17 +6,22 @@ use axum::http::{header::SET_COOKIE, HeaderMap};
|
|||||||
use oauth2::url::Url;
|
use oauth2::url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
introspection::IntrospectionService,
|
introspection::{IdToken, IntrospectionService},
|
||||||
login::{auth_clap::AuthEngine, AuthClap},
|
login::{auth_clap::AuthEngine, config::ConfigClap, AuthClap},
|
||||||
oauth::{zitadel::ZitadelConfig, OAuth},
|
oauth::{zitadel::ZitadelConfig, OAuth},
|
||||||
session::{SessionService, User},
|
session::{AppSession, SessionService, User},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Auth {
|
pub trait Auth {
|
||||||
async fn login(&self) -> anyhow::Result<Url>;
|
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, 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>;
|
||||||
async fn login_authorized(&self, code: &str, state: &str) -> anyhow::Result<(HeaderMap, Url)>;
|
async fn login_authorized(
|
||||||
|
&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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,14 +31,23 @@ 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 => Ok(Self::new_noop()),
|
AuthEngine::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(oauth, introspection, session))
|
Ok(Self::new_zitadel(
|
||||||
|
oauth,
|
||||||
|
introspection,
|
||||||
|
session,
|
||||||
|
&config.config,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,16 +56,21 @@ 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() -> Self {
|
pub fn new_noop(session: SessionService, config: &ConfigClap) -> Self {
|
||||||
Self(Arc::new(NoopAuthService {}))
|
Self(Arc::new(NoopAuthService {
|
||||||
|
session,
|
||||||
|
config: config.clone(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,20 +86,110 @@ 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) -> anyhow::Result<Url> {
|
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, 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(authorize_url)
|
Ok((headers, authorize_url))
|
||||||
}
|
}
|
||||||
async fn login_authorized(&self, code: &str, _state: &str) -> anyhow::Result<(HeaderMap, Url)> {
|
async fn login_authorized(
|
||||||
|
&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 user_id = self.introspection.get_id_token(token.as_str()).await?;
|
let id_token = self.introspection.get_id_token(token.as_str()).await?;
|
||||||
let cookie_value = self.session.insert_user("user", user_id.as_str()).await?;
|
let cookie_value = self.session.insert_user("user", id_token).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);
|
||||||
|
|
||||||
@ -89,41 +198,19 @@ impl Auth for ZitadelAuthService {
|
|||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
headers,
|
headers,
|
||||||
Url::parse("http://localhost:3001/authed")
|
Url::parse(&self.config.return_url)
|
||||||
.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> {
|
|
||||||
self.introspection.get_id_token(password).await
|
async fn login_token(&self, _user: &str, _password: &str) -> anyhow::Result<IdToken> {
|
||||||
|
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(User { id: u }),
|
Some(u) => Ok(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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
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::{HeaderMap, StatusCode, Uri};
|
||||||
|
use axum::response::{ErrorResponse, IntoResponse, Redirect, Response};
|
||||||
use axum::response::{ErrorResponse, IntoResponse, Redirect};
|
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{async_trait, Json, RequestPartsExt, Router, TypedHeader};
|
use axum::{async_trait, Json, RequestPartsExt, Router};
|
||||||
|
|
||||||
|
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;
|
use crate::auth::{AuthService, COOKIE_APP_SESSION_NAME};
|
||||||
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,9 +51,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 url = auth_service.login().await.into_response()?;
|
let (headers, url) = auth_service.login(None).await.into_response()?;
|
||||||
|
|
||||||
Ok(Redirect::to(url.as_ref()))
|
Ok((headers, Redirect::to(url.as_ref())))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -67,9 +66,14 @@ 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)
|
.login_authorized(&query.code, &query.state, cookie_value)
|
||||||
.await
|
.await
|
||||||
.into_response()?;
|
.into_response()?;
|
||||||
|
|
||||||
@ -93,13 +97,21 @@ 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 = (StatusCode, &'static str);
|
type Rejection = AuthRedirect;
|
||||||
|
|
||||||
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);
|
||||||
@ -110,43 +122,58 @@ 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 = auth_service
|
let token = match 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,
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
Err(e) => {
|
||||||
"could not get token from basic",
|
tracing::info!("did not find a basic login token, will trigger login");
|
||||||
)
|
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 { id: token },
|
user: User {
|
||||||
|
id: token.sub,
|
||||||
|
email: token.email,
|
||||||
|
name: token.name,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(anyhow::anyhow!("No session was found"))
|
tracing::info!("did not find a cookie, will trigger login");
|
||||||
.into_response()
|
let (headers, url) = auth_service
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "did not find a cookie"))?;
|
.login(Some(parts.uri.to_string()))
|
||||||
|
.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 = auth_service
|
let user = match 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,
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
Err(_) => {
|
||||||
"failed to decode session cookie",
|
tracing::info!("could not get user from session, will trigger login");
|
||||||
)
|
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 {
|
Ok(UserFromSession { user })
|
||||||
user: User { id: user.id },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ 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,
|
||||||
@ -15,10 +16,17 @@ 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<String>;
|
async fn get_id_token(&self, token: &str) -> anyhow::Result<IdToken>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IntrospectionService(Arc<dyn Introspection + Send + Sync + 'static>);
|
pub struct IntrospectionService(Arc<dyn Introspection + Send + Sync + 'static>);
|
||||||
@ -61,7 +69,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<String> {
|
async fn get_id_token(&self, token: &str) -> anyhow::Result<IdToken> {
|
||||||
let config = &self.state.config;
|
let config = &self.state.config;
|
||||||
let res = introspect(
|
let res = introspect(
|
||||||
&config.introspection_uri,
|
&config.introspection_uri,
|
||||||
@ -71,10 +79,21 @@ impl Introspection for ZitadelIntrospection {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(res
|
let sub = 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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ pub struct AuthConfigFile {
|
|||||||
zitadel: Option<ZitadelClap>,
|
zitadel: Option<ZitadelClap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[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")]
|
||||||
|
@ -105,12 +105,15 @@ impl OAuthClient for ZitadelOAuthClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn authorize_url(&self) -> anyhow::Result<Url> {
|
async fn authorize_url(&self) -> anyhow::Result<Url> {
|
||||||
let (auth_url, _csrf_token) = self
|
let req = 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()))
|
||||||
.url();
|
.add_scope(Scope::new("email".to_string()))
|
||||||
|
.add_scope(Scope::new("profile".to_string()));
|
||||||
|
|
||||||
|
let (auth_url, _csrf_token) = req.url();
|
||||||
|
|
||||||
Ok(auth_url)
|
Ok(auth_url)
|
||||||
}
|
}
|
||||||
|
@ -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::login::AuthClap;
|
use crate::{introspection::IdToken, 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)]
|
#[derive(clap::Args, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
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,16 +27,17 @@ pub struct PostgresqlSessionClap {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Session {
|
pub trait Session {
|
||||||
async fn insert_user(&self, id: &str, user_id: &str) -> anyhow::Result<String>;
|
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String>;
|
||||||
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<String>>;
|
async fn insert_user(&self, id: &str, id_token: IdToken) -> anyhow::Result<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 {}))),
|
SessionBackend::InMemory => Ok(Self(Arc::new(InMemorySessionService::default()))),
|
||||||
SessionBackend::Postgresql => {
|
SessionBackend::Postgresql => {
|
||||||
let postgres_session = PostgresSessionStore::new(
|
let postgres_session = PostgresSessionStore::new(
|
||||||
config
|
config
|
||||||
@ -48,6 +49,8 @@ impl SessionService {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
postgres_session.migrate().await?;
|
||||||
|
|
||||||
Ok(Self(Arc::new(PostgresSessionService {
|
Ok(Self(Arc::new(PostgresSessionService {
|
||||||
store: postgres_session,
|
store: postgres_session,
|
||||||
})))
|
})))
|
||||||
@ -71,16 +74,38 @@ 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_user(&self, _id: &str, user_id: &str) -> anyhow::Result<String> {
|
async fn insert(&self, app_session: AppSession) -> 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: user_id.to_string(),
|
id: id_token.sub,
|
||||||
|
email: id_token.email,
|
||||||
|
name: id_token.name,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -92,14 +117,14 @@ impl Session for PostgresSessionService {
|
|||||||
|
|
||||||
Ok(cookie)
|
Ok(cookie)
|
||||||
}
|
}
|
||||||
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<String>> {
|
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<User>> {
|
||||||
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.id))
|
Ok(Some(user))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@ -111,17 +136,50 @@ 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InMemorySessionService {}
|
#[derive(Default)]
|
||||||
|
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_user(&self, _id: &str, _user_id: &str) -> anyhow::Result<String> {
|
async fn insert(&self, app_session: AppSession) -> 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();
|
||||||
|
|
||||||
|
self.store.lock().unwrap().insert(id.to_string(), user);
|
||||||
|
|
||||||
|
Ok(id.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_user(&self, _cookie: &str) -> anyhow::Result<Option<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!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,3 +5,9 @@ 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, str::FromStr};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{FromRef, State},
|
extract::{FromRef, State},
|
||||||
@ -60,13 +60,10 @@ 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(([127, 0, 0, 1], 3001));
|
let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?;
|
||||||
println!("listening on: {addr}");
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
println!("open browser at: http://localhost:3001/auth/zitadel");
|
|
||||||
axum::Server::bind(&addr)
|
axum::serve(listener, app).await?;
|
||||||
.serve(app.into_make_service())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, str::FromStr};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{FromRef, State},
|
extract::{FromRef, State},
|
||||||
@ -58,10 +58,11 @@ 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)
|
|
||||||
.serve(app.into_make_service())
|
let addr = SocketAddr::from_str(&format!("{}:{}", "127.0.0.1", "3000"))?;
|
||||||
.await
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
.unwrap();
|
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
16
examples/custom_redirect/Cargo.toml
Normal file
16
examples/custom_redirect/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[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
|
94
examples/custom_redirect/src/main.rs
Normal file
94
examples/custom_redirect/src/main.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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)
|
||||||
|
}
|
16
examples/noop/Cargo.toml
Normal file
16
examples/noop/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[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
|
84
examples/noop/src/main.rs
Normal file
84
examples/noop/src/main.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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)
|
||||||
|
}
|
7
scripts/local_down.sh
Executable file
7
scripts/local_down.sh
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/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
|
9
scripts/local_up.sh
Executable file
9
scripts/local_up.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/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
|
7
templates/docker-compose.local_up.dockerignore
Normal file
7
templates/docker-compose.local_up.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
target/
|
||||||
|
.git/
|
||||||
|
.cuddle/
|
||||||
|
scripts/
|
||||||
|
cuddle.yaml
|
||||||
|
local.sh
|
||||||
|
README.md
|
17
templates/docker-compose.local_up.yml.tmpl
Normal file
17
templates/docker-compose.local_up.yml.tmpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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:
|
Loading…
Reference in New Issue
Block a user