feat: add basic example
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
f1cff02673
commit
131867ef61
146
Cargo.lock
generated
146
Cargo.lock
generated
@ -59,6 +59,15 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -505,6 +514,18 @@ version = "1.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "basic"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"axum",
|
||||||
|
"clap",
|
||||||
|
"nefarious-login",
|
||||||
|
"tokio",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@ -1773,6 +1794,15 @@ dependencies = [
|
|||||||
"value-bag",
|
"value-bag",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@ -1852,6 +1882,7 @@ dependencies = [
|
|||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"sealed_test",
|
"sealed_test",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sqlx 0.6.3",
|
"sqlx 0.6.3",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@ -1871,6 +1902,16 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@ -2057,6 +2098,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@ -2384,6 +2431,50 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.3",
|
||||||
|
"regex-syntax 0.8.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax 0.8.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.22"
|
version = "0.11.22"
|
||||||
@ -2825,6 +2916,15 @@ dependencies = [
|
|||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@ -3229,6 +3329,16 @@ dependencies = [
|
|||||||
"syn 2.0.38",
|
"syn 2.0.38",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@ -3427,6 +3537,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3520,6 +3660,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag"
|
name = "value-bag"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*"]
|
members = ["crates/*", "examples/*"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@ -8,6 +8,7 @@ nefarious-login = { path = "crates/nefarious-login" }
|
|||||||
anyhow = { version = "1.0.71" }
|
anyhow = { version = "1.0.71" }
|
||||||
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"] }
|
||||||
|
|
||||||
clap = {version = "4.3.0", features = ["derive", "env"]}
|
clap = {version = "4.3.0", features = ["derive", "env"]}
|
||||||
async-trait = {version = "0.1.68", features = []}
|
async-trait = {version = "0.1.68", features = []}
|
||||||
@ -17,7 +18,9 @@ axum-extra = {version = "0.7.4", features = ["cookie", "cookie-private"]}
|
|||||||
axum-sessions = {version = "0.5.0", features = []}
|
axum-sessions = {version = "0.5.0", features = []}
|
||||||
async-sqlx-session = {version = "0.4.0", features = ["pg"]}
|
async-sqlx-session = {version = "0.4.0", features = ["pg"]}
|
||||||
|
|
||||||
serde = {version = "1.0", features = []}
|
serde = {version = "1.0", features = ["derive"]}
|
||||||
|
serde_json = {version = "1.0.107"}
|
||||||
|
|
||||||
uuid = {version = "1.3.3", features = []}
|
uuid = {version = "1.3.3", features = []}
|
||||||
sqlx = { version = "0.6.2", features = [
|
sqlx = { version = "0.6.2", features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
|
@ -10,6 +10,7 @@ axum.workspace = true
|
|||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
axum-sessions.workspace = true
|
axum-sessions.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
128
crates/nefarious-login/src/auth.rs
Normal file
128
crates/nefarious-login/src/auth.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use axum::http::{header::SET_COOKIE, HeaderMap};
|
||||||
|
use oauth2::url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
introspection::IntrospectionService,
|
||||||
|
login::{config::AuthEngine, AuthClap},
|
||||||
|
oauth::{zitadel::ZitadelConfig, OAuth},
|
||||||
|
session::{SessionService, User},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Auth {
|
||||||
|
async fn login(&self) -> anyhow::Result<Url>;
|
||||||
|
async fn login_token(&self, user: &str, password: &str) -> anyhow::Result<String>;
|
||||||
|
async fn login_authorized(&self, code: &str, state: &str) -> anyhow::Result<(HeaderMap, Url)>;
|
||||||
|
async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthService(Arc<dyn Auth + Send + Sync + 'static>);
|
||||||
|
|
||||||
|
impl AuthService {
|
||||||
|
pub async fn new(config: &AuthClap, session: SessionService) -> anyhow::Result<Self> {
|
||||||
|
match config.engine {
|
||||||
|
AuthEngine::Noop => Ok(Self::new_noop()),
|
||||||
|
AuthEngine::Zitadel => {
|
||||||
|
let oauth: OAuth = ZitadelConfig::try_from(config.zitadel.clone())?.into();
|
||||||
|
let introspection: IntrospectionService =
|
||||||
|
IntrospectionService::new_zitadel(config).await?;
|
||||||
|
|
||||||
|
Ok(Self::new_zitadel(oauth, introspection, session))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_zitadel(
|
||||||
|
oauth: OAuth,
|
||||||
|
introspection: IntrospectionService,
|
||||||
|
session: SessionService,
|
||||||
|
) -> Self {
|
||||||
|
Self(Arc::new(ZitadelAuthService {
|
||||||
|
oauth,
|
||||||
|
introspection,
|
||||||
|
session,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_noop() -> Self {
|
||||||
|
Self(Arc::new(NoopAuthService {}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for AuthService {
|
||||||
|
type Target = Arc<dyn Auth + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ZitadelAuthService {
|
||||||
|
oauth: OAuth,
|
||||||
|
introspection: IntrospectionService,
|
||||||
|
session: SessionService,
|
||||||
|
}
|
||||||
|
pub static COOKIE_NAME: &str = "SESSION";
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Auth for ZitadelAuthService {
|
||||||
|
async fn login(&self) -> anyhow::Result<Url> {
|
||||||
|
let authorize_url = self.oauth.authorize_url().await?;
|
||||||
|
|
||||||
|
Ok(authorize_url)
|
||||||
|
}
|
||||||
|
async fn login_authorized(&self, code: &str, _state: &str) -> anyhow::Result<(HeaderMap, Url)> {
|
||||||
|
let token = self.oauth.exchange(code).await?;
|
||||||
|
let user_id = self.introspection.get_id_token(token.as_str()).await?;
|
||||||
|
let cookie_value = self.session.insert_user("user", user_id.as_str()).await?;
|
||||||
|
|
||||||
|
let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie_value);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(SET_COOKIE, cookie.parse().unwrap());
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
headers,
|
||||||
|
Url::parse("http://localhost:3000/dash/home")
|
||||||
|
.context("failed to parse login_authorized zitadel return url")?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
async fn login_token(&self, _user: &str, password: &str) -> anyhow::Result<String> {
|
||||||
|
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(User { id: u }),
|
||||||
|
None => Err(anyhow::anyhow!("failed to find user")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NoopAuthService {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Auth for NoopAuthService {
|
||||||
|
async fn login(&self) -> anyhow::Result<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!()
|
||||||
|
}
|
||||||
|
}
|
152
crates/nefarious-login/src/axum.rs
Normal file
152
crates/nefarious-login/src/axum.rs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
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::StatusCode;
|
||||||
|
|
||||||
|
use axum::response::{ErrorResponse, IntoResponse, Redirect};
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::{async_trait, Json, RequestPartsExt, Router, TypedHeader};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::auth::AuthService;
|
||||||
|
use crate::session::User;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ZitadelAuthParams {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
return_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait AnyhowExtensions<T, E>
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn into_response(self) -> Result<T, ErrorResponse>;
|
||||||
|
}
|
||||||
|
impl<T, E> AnyhowExtensions<T, E> for anyhow::Result<T, E>
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn into_response(self) -> Result<T, ErrorResponse> {
|
||||||
|
match self {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("failed with anyhow error: {}", e);
|
||||||
|
Err(ErrorResponse::from((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({
|
||||||
|
"status": "something",
|
||||||
|
})),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn zitadel_auth(
|
||||||
|
State(auth_service): State<AuthService>,
|
||||||
|
) -> Result<impl IntoResponse, ErrorResponse> {
|
||||||
|
let url = auth_service.login().await.into_response()?;
|
||||||
|
|
||||||
|
Ok(Redirect::to(url.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
code: String,
|
||||||
|
state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_authorized(
|
||||||
|
Query(query): Query<AuthRequest>,
|
||||||
|
State(auth_service): State<AuthService>,
|
||||||
|
) -> Result<impl IntoResponse, ErrorResponse> {
|
||||||
|
let (headers, url) = auth_service
|
||||||
|
.login_authorized(&query.code, &query.state)
|
||||||
|
.await
|
||||||
|
.into_response()?;
|
||||||
|
|
||||||
|
Ok((headers, Redirect::to(url.as_str())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthController;
|
||||||
|
|
||||||
|
impl AuthController {
|
||||||
|
pub async fn new_router(auth_service: AuthService) -> anyhow::Result<Router> {
|
||||||
|
Ok(Router::new()
|
||||||
|
.route("/zitadel", get(zitadel_auth))
|
||||||
|
.route("/authorized", get(login_authorized))
|
||||||
|
.with_state(auth_service))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UserFromSession {
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static COOKIE_NAME: &str = "SESSION";
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for UserFromSession
|
||||||
|
where
|
||||||
|
AuthService: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = (StatusCode, &'static str);
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let auth_service = AuthService::from_ref(state);
|
||||||
|
|
||||||
|
let cookie: Option<TypedHeader<Cookie>> = parts.extract().await.unwrap();
|
||||||
|
let session_cookie = cookie.as_ref().and_then(|cookie| cookie.get(COOKIE_NAME));
|
||||||
|
if session_cookie.is_none() {
|
||||||
|
let basic: Option<TypedHeader<Authorization<Basic>>> = parts.extract().await.unwrap();
|
||||||
|
|
||||||
|
if let Some(basic) = basic {
|
||||||
|
let token = auth_service
|
||||||
|
.login_token(basic.username(), basic.password())
|
||||||
|
.await
|
||||||
|
.into_response()
|
||||||
|
.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"could not get token from basic",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(UserFromSession {
|
||||||
|
user: User { id: token },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(anyhow::anyhow!("No session was found"))
|
||||||
|
.into_response()
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "did not find a cookie"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_cookie = session_cookie.unwrap();
|
||||||
|
|
||||||
|
// continue to decode the session cookie
|
||||||
|
let user = auth_service
|
||||||
|
.get_user_from_session(session_cookie)
|
||||||
|
.await
|
||||||
|
.into_response()
|
||||||
|
.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"failed to decode session cookie",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(UserFromSession {
|
||||||
|
user: User { id: user.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,148 +1,10 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod axum;
|
||||||
pub mod introspection;
|
pub mod introspection;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
pub mod auth {
|
|
||||||
|
|
||||||
use std::{ops::Deref, sync::Arc};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use axum::http::{header::SET_COOKIE, HeaderMap};
|
|
||||||
use oauth2::url::Url;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
introspection::IntrospectionService,
|
|
||||||
login::{config::AuthEngine, AuthClap},
|
|
||||||
oauth::{zitadel::ZitadelConfig, OAuth},
|
|
||||||
session::{SessionService, User},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Auth {
|
|
||||||
async fn login(&self) -> anyhow::Result<Url>;
|
|
||||||
async fn login_token(&self, user: &str, password: &str) -> anyhow::Result<String>;
|
|
||||||
async fn login_authorized(
|
|
||||||
&self,
|
|
||||||
code: &str,
|
|
||||||
state: &str,
|
|
||||||
) -> anyhow::Result<(HeaderMap, Url)>;
|
|
||||||
async fn get_user_from_session(&self, cookie: &str) -> anyhow::Result<User>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AuthService(Arc<dyn Auth + Send + Sync + 'static>);
|
|
||||||
|
|
||||||
impl AuthService {
|
|
||||||
pub async fn new(config: &AuthClap, session: SessionService) -> anyhow::Result<Self> {
|
|
||||||
match config.engine {
|
|
||||||
AuthEngine::Noop => Ok(Self::new_noop()),
|
|
||||||
AuthEngine::Zitadel => {
|
|
||||||
let oauth: OAuth = ZitadelConfig::try_from(config.zitadel.clone())?.into();
|
|
||||||
let introspection: IntrospectionService =
|
|
||||||
IntrospectionService::new_zitadel(config).await?;
|
|
||||||
|
|
||||||
Ok(Self::new_zitadel(oauth, introspection, session))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_zitadel(
|
|
||||||
oauth: OAuth,
|
|
||||||
introspection: IntrospectionService,
|
|
||||||
session: SessionService,
|
|
||||||
) -> Self {
|
|
||||||
Self(Arc::new(ZitadelAuthService {
|
|
||||||
oauth,
|
|
||||||
introspection,
|
|
||||||
session,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_noop() -> Self {
|
|
||||||
Self(Arc::new(NoopAuthService {}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for AuthService {
|
|
||||||
type Target = Arc<dyn Auth + Send + Sync + 'static>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ZitadelAuthService {
|
|
||||||
oauth: OAuth,
|
|
||||||
introspection: IntrospectionService,
|
|
||||||
session: SessionService,
|
|
||||||
}
|
|
||||||
pub static COOKIE_NAME: &str = "SESSION";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Auth for ZitadelAuthService {
|
|
||||||
async fn login(&self) -> anyhow::Result<Url> {
|
|
||||||
let authorize_url = self.oauth.authorize_url().await?;
|
|
||||||
|
|
||||||
Ok(authorize_url)
|
|
||||||
}
|
|
||||||
async fn login_authorized(
|
|
||||||
&self,
|
|
||||||
code: &str,
|
|
||||||
_state: &str,
|
|
||||||
) -> anyhow::Result<(HeaderMap, Url)> {
|
|
||||||
let token = self.oauth.exchange(code).await?;
|
|
||||||
let user_id = self.introspection.get_id_token(token.as_str()).await?;
|
|
||||||
let cookie_value = self.session.insert_user("user", user_id.as_str()).await?;
|
|
||||||
|
|
||||||
let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie_value);
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert(SET_COOKIE, cookie.parse().unwrap());
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
headers,
|
|
||||||
Url::parse("http://localhost:3000/dash/home")
|
|
||||||
.context("failed to parse login_authorized zitadel return url")?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
async fn login_token(&self, _user: &str, password: &str) -> anyhow::Result<String> {
|
|
||||||
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(User { id: u }),
|
|
||||||
None => Err(anyhow::anyhow!("failed to find user")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NoopAuthService {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Auth for NoopAuthService {
|
|
||||||
async fn login(&self) -> anyhow::Result<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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -32,6 +32,7 @@ pub trait Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
16
examples/basic/Cargo.toml
Normal file
16
examples/basic/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "basic"
|
||||||
|
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
|
62
examples/basic/src/main.rs
Normal file
62
examples/basic/src/main.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{FromRef, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use nefarious_login::{
|
||||||
|
auth::AuthService,
|
||||||
|
axum::{AuthController, UserFromSession},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
auth: AuthService,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
|
// Change to zitadel test instance
|
||||||
|
let auth_service = AuthService::new_noop();
|
||||||
|
|
||||||
|
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(([127, 0, 0, 1], 3001));
|
||||||
|
println!("listening on: {addr}");
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user