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 crate::zitadel::{IntrospectionConfig, IntrospectionState, IntrospectionStateBuilder};
|
||||
|
||||
use axum::extract::{FromRef, Query, State};
|
||||
use axum::extract::{FromRef, FromRequestParts, Query, State};
|
||||
use axum::headers::Cookie;
|
||||
use axum::http::request::Parts;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{header::SET_COOKIE, HeaderMap};
|
||||
use axum::response::{IntoResponse, Redirect};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use axum::{async_trait, RequestPartsExt, Router, TypedHeader};
|
||||
use axum_sessions::async_session::{MemoryStore, Session, SessionStore};
|
||||
use como_domain::users::User;
|
||||
use como_infrastructure::register::ServiceRegister;
|
||||
use oauth2::basic::BasicClient;
|
||||
use oauth2::TokenIntrospectionResponse;
|
||||
use oauth2::{reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope, TokenResponse};
|
||||
use serde::Deserialize;
|
||||
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())
|
||||
}
|
||||
|
||||
static COOKIE_NAME: &str = "SESSION";
|
||||
pub static COOKIE_NAME: &str = "SESSION";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
@ -58,7 +61,14 @@ pub async fn login_authorized(
|
||||
.unwrap();
|
||||
|
||||
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();
|
||||
|
||||
@ -73,52 +83,68 @@ pub async fn login_authorized(
|
||||
pub struct AuthController;
|
||||
|
||||
impl AuthController {
|
||||
pub async fn new_router(_service_register: ServiceRegister) -> anyhow::Result<Router> {
|
||||
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,
|
||||
};
|
||||
|
||||
pub async fn new_router(
|
||||
_service_register: ServiceRegister,
|
||||
app_state: AppState,
|
||||
) -> anyhow::Result<Router> {
|
||||
Ok(Router::new()
|
||||
.route("/auth/zitadel", get(zitadel_auth))
|
||||
.route("/auth/authorized", get(login_authorized))
|
||||
.route("/zitadel", get(zitadel_auth))
|
||||
.route("/authorized", get(login_authorized))
|
||||
.with_state(app_state))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
oauth_client: BasicClient,
|
||||
introspection_state: IntrospectionState,
|
||||
store: MemoryStore,
|
||||
}
|
||||
pub struct UserFromSession {}
|
||||
|
||||
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()
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for UserFromSession
|
||||
where
|
||||
MemoryStore: 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 store = MemoryStore::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 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 axum::{routing::get, Extension, Router};
|
||||
use como_gql::{
|
||||
graphql::{MutationRoot, QueryRoot},
|
||||
graphql_handler, graphql_playground,
|
||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
||||
use axum::response::Html;
|
||||
use axum::{
|
||||
http::{StatusCode},
|
||||
response::{IntoResponse},
|
||||
routing::get,
|
||||
Extension, Router,
|
||||
};
|
||||
|
||||
|
||||
use como_gql::graphql::{ComoSchema, MutationRoot, QueryRoot};
|
||||
use como_infrastructure::register::ServiceRegister;
|
||||
use tower::ServiceBuilder;
|
||||
|
||||
pub struct 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)
|
||||
.data(service_register)
|
||||
.finish();
|
||||
|
||||
Router::new()
|
||||
.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 axum::{
|
||||
http::{HeaderValue, Method},
|
||||
response::IntoResponse,
|
||||
Router,
|
||||
};
|
||||
use axum::extract::FromRef;
|
||||
use axum::http::{HeaderValue, Method};
|
||||
use axum::Router;
|
||||
use axum_sessions::async_session::MemoryStore;
|
||||
use como_infrastructure::register::ServiceRegister;
|
||||
use oauth2::basic::BasicClient;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||
use zitadel::axum::introspection::IntrospectedUser;
|
||||
|
||||
use crate::controllers::auth::AuthController;
|
||||
use crate::controllers::graphql::GraphQLController;
|
||||
|
||||
async fn authed(user: IntrospectedUser) -> impl IntoResponse {
|
||||
format!(
|
||||
"Hello authorized user: {:?} with id {}",
|
||||
user.username, user.user_id
|
||||
)
|
||||
}
|
||||
use crate::zitadel::client::oauth_client;
|
||||
use crate::zitadel::{IntrospectionState, IntrospectionStateBuilder};
|
||||
|
||||
pub struct Api;
|
||||
|
||||
@ -27,30 +23,52 @@ impl Api {
|
||||
cors_origin: &str,
|
||||
service_register: ServiceRegister,
|
||||
) -> 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()
|
||||
.nest(
|
||||
"/auth",
|
||||
AuthController::new_router(service_register.clone()).await?,
|
||||
AuthController::new_router(service_register.clone(), app_state.clone()).await?,
|
||||
)
|
||||
.nest(
|
||||
"/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(
|
||||
CorsLayer::new()
|
||||
.allow_origin(
|
||||
cors_origin
|
||||
.parse::<HeaderValue>()
|
||||
.context("could not parse cors origin as header")?,
|
||||
)
|
||||
.allow_headers([axum::http::header::CONTENT_TYPE])
|
||||
.allow_methods([Method::GET, Method::POST, Method::OPTIONS]),
|
||||
ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(
|
||||
cors_origin
|
||||
.parse::<HeaderValue>()
|
||||
.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())
|
||||
.await
|
||||
.context("error while starting API")?;
|
||||
@ -58,3 +76,28 @@ impl Api {
|
||||
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 email: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
}
|
||||
|
@ -20,42 +20,6 @@ pub struct MutationRoot;
|
||||
|
||||
#[Object]
|
||||
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(
|
||||
&self,
|
||||
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;
|
||||
mod items;
|
||||
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