fraiseql_auth/providers/
auth0.rs1use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5
6use crate::{
7 error::Result,
8 oidc_provider::OidcProvider,
9 provider::{OAuthProvider, TokenResponse, UserInfo},
10};
11
12#[derive(Debug)]
17pub struct Auth0OAuth {
18 oidc: OidcProvider,
19 domain: String,
20}
21
22#[derive(Debug, Clone, Deserialize)]
24pub struct Auth0User {
25 pub sub: String,
27 pub email: String,
29 pub email_verified: Option<bool>,
31 pub name: Option<String>,
33 pub picture: Option<String>,
35 pub locale: Option<String>,
37 pub nickname: Option<String>,
39}
40
41#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct Auth0Roles {
44 pub roles: Option<Vec<String>>,
46}
47
48impl Auth0OAuth {
49 pub async fn new(
61 client_id: String,
62 client_secret: String,
63 auth0_domain: String,
64 redirect_uri: String,
65 ) -> Result<Self> {
66 let issuer_url = format!("https://{}", auth0_domain);
67
68 let oidc =
69 OidcProvider::new("auth0", &issuer_url, &client_id, &client_secret, &redirect_uri)
70 .await?;
71
72 Ok(Self {
73 oidc,
74 domain: auth0_domain,
75 })
76 }
77
78 #[must_use]
86 pub fn extract_roles(raw_claims: &serde_json::Value) -> Vec<String> {
87 if let Some(roles_val) = raw_claims.get("https://fraiseql.dev/roles") {
89 if let Ok(roles) = serde_json::from_value::<Vec<String>>(roles_val.clone()) {
90 return roles;
91 }
92 }
93
94 if let Some(roles_array) = raw_claims.get("roles") {
96 if let Ok(roles) = serde_json::from_value::<Vec<String>>(roles_array.clone()) {
97 return roles;
98 }
99 }
100
101 Vec::new()
102 }
103
104 #[must_use]
112 pub fn map_auth0_roles_to_fraiseql(auth0_roles: Vec<String>) -> Vec<String> {
113 auth0_roles
114 .into_iter()
115 .filter_map(|role| {
116 let role_lower = role.to_lowercase();
117
118 match role_lower.as_str() {
119 "admin" | "fraiseql-admin" | "administrators" | "fraiseql_admin" => {
121 Some("admin".to_string())
122 },
123 "operator" | "fraiseql-operator" | "operators" | "fraiseql_operator" => {
124 Some("operator".to_string())
125 },
126 "viewer" | "fraiseql-viewer" | "viewers" | "fraiseql_viewer" | "user"
127 | "fraiseql-user" | "viewer_user" | "read_only" => Some("viewer".to_string()),
128 "admin_user" => Some("admin".to_string()),
130 "operator_user" => Some("operator".to_string()),
131 _ => None,
132 }
133 })
134 .collect()
135 }
136
137 #[must_use]
145 pub fn extract_org_id(raw_claims: &serde_json::Value, email: &str) -> Option<String> {
146 if let Some(org_id_val) = raw_claims.get("org_id") {
148 if let Some(org_id_str) = org_id_val.as_str() {
149 return Some(org_id_str.to_string());
150 }
151 }
152
153 email
155 .split('@')
156 .nth(1)
157 .and_then(|domain| domain.split('.').next())
158 .map(|domain_part| domain_part.to_string())
159 }
160}
161
162#[async_trait]
166impl OAuthProvider for Auth0OAuth {
167 fn name(&self) -> &'static str {
168 "auth0"
169 }
170
171 fn authorization_url(&self, state: &str) -> String {
172 self.oidc.authorization_url(state)
173 }
174
175 async fn exchange_code(&self, code: &str) -> Result<TokenResponse> {
176 self.oidc.exchange_code(code).await
177 }
178
179 async fn user_info(&self, access_token: &str) -> Result<UserInfo> {
180 let mut user_info = self.oidc.user_info(access_token).await?;
181
182 let roles = Self::extract_roles(&user_info.raw_claims);
184 user_info.raw_claims["auth0_roles"] = json!(roles);
185
186 if let Some(org_id) = Self::extract_org_id(&user_info.raw_claims, &user_info.email) {
188 user_info.raw_claims["org_id"] = json!(&org_id);
189 }
190
191 user_info.raw_claims["auth0_domain"] = json!(&self.domain);
193
194 if let Some(email_verified) = user_info.raw_claims.get("email_verified") {
196 user_info.raw_claims["auth0_email_verified"] = email_verified.clone();
197 }
198
199 Ok(user_info)
200 }
201
202 async fn refresh_token(&self, refresh_token: &str) -> Result<TokenResponse> {
203 self.oidc.refresh_token(refresh_token).await
204 }
205
206 async fn revoke_token(&self, token: &str) -> Result<()> {
207 self.oidc.revoke_token(token).await
208 }
209}