diff --git a/examples/config/rpc.toml b/examples/config/rpc.toml index 7a641db..7566c9e 100644 --- a/examples/config/rpc.toml +++ b/examples/config/rpc.toml @@ -13,6 +13,9 @@ topic="events" [auth] url="https://txpipe.us.auth0.com" +client_id="" +client_secret="" +audience="" [stripe] url = "https://api.stripe.com/v1" diff --git a/src/bin/rpc.rs b/src/bin/rpc.rs index 75a9277..bdb9c23 100644 --- a/src/bin/rpc.rs +++ b/src/bin/rpc.rs @@ -35,6 +35,9 @@ async fn main() -> Result<()> { #[derive(Debug, Clone, Deserialize)] struct Auth { url: String, + client_id: String, + client_secret: String, + audience: String, } #[derive(Debug, Clone, Deserialize)] struct Stripe { @@ -75,6 +78,9 @@ impl From for GrpcConfig { db_path: value.db_path, crds_path: value.crds_path, auth_url: value.auth.url, + auth_client_id: value.auth.client_id, + auth_client_secret: value.auth.client_secret, + auth_audience: value.auth.audience, stripe_url: value.stripe.url, stripe_api_key: value.stripe.api_key, secret: value.secret, diff --git a/src/domain/auth/mod.rs b/src/domain/auth/mod.rs index 91fc5d2..bd1bf26 100644 --- a/src/domain/auth/mod.rs +++ b/src/domain/auth/mod.rs @@ -7,16 +7,15 @@ use crate::domain::Result; #[async_trait::async_trait] pub trait Auth0Driven: Send + Sync { fn verify(&self, token: &str) -> Result; - async fn find_info(&self, token: &str) -> Result<(String, String)>; + async fn find_info(&self, user_id: &str) -> Result<(String, String)>; } pub type UserId = String; -pub type Token = String; pub type SecretId = String; #[derive(Debug, Clone)] pub enum Credential { - Auth0(UserId, Token), + Auth0(UserId), ApiKey(SecretId), } @@ -26,7 +25,7 @@ pub async fn assert_project_permission( project_id: &str, ) -> Result<()> { match credential { - Credential::Auth0(user_id, _) => { + Credential::Auth0(user_id) => { let result = project_cache .find_user_permission(user_id, project_id) .await?; diff --git a/src/domain/project/command.rs b/src/domain/project/command.rs index 74d69c5..72afcfc 100644 --- a/src/domain/project/command.rs +++ b/src/domain/project/command.rs @@ -11,7 +11,7 @@ use tracing::{error, info}; use uuid::Uuid; use crate::domain::{ - auth::{Auth0Driven, Credential, Token, UserId}, + auth::{Auth0Driven, Credential, UserId}, error::Error, event::{ EventDrivenBridge, ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectUpdated, @@ -23,7 +23,7 @@ use crate::domain::{ use super::{cache::ProjectDrivenCache, Project, ProjectSecret, StripeDriven}; pub async fn fetch(cache: Arc, cmd: FetchCmd) -> Result> { - let (user_id, _) = assert_credential(&cmd.credential)?; + let user_id = assert_credential(&cmd.credential)?; cache.find(&user_id, &cmd.page, &cmd.page_size).await } @@ -35,13 +35,13 @@ pub async fn create( stripe: Arc, cmd: CreateCmd, ) -> Result<()> { - let (user_id, token) = assert_credential(&cmd.credential)?; + let user_id = assert_credential(&cmd.credential)?; if cache.find_by_namespace(&cmd.namespace).await?.is_some() { return Err(Error::CommandMalformed("invalid project namespace".into())); } - let (name, email) = auth0.find_info(&token).await?; + let (name, email) = auth0.find_info(&user_id).await?; let billing_provider_id = stripe.create_customer(&name, &email).await?; let evt = ProjectCreated { @@ -215,9 +215,9 @@ pub async fn verify_secret( Ok(secret) } -fn assert_credential(credential: &Credential) -> Result<(UserId, Token)> { +fn assert_credential(credential: &Credential) -> Result { match credential { - Credential::Auth0(user_id, token) => Ok((user_id.into(), token.into())), + Credential::Auth0(user_id) => Ok(user_id.into()), Credential::ApiKey(_) => Err(Error::Unauthorized( "project rpc doesnt support secret".into(), )), @@ -229,7 +229,7 @@ async fn assert_permission( project_id: &str, ) -> Result<()> { match credential { - Credential::Auth0(user_id, _) => { + Credential::Auth0(user_id) => { let result = cache.find_user_permission(user_id, project_id).await?; if result.is_none() { return Err(Error::Unauthorized("user doesnt have permission".into())); @@ -401,7 +401,7 @@ mod tests { impl Default for FetchCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), page: 1, page_size: 12, } @@ -410,7 +410,7 @@ mod tests { impl Default for CreateCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), id: Uuid::new_v4().to_string(), name: "New Project".into(), namespace: "sonic-vegas".into(), @@ -420,7 +420,7 @@ mod tests { impl Default for UpdateCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), id: Uuid::new_v4().to_string(), name: "Other name".into(), } @@ -429,7 +429,7 @@ mod tests { impl Default for CreateSecretCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), id: Uuid::new_v4().to_string(), project_id: Uuid::new_v4().to_string(), name: "Key 1".into(), diff --git a/src/domain/resource/command.rs b/src/domain/resource/command.rs index a6ee776..e58cb50 100644 --- a/src/domain/resource/command.rs +++ b/src/domain/resource/command.rs @@ -355,7 +355,7 @@ mod tests { impl Default for FetchCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), project_id: Uuid::new_v4().to_string(), page: 1, page_size: 12, @@ -365,7 +365,7 @@ mod tests { impl Default for FetchByIdCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), project_id: Uuid::new_v4().to_string(), resource_id: Uuid::new_v4().to_string(), } @@ -374,7 +374,7 @@ mod tests { impl Default for CreateCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), id: Uuid::new_v4().to_string(), project_id: Uuid::new_v4().to_string(), kind: "CardanoNodePort".into(), @@ -385,7 +385,7 @@ mod tests { impl Default for DeleteCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), resource_id: Uuid::new_v4().to_string(), project_id: Uuid::new_v4().to_string(), } diff --git a/src/domain/usage/command.rs b/src/domain/usage/command.rs index a3be63e..f24b524 100644 --- a/src/domain/usage/command.rs +++ b/src/domain/usage/command.rs @@ -95,7 +95,7 @@ mod tests { impl Default for FetchCmd { fn default() -> Self { Self { - credential: Credential::Auth0("user id".into(), "token".into()), + credential: Credential::Auth0("user id".into()), project_id: Uuid::new_v4().to_string(), page: 1, page_size: 12, diff --git a/src/driven/auth0/mod.rs b/src/driven/auth0/mod.rs index cd46cc8..f89c3fb 100644 --- a/src/driven/auth0/mod.rs +++ b/src/driven/auth0/mod.rs @@ -3,7 +3,7 @@ use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet}; use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; use reqwest::header::{HeaderValue, AUTHORIZATION}; use reqwest::Client; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tracing::error; use crate::domain::error::Error; @@ -12,21 +12,38 @@ use crate::domain::{auth::Auth0Driven, Result}; pub struct Auth0DrivenImpl { client: Client, url: String, + client_id: String, + client_secret: String, + audience: String, jwks: JwkSet, } impl Auth0DrivenImpl { - pub async fn try_new(url: &str) -> AnyhowResult { + pub async fn try_new( + url: &str, + client_id: &str, + client_secret: &str, + audience: &str, + ) -> AnyhowResult { let client = Client::new(); let url = url.to_string(); + let client_id = client_id.to_string(); + let client_secret = client_secret.to_string(); + let audience = audience.to_string(); let jwks_request = client .get(format!("{}/.well-known/jwks.json", url)) .build()?; - let jwks_response = client.execute(jwks_request).await?; let jwks = jwks_response.json().await?; - Ok(Self { client, url, jwks }) + Ok(Self { + client, + url, + client_id, + client_secret, + audience, + jwks, + }) } } @@ -65,30 +82,60 @@ impl Auth0Driven for Auth0DrivenImpl { Ok(decoded_token.claims.sub) } - async fn find_info(&self, token: &str) -> Result<(String, String)> { - let response = self + async fn find_info(&self, user_id: &str) -> Result<(String, String)> { + let request_payload = RequestAccessToken { + client_id: self.client_id.clone(), + client_secret: self.client_secret.clone(), + audience: self.audience.clone(), + grant_type: "client_credentials".into(), + }; + + // TODO: consider token expiration + let access_token_response = self + .client + .post(format!("{}/oauth/token", &self.url)) + .json(&request_payload) + .send() + .await?; + + let access_token_status = access_token_response.status(); + if access_token_status.is_client_error() || access_token_status.is_server_error() { + error!( + status = access_token_status.to_string(), + "Auth0 request error to get access token" + ); + return Err(Error::Unexpected(format!( + "Auth0 request error to get access token. Status: {}", + access_token_status + ))); + } + let access_token = access_token_response + .json::() + .await? + .access_token; + + let profile_response = self .client - .get(format!("{}/userinfo", &self.url)) + .get(format!("{}/api/v2/users/{user_id}", &self.url)) .header( AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), + HeaderValue::from_str(&format!("Bearer {access_token}")).unwrap(), ) .send() .await?; - let status = response.status(); - if status.is_client_error() || status.is_server_error() { + let profile_status = profile_response.status(); + if profile_status.is_client_error() || profile_status.is_server_error() { error!( - status = status.to_string(), - "request status code fail to get auth0 user info" + status = profile_status.to_string(), + "Auth0 request error to get user info" ); return Err(Error::Unexpected(format!( "Auth0 request error to get user info. Status: {}", - status + profile_status ))); } - - let profile: Profile = response.json().await?; + let profile = profile_response.json::().await?; Ok((profile.name, profile.email)) } @@ -100,7 +147,19 @@ struct Claims { } #[derive(Deserialize)] -struct Profile { +struct ResponseProfile { name: String, email: String, } + +#[derive(Serialize)] +struct RequestAccessToken { + client_id: String, + client_secret: String, + audience: String, + grant_type: String, +} +#[derive(Deserialize)] +struct ResponseAccessToken { + access_token: String, +} diff --git a/src/drivers/grpc/middlewares/auth.rs b/src/drivers/grpc/middlewares/auth.rs index d829687..dba957d 100644 --- a/src/drivers/grpc/middlewares/auth.rs +++ b/src/drivers/grpc/middlewares/auth.rs @@ -34,7 +34,7 @@ impl tonic::service::Interceptor for AuthenticatorImpl { let token = token.replace("Bearer ", ""); return match self.auth0.verify(&token) { Ok(user_id) => { - let credential = Credential::Auth0(user_id, token); + let credential = Credential::Auth0(user_id); request.extensions_mut().insert(credential); Ok(request) } diff --git a/src/drivers/grpc/mod.rs b/src/drivers/grpc/mod.rs index b443470..276a253 100644 --- a/src/drivers/grpc/mod.rs +++ b/src/drivers/grpc/mod.rs @@ -40,7 +40,15 @@ pub async fn server(config: GrpcConfig) -> Result<()> { let metadata = Arc::new(MetadataCrd::new(&config.crds_path)?); - let auth0 = Arc::new(Auth0DrivenImpl::try_new(&config.auth_url).await?); + let auth0 = Arc::new( + Auth0DrivenImpl::try_new( + &config.auth_url, + &config.auth_client_id, + &config.auth_client_secret, + &config.auth_audience, + ) + .await?, + ); let stripe = Arc::new(StripeDrivenImpl::new( &config.stripe_url, &config.stripe_api_key, @@ -100,6 +108,9 @@ pub struct GrpcConfig { pub db_path: String, pub crds_path: PathBuf, pub auth_url: String, + pub auth_client_id: String, + pub auth_client_secret: String, + pub auth_audience: String, pub stripe_url: String, pub stripe_api_key: String, pub secret: String, diff --git a/test/expect b/test/expect index 2c62928..9e01f84 100755 --- a/test/expect +++ b/test/expect @@ -60,7 +60,8 @@ wait_for_pods $KAFKA_NAMESPACE "Kafka" wait_for_pods $MOCK_NAMESPACE "Mock" # Apply Fabric manifest -kubectl apply -f ./test/fabric.manifest.yaml +envsubst < ./test/fabric.manifest.yaml > fabric.manifest-with-env.yaml +kubectl apply -f ./fabric.manifest-with-env.yaml wait_for_pods $FABRIC_NAMESPACE "Fabric" wait_for_pods $DAEMON_NAMESPACE "Daemon" diff --git a/test/fabric.manifest.yaml b/test/fabric.manifest.yaml index 057f2a6..43b83ac 100644 --- a/test/fabric.manifest.yaml +++ b/test/fabric.manifest.yaml @@ -111,6 +111,9 @@ data: [auth] url="https://dev-dflg0ssi.us.auth0.com" + client_id="f6y19wTU92tkVAasM5VubeEOsDSES56X" + client_secret="${CLIENT_SECRET}" + audience="https://dev-dflg0ssi.us.auth0.com/api/v2/" [stripe] url = "http://api.demeter-mock.svc.cluster.local/stripe"