From 1e38b2838cf534dc1a4f71fcfcf9d879e68951a9 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sun, 28 May 2023 15:05:16 +0200 Subject: [PATCH] feat: with zitadel login Signed-off-by: kjuulh --- como_api/src/controllers/auth.rs | 124 +++++++++++------- como_api/src/controllers/graphql.rs | 39 +++++- como_api/src/router.rs | 95 ++++++++++---- como_domain/src/users/mod.rs | 5 + como_gql/src/graphql.rs | 36 ----- como_gql/src/lib.rs | 29 ---- ...fe3ad16b72230967de01f640b7e4729b49fce.json | 33 +++++ 7 files changed, 215 insertions(+), 146 deletions(-) create mode 100644 como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json diff --git a/como_api/src/controllers/auth.rs b/como_api/src/controllers/auth.rs index 065fa17..6e4be06 100644 --- a/como_api/src/controllers/auth.rs +++ b/como_api/src/controllers/auth.rs @@ -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) -> 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 { - 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 { 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 for BasicClient { - fn from_ref(state: &AppState) -> Self { - state.oauth_client.clone() - } -} - -impl FromRef for MemoryStore { - fn from_ref(state: &AppState) -> Self { - state.store.clone() - } -} - -impl FromRef for IntrospectionState { - fn from_ref(input: &AppState) -> Self { - input.introspection_state.clone() +#[async_trait] +impl FromRequestParts for UserFromSession +where + MemoryStore: FromRef, + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let store = MemoryStore::from_ref(state); + + let cookie: Option> = 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") { + 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 {}) } } diff --git a/como_api/src/controllers/graphql.rs b/como_api/src/controllers/graphql.rs index 67bfdfd..e2f897a 100644 --- a/como_api/src/controllers/graphql.rs +++ b/como_api/src/controllers/graphql.rs @@ -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, + req: GraphQLRequest, +) -> Result { + 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"))) +} diff --git a/como_api/src/router.rs b/como_api/src/router.rs index 668243a..f379b5f 100644 --- a/como_api/src/router.rs +++ b/como_api/src/router.rs @@ -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::() - .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::() + .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 for BasicClient { + fn from_ref(state: &AppState) -> Self { + state.oauth_client.clone() + } +} + +impl FromRef for MemoryStore { + fn from_ref(state: &AppState) -> Self { + state.store.clone() + } +} + +impl FromRef for IntrospectionState { + fn from_ref(input: &AppState) -> Self { + input.introspection_state.clone() + } +} diff --git a/como_domain/src/users/mod.rs b/como_domain/src/users/mod.rs index cd1febc..c28c374 100644 --- a/como_domain/src/users/mod.rs +++ b/como_domain/src/users/mod.rs @@ -10,3 +10,8 @@ pub struct UserDto { pub username: String, pub email: String, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct User { + pub id: String, +} diff --git a/como_gql/src/graphql.rs b/como_gql/src/graphql.rs index aa6b848..e039929 100644 --- a/como_gql/src/graphql.rs +++ b/como_gql/src/graphql.rs @@ -20,42 +20,6 @@ pub struct MutationRoot; #[Object] impl MutationRoot { - async fn login( - &self, - ctx: &Context<'_>, - username: String, - password: String, - ) -> anyhow::Result { - let service_register = ctx.data_unchecked::(); - - 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 { - let service_register = ctx.data_unchecked::(); - - let user_id = service_register - .user_service - .add_user(username, password) - .await?; - - Ok(user_id) - } - async fn create_item( &self, ctx: &Context<'_>, diff --git a/como_gql/src/lib.rs b/como_gql/src/lib.rs index 604cfe3..8f093c4 100644 --- a/como_gql/src/lib.rs +++ b/como_gql/src/lib.rs @@ -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, - req: GraphQLRequest, -) -> Result { - let req = req.into_inner(); - - Ok(schema.execute(req).await.into()) -} - -//fn get_token_from_headers(headers: &HeaderMap) -> Option { -// 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"))) -} diff --git a/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json b/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json new file mode 100644 index 0000000..c881b13 --- /dev/null +++ b/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json @@ -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" +} \ No newline at end of file