1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! Auth0 OAuth / OIDC provider implementation.
use async_trait::async_trait;
use serde_json::json;
use crate::{
error::Result,
oidc_provider::OidcProvider,
provider::{OAuthProvider, TokenResponse, UserInfo},
};
/// Auth0 OAuth provider wrapper
///
/// Handles Auth0-specific OAuth flows and role mapping.
/// Supports both Auth0 rules and custom claim mapping.
#[derive(Debug)]
pub struct Auth0OAuth {
oidc: OidcProvider,
domain: String,
}
impl Auth0OAuth {
/// Create a new Auth0 OAuth provider
///
/// # Arguments
/// * `client_id` - Auth0 application client ID
/// * `client_secret` - Auth0 application client secret
/// * `auth0_domain` - Auth0 tenant domain (e.g., "example.auth0.com")
/// * `redirect_uri` - Redirect URI after authentication (e.g., "http://localhost:8000/auth/callback")
///
/// # Errors
///
/// Returns `AuthError` if OIDC discovery against the Auth0 domain fails.
pub async fn new(
client_id: String,
client_secret: String,
auth0_domain: String,
redirect_uri: String,
) -> Result<Self> {
let issuer_url = format!("https://{}", auth0_domain);
let oidc =
OidcProvider::new("auth0", &issuer_url, &client_id, &client_secret, &redirect_uri)
.await?;
Ok(Self {
oidc,
domain: auth0_domain,
})
}
/// Extract roles from Auth0 custom claims
///
/// Auth0 supports custom claim namespaces to avoid claim collisions.
/// This extracts roles from the standard Auth0 roles claim or custom namespace.
///
/// # Arguments
/// * `raw_claims` - Raw JWT claims from Auth0 token
#[must_use]
pub fn extract_roles(raw_claims: &serde_json::Value) -> Vec<String> {
// Try standard Auth0 roles claim first
if let Some(roles_val) = raw_claims.get("https://fraiseql.dev/roles") {
if let Ok(roles) = serde_json::from_value::<Vec<String>>(roles_val.clone()) {
return roles;
}
}
// Fallback: check for roles array
if let Some(roles_array) = raw_claims.get("roles") {
if let Ok(roles) = serde_json::from_value::<Vec<String>>(roles_array.clone()) {
return roles;
}
}
Vec::new()
}
/// Map Auth0 roles to FraiseQL role permissions
///
/// Maps Auth0 role names to FraiseQL role names.
/// Supports flexible role naming conventions.
///
/// # Arguments
/// * `auth0_roles` - List of Auth0 role names
#[must_use]
pub fn map_auth0_roles_to_fraiseql(auth0_roles: Vec<String>) -> Vec<String> {
auth0_roles
.into_iter()
.filter_map(|role| {
let role_lower = role.to_lowercase();
match role_lower.as_str() {
// Direct role matches
"admin" | "fraiseql-admin" | "administrators" | "fraiseql_admin" => {
Some("admin".to_string())
},
"operator" | "fraiseql-operator" | "operators" | "fraiseql_operator" => {
Some("operator".to_string())
},
"viewer" | "fraiseql-viewer" | "viewers" | "fraiseql_viewer" | "user"
| "fraiseql-user" | "viewer_user" | "read_only" => Some("viewer".to_string()),
// Common patterns
"admin_user" => Some("admin".to_string()),
"operator_user" => Some("operator".to_string()),
_ => None,
}
})
.collect()
}
/// Extract organization ID from Auth0 claims
///
/// Auth0 supports org_id in custom claims or extracted from domain.
///
/// # Arguments
/// * `raw_claims` - Raw JWT claims
/// * `email` - User email as fallback
#[must_use]
pub fn extract_org_id(raw_claims: &serde_json::Value, email: &str) -> Option<String> {
// Check for explicit org_id claim
if let Some(org_id_val) = raw_claims.get("org_id") {
if let Some(org_id_str) = org_id_val.as_str() {
return Some(org_id_str.to_string());
}
}
// Fallback: extract from email domain
email
.split('@')
.nth(1)
.and_then(|domain| domain.split('.').next())
.map(|domain_part| domain_part.to_string())
}
}
// Reason: OAuthProvider is defined with #[async_trait]; all implementations must match
// its transformed method signatures to satisfy the trait contract
// async_trait: dyn-dispatch required; remove when RTN + Send is stable (RFC 3425)
#[async_trait]
impl OAuthProvider for Auth0OAuth {
fn name(&self) -> &'static str {
"auth0"
}
fn authorization_url(&self, state: &str) -> String {
self.oidc.authorization_url(state)
}
async fn exchange_code(&self, code: &str) -> Result<TokenResponse> {
self.oidc.exchange_code(code).await
}
async fn user_info(&self, access_token: &str) -> Result<UserInfo> {
let mut user_info = self.oidc.user_info(access_token).await?;
// Extract Auth0-specific claims
let roles = Self::extract_roles(&user_info.raw_claims);
user_info.raw_claims["auth0_roles"] = json!(roles);
// Extract organization ID
let email = user_info.email.clone().unwrap_or_default();
if let Some(org_id) = Self::extract_org_id(&user_info.raw_claims, &email) {
user_info.raw_claims["org_id"] = json!(&org_id);
}
// Store Auth0 domain for reference
user_info.raw_claims["auth0_domain"] = json!(&self.domain);
// Add email verification status
if let Some(email_verified) = user_info.raw_claims.get("email_verified") {
user_info.raw_claims["auth0_email_verified"] = email_verified.clone();
}
Ok(user_info)
}
async fn refresh_token(&self, refresh_token: &str) -> Result<TokenResponse> {
self.oidc.refresh_token(refresh_token).await
}
async fn revoke_token(&self, token: &str) -> Result<()> {
self.oidc.revoke_token(token).await
}
}