feat: with zitadel login
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
e991caef73
commit
1e38b2838c
@ -1,16 +1,19 @@
|
|||||||
use std::env;
|
use crate::router::AppState;
|
||||||
|
use crate::zitadel::{IntrospectionConfig, IntrospectionState};
|
||||||
|
|
||||||
use crate::zitadel::client::oauth_client;
|
use axum::extract::{FromRef, FromRequestParts, Query, State};
|
||||||
use crate::zitadel::{IntrospectionConfig, IntrospectionState, IntrospectionStateBuilder};
|
use axum::headers::Cookie;
|
||||||
|
use axum::http::request::Parts;
|
||||||
use axum::extract::{FromRef, Query, State};
|
use axum::http::StatusCode;
|
||||||
use axum::http::{header::SET_COOKIE, HeaderMap};
|
use axum::http::{header::SET_COOKIE, HeaderMap};
|
||||||
use axum::response::{IntoResponse, Redirect};
|
use axum::response::{IntoResponse, Redirect};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::{async_trait, RequestPartsExt, Router, TypedHeader};
|
||||||
use axum_sessions::async_session::{MemoryStore, Session, SessionStore};
|
use axum_sessions::async_session::{MemoryStore, Session, SessionStore};
|
||||||
|
use como_domain::users::User;
|
||||||
use como_infrastructure::register::ServiceRegister;
|
use como_infrastructure::register::ServiceRegister;
|
||||||
use oauth2::basic::BasicClient;
|
use oauth2::basic::BasicClient;
|
||||||
|
use oauth2::TokenIntrospectionResponse;
|
||||||
use oauth2::{reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope, TokenResponse};
|
use oauth2::{reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope, TokenResponse};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use zitadel::oidc::introspection::introspect;
|
use zitadel::oidc::introspection::introspect;
|
||||||
@ -25,7 +28,7 @@ pub async fn zitadel_auth(State(client): State<BasicClient>) -> impl IntoRespons
|
|||||||
Redirect::to(auth_url.as_ref())
|
Redirect::to(auth_url.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
static COOKIE_NAME: &str = "SESSION";
|
pub static COOKIE_NAME: &str = "SESSION";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -58,7 +61,14 @@ pub async fn login_authorized(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut session = Session::new();
|
let mut session = Session::new();
|
||||||
session.insert("user", &res).unwrap();
|
session
|
||||||
|
.insert(
|
||||||
|
"user",
|
||||||
|
User {
|
||||||
|
id: res.sub().unwrap().into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let cookie = store.store_session(session).await.unwrap().unwrap();
|
let cookie = store.store_session(session).await.unwrap().unwrap();
|
||||||
|
|
||||||
@ -73,52 +83,68 @@ pub async fn login_authorized(
|
|||||||
pub struct AuthController;
|
pub struct AuthController;
|
||||||
|
|
||||||
impl AuthController {
|
impl AuthController {
|
||||||
pub async fn new_router(_service_register: ServiceRegister) -> anyhow::Result<Router> {
|
pub async fn new_router(
|
||||||
let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!");
|
_service_register: ServiceRegister,
|
||||||
let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!");
|
app_state: AppState,
|
||||||
let zitadel_url = env::var("ZITADEL_URL").expect("missing ZITADEL_URL");
|
) -> anyhow::Result<Router> {
|
||||||
|
|
||||||
let is = IntrospectionStateBuilder::new(&zitadel_url)
|
|
||||||
.with_basic_auth(&client_id, &client_secret)
|
|
||||||
.build()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let store = MemoryStore::new();
|
|
||||||
let oauth_client = oauth_client();
|
|
||||||
let app_state = AppState {
|
|
||||||
oauth_client,
|
|
||||||
store,
|
|
||||||
introspection_state: is,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Router::new()
|
Ok(Router::new()
|
||||||
.route("/auth/zitadel", get(zitadel_auth))
|
.route("/zitadel", get(zitadel_auth))
|
||||||
.route("/auth/authorized", get(login_authorized))
|
.route("/authorized", get(login_authorized))
|
||||||
.with_state(app_state))
|
.with_state(app_state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct UserFromSession {}
|
||||||
struct AppState {
|
|
||||||
oauth_client: BasicClient,
|
|
||||||
introspection_state: IntrospectionState,
|
|
||||||
store: MemoryStore,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromRef<AppState> for BasicClient {
|
#[async_trait]
|
||||||
fn from_ref(state: &AppState) -> Self {
|
impl<S> FromRequestParts<S> for UserFromSession
|
||||||
state.oauth_client.clone()
|
where
|
||||||
}
|
MemoryStore: FromRef<S>,
|
||||||
}
|
S: Send + Sync,
|
||||||
|
{
|
||||||
impl FromRef<AppState> for MemoryStore {
|
type Rejection = (StatusCode, &'static str);
|
||||||
fn from_ref(state: &AppState) -> Self {
|
|
||||||
state.store.clone()
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
}
|
let store = MemoryStore::from_ref(state);
|
||||||
}
|
|
||||||
|
let cookie: Option<TypedHeader<Cookie>> = parts.extract().await.unwrap();
|
||||||
impl FromRef<AppState> for IntrospectionState {
|
|
||||||
fn from_ref(input: &AppState) -> Self {
|
let session_cookie = cookie.as_ref().and_then(|cookie| cookie.get(COOKIE_NAME));
|
||||||
input.introspection_state.clone()
|
if let None = session_cookie {
|
||||||
|
return Err((StatusCode::UNAUTHORIZED, "No session was found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_cookie = session_cookie.unwrap();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"UserFromSession: got session cookie from user agent, {}={}",
|
||||||
|
COOKIE_NAME,
|
||||||
|
session_cookie
|
||||||
|
);
|
||||||
|
// continue to decode the session cookie
|
||||||
|
let _user =
|
||||||
|
if let Some(session) = store.load_session(session_cookie.to_owned()).await.unwrap() {
|
||||||
|
if let Some(user) = session.get::<User>("user") {
|
||||||
|
tracing::debug!(
|
||||||
|
"UserFromSession: session decoded success, user_id={:?}",
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
user
|
||||||
|
} else {
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"No `user_id` found in session",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::debug!(
|
||||||
|
"UserIdFromSession: err session not exists in store, {}={}",
|
||||||
|
COOKIE_NAME,
|
||||||
|
session_cookie
|
||||||
|
);
|
||||||
|
return Err((StatusCode::BAD_REQUEST, "No session found for cookie"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(UserFromSession {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,48 @@
|
|||||||
|
use super::auth::UserFromSession;
|
||||||
|
use crate::{router::AppState};
|
||||||
|
|
||||||
|
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
||||||
use async_graphql::{EmptySubscription, Schema};
|
use async_graphql::{EmptySubscription, Schema};
|
||||||
use axum::{routing::get, Extension, Router};
|
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
||||||
use como_gql::{
|
use axum::response::Html;
|
||||||
graphql::{MutationRoot, QueryRoot},
|
use axum::{
|
||||||
graphql_handler, graphql_playground,
|
http::{StatusCode},
|
||||||
|
response::{IntoResponse},
|
||||||
|
routing::get,
|
||||||
|
Extension, Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
use como_gql::graphql::{ComoSchema, MutationRoot, QueryRoot};
|
||||||
use como_infrastructure::register::ServiceRegister;
|
use como_infrastructure::register::ServiceRegister;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
|
||||||
pub struct GraphQLController;
|
pub struct GraphQLController;
|
||||||
|
|
||||||
impl GraphQLController {
|
impl GraphQLController {
|
||||||
pub fn new_router(service_register: ServiceRegister) -> Router {
|
pub fn new_router(service_register: ServiceRegister, state: AppState) -> Router {
|
||||||
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
|
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
|
||||||
.data(service_register)
|
.data(service_register)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(graphql_playground).post(graphql_handler))
|
.route("/", get(graphql_playground).post(graphql_handler))
|
||||||
.layer(Extension(schema))
|
.layer(ServiceBuilder::new().layer(Extension(schema)))
|
||||||
|
.with_state(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn graphql_handler(
|
||||||
|
user: UserFromSession,
|
||||||
|
schema: Extension<ComoSchema>,
|
||||||
|
req: GraphQLRequest,
|
||||||
|
) -> Result<GraphQLResponse, StatusCode> {
|
||||||
|
let req = req.into_inner();
|
||||||
|
let req = req.data(user);
|
||||||
|
|
||||||
|
Ok(schema.execute(req).await.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn graphql_playground() -> impl IntoResponse {
|
||||||
|
Html(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
|
||||||
|
}
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::extract::FromRef;
|
||||||
http::{HeaderValue, Method},
|
use axum::http::{HeaderValue, Method};
|
||||||
response::IntoResponse,
|
use axum::Router;
|
||||||
Router,
|
use axum_sessions::async_session::MemoryStore;
|
||||||
};
|
|
||||||
use como_infrastructure::register::ServiceRegister;
|
use como_infrastructure::register::ServiceRegister;
|
||||||
|
use oauth2::basic::BasicClient;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||||
use zitadel::axum::introspection::IntrospectedUser;
|
|
||||||
|
|
||||||
use crate::controllers::auth::AuthController;
|
use crate::controllers::auth::AuthController;
|
||||||
use crate::controllers::graphql::GraphQLController;
|
use crate::controllers::graphql::GraphQLController;
|
||||||
|
use crate::zitadel::client::oauth_client;
|
||||||
async fn authed(user: IntrospectedUser) -> impl IntoResponse {
|
use crate::zitadel::{IntrospectionState, IntrospectionStateBuilder};
|
||||||
format!(
|
|
||||||
"Hello authorized user: {:?} with id {}",
|
|
||||||
user.username, user.user_id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Api;
|
pub struct Api;
|
||||||
|
|
||||||
@ -27,30 +23,52 @@ impl Api {
|
|||||||
cors_origin: &str,
|
cors_origin: &str,
|
||||||
service_register: ServiceRegister,
|
service_register: ServiceRegister,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!");
|
||||||
|
let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!");
|
||||||
|
let zitadel_url = env::var("ZITADEL_URL").expect("missing ZITADEL_URL");
|
||||||
|
|
||||||
|
let is = IntrospectionStateBuilder::new(&zitadel_url)
|
||||||
|
.with_basic_auth(&client_id, &client_secret)
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let store = MemoryStore::new();
|
||||||
|
let oauth_client = oauth_client();
|
||||||
|
let app_state = AppState {
|
||||||
|
oauth_client,
|
||||||
|
store,
|
||||||
|
introspection_state: is,
|
||||||
|
};
|
||||||
|
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.nest(
|
.nest(
|
||||||
"/auth",
|
"/auth",
|
||||||
AuthController::new_router(service_register.clone()).await?,
|
AuthController::new_router(service_register.clone(), app_state.clone()).await?,
|
||||||
)
|
)
|
||||||
.nest(
|
.nest(
|
||||||
"/graphql",
|
"/graphql",
|
||||||
GraphQLController::new_router(service_register.clone()),
|
GraphQLController::new_router(service_register.clone(), app_state.clone()),
|
||||||
)
|
)
|
||||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
|
||||||
.layer(
|
.layer(
|
||||||
CorsLayer::new()
|
ServiceBuilder::new()
|
||||||
.allow_origin(
|
.layer(TraceLayer::new_for_http())
|
||||||
cors_origin
|
.layer(
|
||||||
.parse::<HeaderValue>()
|
CorsLayer::new()
|
||||||
.context("could not parse cors origin as header")?,
|
.allow_origin(
|
||||||
)
|
cors_origin
|
||||||
.allow_headers([axum::http::header::CONTENT_TYPE])
|
.parse::<HeaderValue>()
|
||||||
.allow_methods([Method::GET, Method::POST, Method::OPTIONS]),
|
.context("could not parse cors origin as header")?,
|
||||||
|
)
|
||||||
|
.allow_headers([axum::http::header::CONTENT_TYPE])
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::OPTIONS]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!("running on: 0.0.0.0:{}", port);
|
let host = env::var("HOST").unwrap_or("0.0.0.0".to_string());
|
||||||
|
|
||||||
axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
|
tracing::info!("running on: {host}:{}", port);
|
||||||
|
|
||||||
|
axum::Server::bind(&format!("{host}:{}", port).parse().unwrap())
|
||||||
.serve(router.into_make_service())
|
.serve(router.into_make_service())
|
||||||
.await
|
.await
|
||||||
.context("error while starting API")?;
|
.context("error while starting API")?;
|
||||||
@ -58,3 +76,28 @@ impl Api {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
oauth_client: BasicClient,
|
||||||
|
introspection_state: IntrospectionState,
|
||||||
|
store: MemoryStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for BasicClient {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.oauth_client.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for MemoryStore {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.store.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for IntrospectionState {
|
||||||
|
fn from_ref(input: &AppState) -> Self {
|
||||||
|
input.introspection_state.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,3 +10,8 @@ pub struct UserDto {
|
|||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
@ -20,42 +20,6 @@ pub struct MutationRoot;
|
|||||||
|
|
||||||
#[Object]
|
#[Object]
|
||||||
impl MutationRoot {
|
impl MutationRoot {
|
||||||
async fn login(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
) -> anyhow::Result<bool> {
|
|
||||||
let service_register = ctx.data_unchecked::<ServiceRegister>();
|
|
||||||
|
|
||||||
let valid = service_register
|
|
||||||
.user_service
|
|
||||||
.validate_user(username, password)
|
|
||||||
.await?;
|
|
||||||
let returnvalid = match valid {
|
|
||||||
Some(..) => true,
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(returnvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn register(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let service_register = ctx.data_unchecked::<ServiceRegister>();
|
|
||||||
|
|
||||||
let user_id = service_register
|
|
||||||
.user_service
|
|
||||||
.add_user(username, password)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(user_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_item(
|
async fn create_item(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
@ -1,34 +1,5 @@
|
|||||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
|
||||||
use axum::{
|
|
||||||
extract::Extension,
|
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
|
||||||
use graphql::ComoSchema;
|
|
||||||
|
|
||||||
pub mod graphql;
|
pub mod graphql;
|
||||||
mod items;
|
mod items;
|
||||||
mod projects;
|
mod projects;
|
||||||
|
|
||||||
pub async fn graphql_handler(
|
|
||||||
schema: Extension<ComoSchema>,
|
|
||||||
req: GraphQLRequest,
|
|
||||||
) -> Result<GraphQLResponse, StatusCode> {
|
|
||||||
let req = req.into_inner();
|
|
||||||
|
|
||||||
Ok(schema.execute(req).await.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
//fn get_token_from_headers(headers: &HeaderMap) -> Option<Token> {
|
|
||||||
// headers.get("Authorization").and_then(|value| {
|
|
||||||
// let value = value.to_str().ok()?;
|
|
||||||
// let value = value.strip_prefix("Bearer ")?;
|
|
||||||
// Some(Token(value.to_string()))
|
|
||||||
// })
|
|
||||||
//}
|
|
||||||
|
|
||||||
pub async fn graphql_playground() -> impl IntoResponse {
|
|
||||||
Html(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"query": "\n SELECT * from users\n where username=$1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "username",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "password_hash",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "d3f222cf6c3d9816705426fdbed3b13cb575bb432eb1f33676c0b414e67aecaf"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user