authia 0.3.4

High-performance JWT verification library for Ed25519 using WebAssembly
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyOptions {
    /// Base64-encoded JWK (legacy, kept for backward compatibility)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub public_key_jwk: Option<String>,
    /// Raw JWK JSON string (recommended for better performance)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub public_key_jwk_raw: Option<String>,
    pub audience: String,
    pub issuer: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AccessTokenType {
    Access,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RefreshTokenType {
    Refresh,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccessTokenPayload {
    pub sub: Option<String>,
    pub iss: Option<String>,
    pub aud: Option<String>,
    pub auth_id: Option<String>,
    pub email: Option<String>,
    pub token_version: Option<u32>,
    pub exp: Option<i64>,
    pub iat: Option<i64>,
    #[serde(rename = "type")]
    pub token_type: Option<AccessTokenType>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshTokenPayload {
    pub sub: Option<String>,
    pub iss: Option<String>,
    pub aud: Option<String>,
    pub auth_id: Option<String>,
    pub email: Option<String>,
    pub token_version: Option<u32>,
    pub exp: Option<i64>,
    pub iat: Option<i64>,
    #[serde(rename = "type")]
    pub token_type: Option<RefreshTokenTokenType>,
    pub jti: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RefreshTokenTokenType {
    Refresh,
}

pub trait TokenPayload {
    fn get_type(&self) -> &str;
    fn expected_type() -> &'static str;
    fn validate(&self, options: &VerifyOptions, now: i64) -> Result<(), crate::error::AuthiaError>;
}

const CLOCK_SKEW_SECONDS: i64 = 300;

impl TokenPayload for AccessTokenPayload {
    fn get_type(&self) -> &str {
        "access"
    }

    fn expected_type() -> &'static str {
        "access"
    }

    fn validate(&self, options: &VerifyOptions, now: i64) -> Result<(), crate::error::AuthiaError> {
        use crate::error::AuthiaError;
        use subtle::ConstantTimeEq;

        // Verify sub (required)
        if self.sub.is_none() {
            return Err(AuthiaError::invalid_claims("Missing 'sub' claim"));
        }

        // Verify issuer (if present)
        if let Some(iss) = &self.iss {
            if !bool::from(iss.as_bytes().ct_eq(options.issuer.as_bytes())) {
                return Err(AuthiaError::invalid_issuer(&options.issuer, iss));
            }
        }

        // Verify audience (if present)
        if let Some(aud) = &self.aud {
            if !bool::from(aud.as_bytes().ct_eq(options.audience.as_bytes())) {
                return Err(AuthiaError::invalid_audience(&options.audience, aud));
            }
        }

        // Verify iat
        if let Some(iat) = self.iat {
            if iat > now + CLOCK_SKEW_SECONDS {
                return Err(AuthiaError::invalid_claims("Token issued in the future"));
            }
        }

        // Verify exp
        if let Some(exp) = self.exp {
            if exp < now - CLOCK_SKEW_SECONDS {
                return Err(AuthiaError::token_expired());
            }
        } else {
            return Err(AuthiaError::invalid_claims("Missing 'exp' claim"));
        }

        Ok(())
    }
}

impl TokenPayload for RefreshTokenPayload {
    fn get_type(&self) -> &str {
        "refresh"
    }

    fn expected_type() -> &'static str {
        "refresh"
    }

    fn validate(&self, options: &VerifyOptions, now: i64) -> Result<(), crate::error::AuthiaError> {
        use crate::error::AuthiaError;
        use subtle::ConstantTimeEq;

        // Verify sub (required)
        if self.sub.is_none() {
            return Err(AuthiaError::invalid_claims("Missing 'sub' claim"));
        }

        // Verify issuer (if present)
        if let Some(iss) = &self.iss {
            if !bool::from(iss.as_bytes().ct_eq(options.issuer.as_bytes())) {
                return Err(AuthiaError::invalid_issuer(&options.issuer, iss));
            }
        }

        // Verify audience (if present)
        if let Some(aud) = &self.aud {
            if !bool::from(aud.as_bytes().ct_eq(options.audience.as_bytes())) {
                return Err(AuthiaError::invalid_audience(&options.audience, aud));
            }
        }

        // Verify exp
        if let Some(exp) = self.exp {
            if exp < now - CLOCK_SKEW_SECONDS {
                return Err(AuthiaError::token_expired());
            }
        } else {
            return Err(AuthiaError::invalid_claims("Missing 'exp' claim"));
        }

        Ok(())
    }
}