chore(auth): with introspection

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2023-08-20 00:23:27 +02:00
parent 0893f285a3
commit 5837ee0288
Signed by: kjuulh
GPG Key ID: 9AA7BC13CE474394
9 changed files with 882 additions and 636 deletions

1325
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ members = [
"como_infrastructure", "como_infrastructure",
"como_gql", "como_gql",
"como_api", "como_api",
"como_auth",
] ]
resolver = "2" resolver = "2"
@ -16,6 +17,7 @@ como_domain = { path = "./como_domain/" }
como_infrastructure = { path = "./como_infrastructure/" } como_infrastructure = { path = "./como_infrastructure/" }
como_gql = { path = "./como_gql/" } como_gql = { path = "./como_gql/" }
como_api = { path = "./como_api/" } como_api = { path = "./como_api/" }
como_auth = { path = "./como_auth/" }
async-trait = "0.1.68" async-trait = "0.1.68"
async-graphql = { version = "5.0.9", features = ["uuid"] } async-graphql = { version = "5.0.9", features = ["uuid"] }
@ -51,3 +53,6 @@ clap = { version = "4.3.0", features = ["derive", "env"] }
argon2 = { version = "0.5.0" } argon2 = { version = "0.5.0" }
rand_core = { version = "0.6.4" } rand_core = { version = "0.6.4" }
pretty_assertions = "1.4.0"
sealed_test = "1.0.0"

View File

@ -10,7 +10,9 @@ como_gql.workspace = true
como_core.workspace = true como_core.workspace = true
como_domain.workspace = true como_domain.workspace = true
como_infrastructure.workspace = true como_infrastructure.workspace = true
como_auth.workspace = true
async-trait.workspace = true
async-graphql.workspace = true async-graphql.workspace = true
async-graphql-axum.workspace = true async-graphql-axum.workspace = true
axum.workspace = true axum.workspace = true

View File

@ -49,7 +49,6 @@ pub struct AuthRequest {
pub async fn login_authorized( pub async fn login_authorized(
Query(query): Query<AuthRequest>, Query(query): Query<AuthRequest>,
State(store): State<PostgresSessionStore>, State(store): State<PostgresSessionStore>,
State(oauth_client): State<BasicClient>,
State(introspection_state): State<IntrospectionState>, State(introspection_state): State<IntrospectionState>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let token = oauth_client let token = oauth_client
@ -111,7 +110,6 @@ pub struct UserFromSession {
impl<S> FromRequestParts<S> for UserFromSession impl<S> FromRequestParts<S> for UserFromSession
where where
PostgresSessionStore: FromRef<S>, PostgresSessionStore: FromRef<S>,
BasicClient: FromRef<S>,
IntrospectionState: FromRef<S>, IntrospectionState: FromRef<S>,
S: Send + Sync, S: Send + Sync,
{ {

View File

@ -12,7 +12,6 @@ use tower_http::{cors::CorsLayer, trace::TraceLayer};
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;
use crate::zitadel::{IntrospectionState, IntrospectionStateBuilder}; use crate::zitadel::{IntrospectionState, IntrospectionStateBuilder};
pub struct Api; pub struct Api;
@ -32,9 +31,7 @@ impl Api {
.build() .build()
.await?; .await?;
let oauth_client = oauth_client();
let app_state = AppState { let app_state = AppState {
oauth_client,
store: service_register.session_store.clone(), store: service_register.session_store.clone(),
introspection_state: is, introspection_state: is,
}; };
@ -79,17 +76,10 @@ impl Api {
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
oauth_client: BasicClient,
introspection_state: IntrospectionState, introspection_state: IntrospectionState,
store: PostgresSessionStore, store: PostgresSessionStore,
} }
impl FromRef<AppState> for BasicClient {
fn from_ref(state: &AppState) -> Self {
state.oauth_client.clone()
}
}
impl FromRef<AppState> for PostgresSessionStore { impl FromRef<AppState> for PostgresSessionStore {
fn from_ref(state: &AppState) -> Self { fn from_ref(state: &AppState) -> Self {
state.store.clone() state.store.clone()

View File

@ -1,7 +1,54 @@
use async_trait::async_trait;
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use std::env; use std::{env, ops::Deref, sync::Arc};
pub fn oauth_client() -> BasicClient { #[async_trait]
pub trait OAuthClient {
async fn get_token(&self) -> anyhow::Result<()>;
}
pub struct OAuth(Arc<dyn OAuthClient + Send + Sync + 'static>);
impl OAuth {
pub fn new_zitadel() -> Self {
Self(Arc::new(ZitadelOAuthClient {
client: oauth_client(),
}))
}
pub fn new_noop() -> Self {
Self(Arc::new(NoopOAuthClient {}))
}
}
impl Deref for OAuth {
type Target = Arc<dyn OAuthClient + Send + Sync + 'static>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct NoopOAuthClient;
#[async_trait]
impl OAuthClient for NoopOAuthClient {
async fn get_token(&self) -> anyhow::Result<()> {
Ok(())
}
}
pub struct ZitadelOAuthClient {
client: BasicClient,
}
#[async_trait]
impl OAuthClient for ZitadelOAuthClient {
async fn get_token(&self) -> anyhow::Result<()> {
Ok(())
}
}
fn oauth_client() -> BasicClient {
let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!"); let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!");
let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!"); let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!");
let redirect_url = env::var("REDIRECT_URL").expect("missing REDIRECT_URL"); let redirect_url = env::var("REDIRECT_URL").expect("missing REDIRECT_URL");

View File

@ -0,0 +1,120 @@
use anyhow::Context;
use axum::extract::FromRef;
use openidconnect::IntrospectionUrl;
use zitadel::{
axum::introspection::IntrospectionStateBuilderError,
credentials::Application,
oidc::{discovery::discover, introspection::AuthorityAuthentication},
};
#[derive(Clone, Debug)]
pub struct IntrospectionState {
pub(crate) config: IntrospectionConfig,
}
#[derive(clap::Args, Clone, Debug, PartialEq, Eq)]
pub struct IntrospectionConfigClap {
#[arg(
env = "ZITADEL_AUTHORITY",
long = "zitadel-authority",
group = "introspection"
)]
pub authority: String,
#[arg(
env = "ZITADEL_CLIENT_ID",
long = "zitadel-client-id",
group = "introspection"
)]
pub client_id: String,
#[arg(
env = "ZITADEL_CLIENT_SECRET",
long = "zitadel-client-secret",
group = "introspection"
)]
pub client_secret: String,
}
impl IntrospectionConfigClap {
async fn try_into(self) -> anyhow::Result<IntrospectionState> {
IntrospectionStateBuilder::new(&self.authority)
.with_basic_auth(&self.client_id, &self.client_secret)
.build()
.await
.context("failed to generate an introspection builder")
}
}
/// Configuration that must be inject into the axum application state. Used by the
/// [IntrospectionStateBuilder](super::IntrospectionStateBuilder). This struct is also used to create the [IntrospectionState](IntrospectionState)
#[derive(Debug, Clone)]
pub struct IntrospectionConfig {
pub authority: String,
pub authentication: AuthorityAuthentication,
pub introspection_uri: IntrospectionUrl,
}
impl FromRef<IntrospectionState> for IntrospectionConfig {
fn from_ref(input: &IntrospectionState) -> Self {
input.config.clone()
}
}
pub struct IntrospectionStateBuilder {
authority: String,
authentication: Option<AuthorityAuthentication>,
}
/// Builder for [IntrospectionConfig]
impl IntrospectionStateBuilder {
pub fn new(authority: &str) -> Self {
Self {
authority: authority.to_string(),
authentication: None,
}
}
pub fn with_basic_auth(
&mut self,
client_id: &str,
client_secret: &str,
) -> &mut IntrospectionStateBuilder {
self.authentication = Some(AuthorityAuthentication::Basic {
client_id: client_id.to_string(),
client_secret: client_secret.to_string(),
});
self
}
pub fn with_jwt_profile(&mut self, application: Application) -> &mut IntrospectionStateBuilder {
self.authentication = Some(AuthorityAuthentication::JWTProfile { application });
self
}
pub async fn build(&mut self) -> Result<IntrospectionState, IntrospectionStateBuilderError> {
let authentication = self
.authentication
.clone()
.ok_or(IntrospectionStateBuilderError::NoAuthSchema)?;
let metadata = discover(&self.authority)
.await
.map_err(|source| IntrospectionStateBuilderError::Discovery { source })?;
let introspection_uri = metadata
.additional_metadata()
.introspection_endpoint
.clone()
.ok_or(IntrospectionStateBuilderError::NoIntrospectionUrl)?;
Ok(IntrospectionState {
config: IntrospectionConfig {
authority: self.authority.clone(),
introspection_uri: introspection_uri,
authentication: authentication,
},
})
}
}

View File

@ -1 +1,2 @@
mod introspection;
mod oauth; mod oauth;

View File

@ -14,7 +14,7 @@ pub struct OAuthClientClap {
#[derive(Clone, clap::Args, Debug, PartialEq, Eq)] #[derive(Clone, clap::Args, Debug, PartialEq, Eq)]
pub struct NoopConfig { pub struct NoopConfig {
#[arg(env = "OAUTH_NOOP", long = "oauth-noop", group = "auth", global = true)] #[arg(env = "OAUTH_NOOP", long = "oauth-noop", group = "auth")]
pub oauth_noop: Option<bool>, pub oauth_noop: Option<bool>,
} }