auth0-integration 0.6.1

Auth0 client library for M2M token retrieval and JWT validation (RS256)
Documentation
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Deserializer};

use super::decoded_access_token::DecodedAccessToken;

pub trait IntoPermissions {
    fn into_permissions(self) -> Vec<&'static str>;
}

impl IntoPermissions for &'static str {
    fn into_permissions(self) -> Vec<&'static str> {
        vec![self]
    }
}

impl<const N: usize> IntoPermissions for [&'static str; N] {
    fn into_permissions(self) -> Vec<&'static str> {
        self.to_vec()
    }
}

#[derive(Debug)]
pub struct AccessToken {
    pub token: String,
}

impl AccessToken {
    pub fn new(token: String) -> Self {
        Self { token }
    }

    pub fn as_str(&self) -> &str {
        &self.token
    }

    pub fn decoded(&self) -> Result<DecodedAccessToken, String> {
        decode_access_token(&self.token)
    }

    pub fn is_expired(&self) -> bool {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        self.decoded().map_or(true, |d| d.exp <= now)
    }

    pub fn auth0_id(&self) -> Option<String> {
        self.decoded().ok().map(|d| d.sub)
    }

    pub fn user_roles(&self) -> Vec<String> {
        self.decoded().ok().map(|d| d.user_roles).unwrap_or_default()
    }

    pub fn permissions(&self) -> Vec<String> {
        self.decoded().ok().map(|d| d.permissions).unwrap_or_default()
    }

    pub fn validate_permissions<P: IntoPermissions>(&self, permissions: P) -> bool {
        let Ok(decoded) = self.decoded() else { return false };
        permissions
            .into_permissions()
            .iter()
            .all(|p| decoded.permissions.iter().any(|g| g == p))
    }
}

impl<'de> Deserialize<'de> for AccessToken {
    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let token = String::deserialize(d)?;
        Ok(Self { token })
    }
}

fn decode_access_token(token: &str) -> Result<DecodedAccessToken, String> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        return Err("Invalid JWT format".to_string());
    }

    let decoded = URL_SAFE_NO_PAD
        .decode(parts[1])
        .map_err(|e| format!("Failed to base64-decode payload: {e}"))?;

    serde_json::from_slice(&decoded)
        .map_err(|e| format!("Failed to parse payload JSON: {e}"))
}