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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.16"
|
||||
@ -505,6 +514,18 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "basic"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"clap",
|
||||
"nefarious-login",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@ -1773,6 +1794,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
@ -1852,6 +1882,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"sealed_test",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx 0.6.3",
|
||||
"tokio",
|
||||
"tower",
|
||||
@ -1871,6 +1902,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.4"
|
||||
@ -2057,6 +2098,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@ -2384,6 +2431,50 @@ dependencies = [
|
||||
"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]]
|
||||
name = "reqwest"
|
||||
version = "0.11.22"
|
||||
@ -2825,6 +2916,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
@ -3229,6 +3329,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.3.30"
|
||||
@ -3427,6 +3537,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@ -3520,6 +3660,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.4.2"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
members = ["crates/*", "examples/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -8,6 +8,7 @@ nefarious-login = { path = "crates/nefarious-login" }
|
||||
anyhow = { version = "1.0.71" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
|
||||
clap = {version = "4.3.0", features = ["derive", "env"]}
|
||||
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 = []}
|
||||
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 = []}
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
|
@ -10,6 +10,7 @@ axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
axum-sessions.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
sqlx.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 login;
|
||||
pub mod oauth;
|
||||
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)]
|
||||
mod test {
|
||||
use clap::Parser;
|
||||
|
@ -32,6 +32,7 @@ pub trait Session {
|
||||
}
|
||||
|
||||
pub struct SessionService(Arc<dyn Session + Send + Sync + 'static>);
|
||||
|
||||
impl SessionService {
|
||||
pub async fn new(config: &AuthClap) -> anyhow::Result<Self> {
|
||||
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