use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_jwk: Option<String>,
#[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;
if self.sub.is_none() {
return Err(AuthiaError::invalid_claims("Missing 'sub' claim"));
}
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));
}
}
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));
}
}
if let Some(iat) = self.iat {
if iat > now + CLOCK_SKEW_SECONDS {
return Err(AuthiaError::invalid_claims("Token issued in the future"));
}
}
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;
if self.sub.is_none() {
return Err(AuthiaError::invalid_claims("Missing 'sub' claim"));
}
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));
}
}
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));
}
}
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(())
}
}