feat/axum-0.7.x - uses breaking changes from axum 0.7.x #4
@ -9,18 +9,18 @@ use crate::{
|
|||||||
introspection::{IdToken, IntrospectionService},
|
introspection::{IdToken, IntrospectionService},
|
||||||
login::{auth_clap::AuthEngine, config::ConfigClap, 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, return_url: Option<String>) -> 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<IdToken>;
|
async fn login_token(&self, user: &str, password: &str) -> anyhow::Result<IdToken>;
|
||||||
async fn login_authorized(
|
async fn login_authorized(
|
||||||
&self,
|
&self,
|
||||||
code: &str,
|
code: &str,
|
||||||
state: &str,
|
state: &str,
|
||||||
return_url: Option<String>,
|
app_session_cookie: Option<String>,
|
||||||
) -> anyhow::Result<(HeaderMap, Url)>;
|
) -> 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>;
|
||||||
}
|
}
|
||||||
@ -89,19 +89,31 @@ pub struct ZitadelAuthService {
|
|||||||
config: ConfigClap,
|
config: ConfigClap,
|
||||||
}
|
}
|
||||||
pub static COOKIE_NAME: &str = "SESSION";
|
pub static COOKIE_NAME: &str = "SESSION";
|
||||||
|
pub static COOKIE_APP_SESSION_NAME: &str = "APP_SESSION";
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Auth for ZitadelAuthService {
|
impl Auth for ZitadelAuthService {
|
||||||
async fn login(&self, return_url: Option<String>) -> anyhow::Result<Url> {
|
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, Url)> {
|
||||||
let authorize_url = self.oauth.authorize_url(return_url).await?;
|
let mut headers = HeaderMap::new();
|
||||||
|
if let Some(return_url) = return_url.clone() {
|
||||||
|
let cookie_value = self.session.insert(AppSession { return_url }).await?;
|
||||||
|
|
||||||
Ok(authorize_url)
|
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?;
|
||||||
|
|
||||||
|
Ok((headers, authorize_url))
|
||||||
}
|
}
|
||||||
async fn login_authorized(
|
async fn login_authorized(
|
||||||
&self,
|
&self,
|
||||||
code: &str,
|
code: &str,
|
||||||
_state: &str,
|
_state: &str,
|
||||||
return_path: Option<String>,
|
app_session_cookie: Option<String>,
|
||||||
) -> anyhow::Result<(HeaderMap, Url)> {
|
) -> anyhow::Result<(HeaderMap, Url)> {
|
||||||
let token = self.oauth.exchange(code).await?;
|
let token = self.oauth.exchange(code).await?;
|
||||||
let id_token = self.introspection.get_id_token(token.as_str()).await?;
|
let id_token = self.introspection.get_id_token(token.as_str()).await?;
|
||||||
@ -113,8 +125,16 @@ impl Auth for ZitadelAuthService {
|
|||||||
headers.insert(SET_COOKIE, cookie.parse().unwrap());
|
headers.insert(SET_COOKIE, cookie.parse().unwrap());
|
||||||
|
|
||||||
let mut return_url = self.config.return_url.clone();
|
let mut return_url = self.config.return_url.clone();
|
||||||
if let Some(return_path) = return_path {
|
if let Some(cookie) = app_session_cookie {
|
||||||
return_url.push_str(&format!("?returnPath={return_path}"));
|
if let Some(session) = self.session.get(&cookie).await? {
|
||||||
|
if session.return_url.starts_with('/') {
|
||||||
|
let mut url = Url::parse(&return_url)?;
|
||||||
|
url.set_path(&session.return_url);
|
||||||
|
return_url = url.to_string();
|
||||||
|
} else {
|
||||||
|
return_url = session.return_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
@ -141,7 +161,7 @@ pub struct NoopAuthService {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Auth for NoopAuthService {
|
impl Auth for NoopAuthService {
|
||||||
async fn login(&self, return_url: Option<String>) -> anyhow::Result<Url> {
|
async fn login(&self, return_url: Option<String>) -> anyhow::Result<(HeaderMap, Url)> {
|
||||||
let url = Url::parse(&format!(
|
let url = Url::parse(&format!(
|
||||||
"{}/auth/authorized?code=noop&state=noop",
|
"{}/auth/authorized?code=noop&state=noop",
|
||||||
self.config
|
self.config
|
||||||
@ -151,13 +171,13 @@ impl Auth for NoopAuthService {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(url)
|
Ok((HeaderMap::new(), url))
|
||||||
}
|
}
|
||||||
async fn login_authorized(
|
async fn login_authorized(
|
||||||
&self,
|
&self,
|
||||||
_code: &str,
|
_code: &str,
|
||||||
_state: &str,
|
_state: &str,
|
||||||
_return_url: Option<String>,
|
_app_session_cookie: Option<String>,
|
||||||
) -> anyhow::Result<(HeaderMap, Url)> {
|
) -> anyhow::Result<(HeaderMap, Url)> {
|
||||||
let cookie_value = self
|
let cookie_value = self
|
||||||
.session
|
.session
|
||||||
|
@ -7,13 +7,14 @@ use axum::response::{ErrorResponse, IntoResponse, Redirect};
|
|||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{async_trait, Json, RequestPartsExt, Router};
|
use axum::{async_trait, Json, RequestPartsExt, Router};
|
||||||
|
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
use axum_extra::headers::authorization::Basic;
|
use axum_extra::headers::authorization::Basic;
|
||||||
use axum_extra::headers::{Authorization, Cookie};
|
use axum_extra::headers::{Authorization, Cookie};
|
||||||
use axum_extra::TypedHeader;
|
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)]
|
||||||
@ -50,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(None).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)]
|
||||||
@ -60,16 +61,19 @@ pub async fn zitadel_auth(
|
|||||||
pub struct AuthRequest {
|
pub struct AuthRequest {
|
||||||
code: String,
|
code: String,
|
||||||
state: String,
|
state: String,
|
||||||
#[serde(alias = "returnUrl")]
|
|
||||||
return_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, query.return_url)
|
.login_authorized(&query.code, &query.state, cookie_value)
|
||||||
.await
|
.await
|
||||||
.into_response()?;
|
.into_response()?;
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ impl Deref for OAuth {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OAuthClient {
|
pub trait OAuthClient {
|
||||||
async fn get_token(&self) -> anyhow::Result<()>;
|
async fn get_token(&self) -> anyhow::Result<()>;
|
||||||
async fn authorize_url(&self, return_url: Option<String>) -> anyhow::Result<Url>;
|
async fn authorize_url(&self) -> anyhow::Result<Url>;
|
||||||
async fn exchange(&self, code: &str) -> anyhow::Result<String>;
|
async fn exchange(&self, code: &str) -> anyhow::Result<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ impl OAuthClient for NoopOAuthClient {
|
|||||||
async fn get_token(&self) -> anyhow::Result<()> {
|
async fn get_token(&self) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn authorize_url(&self, return_url: Option<String>) -> anyhow::Result<Url> {
|
async fn authorize_url(&self) -> anyhow::Result<Url> {
|
||||||
Ok(Url::parse("http://localhost:3000/auth/zitadel").unwrap())
|
Ok(Url::parse("http://localhost:3000/auth/zitadel").unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ impl OAuthClient for ZitadelOAuthClient {
|
|||||||
async fn get_token(&self) -> anyhow::Result<()> {
|
async fn get_token(&self) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn authorize_url(&self, return_url: Option<String>) -> anyhow::Result<Url> {
|
async fn authorize_url(&self) -> anyhow::Result<Url> {
|
||||||
let req = self
|
let req = self
|
||||||
.client
|
.client
|
||||||
.authorize_url(CsrfToken::new_random)
|
.authorize_url(CsrfToken::new_random)
|
||||||
@ -113,18 +113,6 @@ impl OAuthClient for ZitadelOAuthClient {
|
|||||||
.add_scope(Scope::new("email".to_string()))
|
.add_scope(Scope::new("email".to_string()))
|
||||||
.add_scope(Scope::new("profile".to_string()));
|
.add_scope(Scope::new("profile".to_string()));
|
||||||
|
|
||||||
let req = {
|
|
||||||
if let Some(return_url) = return_url {
|
|
||||||
let mut redirect_url = self.client.redirect_url().unwrap().as_str().to_string();
|
|
||||||
|
|
||||||
redirect_url.push_str(&format!("?returnUrl={}", return_url));
|
|
||||||
|
|
||||||
req.set_redirect_uri(std::borrow::Cow::Owned(RedirectUrl::new(redirect_url)?))
|
|
||||||
} else {
|
|
||||||
req
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (auth_url, _csrf_token) = req.url();
|
let (auth_url, _csrf_token) = req.url();
|
||||||
|
|
||||||
Ok(auth_url)
|
Ok(auth_url)
|
||||||
|
@ -27,12 +27,13 @@ pub struct PostgresqlSessionClap {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Session {
|
pub trait Session {
|
||||||
|
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String>;
|
||||||
async fn insert_user(&self, id: &str, id_token: IdToken) -> anyhow::Result<String>;
|
async fn insert_user(&self, id: &str, id_token: IdToken) -> anyhow::Result<String>;
|
||||||
async fn get_user(&self, cookie: &str) -> anyhow::Result<Option<User>>;
|
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 {
|
||||||
@ -77,8 +78,26 @@ pub struct User {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct AppSession {
|
||||||
|
pub return_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Session for PostgresSessionService {
|
impl Session for PostgresSessionService {
|
||||||
|
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String> {
|
||||||
|
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> {
|
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(
|
||||||
@ -117,6 +136,18 @@ impl Session for PostgresSessionService {
|
|||||||
Err(anyhow::anyhow!("No session found for cookie"))
|
Err(anyhow::anyhow!("No session found for cookie"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get(&self, cookie: &str) -> anyhow::Result<Option<AppSession>> {
|
||||||
|
let Some(session) = self.store.load_session(cookie.to_string()).await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(session) = session.get::<AppSession>("app_session") else {
|
||||||
|
anyhow::bail!("failed to deserialize app_session from cookie");
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(session))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -126,6 +157,9 @@ pub struct InMemorySessionService {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Session for InMemorySessionService {
|
impl Session for InMemorySessionService {
|
||||||
|
async fn insert(&self, app_session: AppSession) -> anyhow::Result<String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn insert_user(&self, _id: &str, id_token: IdToken) -> anyhow::Result<String> {
|
async fn insert_user(&self, _id: &str, id_token: IdToken) -> anyhow::Result<String> {
|
||||||
let user = User {
|
let user = User {
|
||||||
id: id_token.sub,
|
id: id_token.sub,
|
||||||
@ -145,4 +179,7 @@ impl Session for InMemorySessionService {
|
|||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
async fn get(&self, cookie: &str) -> anyhow::Result<Option<AppSession>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
conn: Some("postgres://nefarious-test:somenotverysecurepassword@localhost:5432/nefarious-test".into()),
|
conn: Some("postgres://nefarious-test:somenotverysecurepassword@localhost:5432/nefarious-test".into()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
config: ConfigClap { return_url: "http://localhost:3001".into() } // this normally has /authed
|
config: ConfigClap { return_url: "http://localhost:3001/authed".into() } // this normally has /authed
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_service = AuthService::new(&auth).await?;
|
let auth_service = AuthService::new(&auth).await?;
|
||||||
@ -76,9 +76,9 @@ impl FromRef<AppState> for AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn login(State(auth_service): State<AuthService>) -> impl IntoResponse {
|
async fn login(State(auth_service): State<AuthService>) -> impl IntoResponse {
|
||||||
let url = auth_service.login(Some("/authed".into())).await.unwrap();
|
let (headers, url) = auth_service.login(Some("/authed".into())).await.unwrap();
|
||||||
|
|
||||||
Redirect::to(url.as_ref())
|
(headers, Redirect::to(url.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unauthed() -> String {
|
async fn unauthed() -> String {
|
||||||
|
Loading…
Reference in New Issue
Block a user