Skip to main content

auth0_integration/
auth0.rs

1use crate::{config::Auth0Config, error::AppError};
2use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, TokenData, Validation};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6// ---- Token request / response ----
7
8#[derive(Debug, Serialize)]
9struct ClientCredentialsRequest<'a> {
10    grant_type: &'a str,
11    client_id: &'a str,
12    client_secret: &'a str,
13    audience: &'a str,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct TokenResponse {
18    pub access_token: String,
19    pub token_type: String,
20    pub expires_in: u64,
21}
22
23// ---- JWT Claims ----
24
25#[derive(Debug, Deserialize, Serialize)]
26pub struct Claims {
27    pub sub: String,
28    pub iss: String,
29    pub aud: serde_json::Value,
30    pub exp: u64,
31    pub iat: u64,
32    #[serde(flatten)]
33    pub extra: HashMap<String, serde_json::Value>,
34}
35
36// ---- JWKS ----
37
38#[derive(Debug, Deserialize)]
39struct Jwks {
40    keys: Vec<Jwk>,
41}
42
43#[derive(Debug, Deserialize)]
44struct Jwk {
45    kid: String,
46    n: String,
47    e: String,
48}
49
50// ---- Auth0 client ----
51
52pub struct Auth0Client {
53    http: reqwest::Client,
54    config: Auth0Config,
55}
56
57impl Auth0Client {
58    pub fn new(config: Auth0Config) -> Self {
59        Self {
60            http: reqwest::Client::new(),
61            config,
62        }
63    }
64
65    /// Obtain a machine-to-machine access token via client credentials flow.
66    pub async fn get_access_token(&self) -> Result<TokenResponse, AppError> {
67        let body = ClientCredentialsRequest {
68            grant_type: "client_credentials",
69            client_id: &self.config.auth0_client_id,
70            client_secret: &self.config.auth0_client_secret,
71            audience: &self.config.auth0_audience,
72        };
73
74        let res = self
75            .http
76            .post(&self.config.auth0_token_url())
77            .json(&body)
78            .send()
79            .await?;
80
81        if !res.status().is_success() {
82            let text = res.text().await.unwrap_or_default();
83            return Err(AppError::Auth0(text));
84        }
85
86        Ok(res.json::<TokenResponse>().await?)
87    }
88
89    /// Validate a JWT access token using Auth0's JWKS endpoint.
90    pub async fn validate_token(&self, token: &str) -> Result<TokenData<Claims>, AppError> {
91        let header = decode_header(token)?;
92        let kid = header.kid.ok_or_else(|| AppError::InvalidToken("Missing kid".to_string()))?;
93
94        let decoding_key = self.fetch_decoding_key(&kid).await?;
95
96        let mut validation = Validation::new(Algorithm::RS256);
97        validation.set_issuer(&[self.config.auth0_issuer()]);
98        validation.set_audience(&[&self.config.auth0_audience]);
99
100        decode::<Claims>(token, &decoding_key, &validation).map_err(AppError::Jwt)
101    }
102
103    async fn fetch_decoding_key(&self, kid: &str) -> Result<DecodingKey, AppError> {
104        let jwks: Jwks = self
105            .http
106            .get(&self.config.auth0_jwks_uri())
107            .send()
108            .await?
109            .json()
110            .await?;
111
112        let key = jwks
113            .keys
114            .into_iter()
115            .find(|k| k.kid == kid)
116            .ok_or_else(|| AppError::InvalidToken(format!("No JWK found for kid: {kid}")))?;
117
118        DecodingKey::from_rsa_components(&key.n, &key.e).map_err(AppError::Jwt)
119    }
120}