Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve auth0 integration #104

Merged
merged 3 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/config/rpc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions src/bin/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -75,6 +78,9 @@ impl From<Config> 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,
Expand Down
7 changes: 3 additions & 4 deletions src/domain/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ use crate::domain::Result;
#[async_trait::async_trait]
pub trait Auth0Driven: Send + Sync {
fn verify(&self, token: &str) -> Result<String>;
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),
}

Expand All @@ -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?;
Expand Down
22 changes: 11 additions & 11 deletions src/domain/project/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +23,7 @@ use crate::domain::{
use super::{cache::ProjectDrivenCache, Project, ProjectSecret, StripeDriven};

pub async fn fetch(cache: Arc<dyn ProjectDrivenCache>, cmd: FetchCmd) -> Result<Vec<Project>> {
let (user_id, _) = assert_credential(&cmd.credential)?;
let user_id = assert_credential(&cmd.credential)?;

cache.find(&user_id, &cmd.page, &cmd.page_size).await
}
Expand All @@ -35,13 +35,13 @@ pub async fn create(
stripe: Arc<dyn StripeDriven>,
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 {
Expand Down Expand Up @@ -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<UserId> {
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(),
)),
Expand All @@ -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()));
Expand Down Expand Up @@ -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,
}
Expand All @@ -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(),
Expand All @@ -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(),
}
Expand All @@ -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(),
Expand Down
8 changes: 4 additions & 4 deletions src/domain/resource/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
}
Expand All @@ -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(),
Expand All @@ -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(),
}
Expand Down
2 changes: 1 addition & 1 deletion src/domain/usage/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
91 changes: 75 additions & 16 deletions src/driven/auth0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Self> {
pub async fn try_new(
url: &str,
client_id: &str,
client_secret: &str,
audience: &str,
) -> AnyhowResult<Self> {
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,
})
}
}

Expand Down Expand Up @@ -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::<ResponseAccessToken>()
.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::<ResponseProfile>().await?;

Ok((profile.name, profile.email))
}
Expand All @@ -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,
}
2 changes: 1 addition & 1 deletion src/drivers/grpc/middlewares/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
13 changes: 12 additions & 1 deletion src/drivers/grpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion test/expect
Original file line number Diff line number Diff line change
Expand Up @@ -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
paulobressan marked this conversation as resolved.
Show resolved Hide resolved
kubectl apply -f ./fabric.manifest-with-env.yaml
wait_for_pods $FABRIC_NAMESPACE "Fabric"
wait_for_pods $DAEMON_NAMESPACE "Daemon"

Expand Down
3 changes: 3 additions & 0 deletions test/fabric.manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading