#![warn(missing_docs)]
#[cfg(any(target_arch = "wasm32", feature = "jwt-rust-crypto"))]
extern crate jsonwebtoken10 as jsonwebtoken;
mod cache;
mod claims;
pub mod hasura;
mod jwt;
pub use cache::JwtCache;
pub use claims::Claims;
pub use hasura::{HasuraAuthConfig, HasuraIdentity, SecretOutcome, TokenRole};
pub use jwt::validate_token;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthResult {
pub role: String,
pub claims: HashMap<String, serde_json::Value>,
}
impl AuthResult {
pub fn anonymous(anon_role: &str) -> Self {
Self {
role: anon_role.to_string(),
claims: HashMap::new(),
}
}
pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
self.claims.get(key)
}
pub fn claims_json(&self) -> String {
serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
}
pub fn claim_settings(&self) -> Vec<(String, String)> {
let mut claims = Claims::from(self.claims.clone());
claims.set("role", serde_json::Value::String(self.role.clone()));
let mut settings = vec![("request.jwt.claims".to_string(), claims.to_json())];
settings.extend(claims.prefixed_entries("request.jwt.claims."));
settings
}
}
#[derive(Clone, Debug)]
pub struct JwtConfig {
pub secret: Option<String>,
pub secret_is_base64: bool,
pub audience: Option<String>,
pub role_claim_key: String,
pub anon_role: Option<String>,
}
impl Default for JwtConfig {
fn default() -> Self {
Self {
secret: None,
secret_is_base64: false,
audience: None,
role_claim_key: "role".to_string(),
anon_role: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ClaimFault {
#[error("JWT expired")]
Expired,
#[error("JWT not yet valid")]
NotYetValid,
#[error("JWT issued at future")]
IssuedInFuture,
#[error("JWT not in audience")]
NotInAudience,
#[error("Parsing claims failed")]
Unparsable,
#[error("The JWT '{0}' claim must be a number")]
NotANumber(&'static str),
#[error("The JWT 'aud' claim must be a string or an array of strings")]
AudienceNotStrings,
}
#[derive(Debug, thiserror::Error)]
pub enum JwtError {
#[error("Anonymous access is disabled")]
NoIdentity,
#[error("Unsupported token type")]
InvalidHeaderFormat,
#[error("Server lacks JWT secret")]
SecretMissing,
#[error("JWT cryptographic operation failed")]
InvalidSignature,
#[error("No suitable key or wrong key type")]
NoSuitableKey,
#[error("{0}")]
Claim(#[from] ClaimFault),
}
pub fn authenticate(auth_header: Option<&str>, config: &JwtConfig) -> Result<AuthResult, JwtError> {
let token = match auth_header {
Some(header) => extract_bearer_token(header)?,
None => {
return match &config.anon_role {
Some(role) => Ok(AuthResult::anonymous(role)),
None => Err(JwtError::NoIdentity),
};
}
};
validate_token(token, config)
}
fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
let header = header.trim();
if let Some(token) = header.strip_prefix("Bearer ") {
Ok(token.trim())
} else if let Some(token) = header.strip_prefix("bearer ") {
Ok(token.trim())
} else {
Err(JwtError::InvalidHeaderFormat)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_bearer_token() {
assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
assert!(extract_bearer_token("Basic abc123").is_err());
}
#[test]
fn test_auth_result_anonymous() {
let result = AuthResult::anonymous("anon");
assert_eq!(result.role, "anon");
assert!(result.claims.is_empty());
}
#[test]
fn auth_result_claim_settings_include_json_and_individual_claims() {
let result = AuthResult {
role: "authenticated_user".to_string(),
claims: HashMap::from([
("role".to_string(), serde_json::json!("untrusted_role")),
("user_id".to_string(), serde_json::json!("user-1")),
("org_class".to_string(), serde_json::json!(["defense"])),
]),
};
let settings = result.claim_settings();
let setting = |name: &str| {
settings
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_str())
};
let claims: serde_json::Value =
serde_json::from_str(setting("request.jwt.claims").unwrap()).unwrap();
assert_eq!(claims["role"], serde_json::json!("authenticated_user"));
assert_eq!(
setting("request.jwt.claims.role"),
Some("authenticated_user")
);
assert_eq!(setting("request.jwt.claims.user_id"), Some("user-1"));
assert_eq!(
setting("request.jwt.claims.org_class"),
Some(r#"["defense"]"#)
);
}
#[test]
fn test_authenticate_no_header_with_anon() {
let config = JwtConfig {
anon_role: Some("web_anon".to_string()),
..Default::default()
};
let result = authenticate(None, &config).unwrap();
assert_eq!(result.role, "web_anon");
}
#[test]
fn test_authenticate_no_header_no_anon() {
let config = JwtConfig::default();
assert!(authenticate(None, &config).is_err());
}
}