use std::collections::HashMap;
use std::time::Duration;
use http::HeaderMap;
use jsonwebtoken::Algorithm;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::config::primitives::{file_path::FilePath, http_header::HttpHeaderName};
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct JwtAuthConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub jwks_providers: Vec<JwksProviderSourceConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audiences: Option<Vec<String>>,
#[serde(
default = "default_lookup_location",
skip_serializing_if = "Vec::is_empty"
)]
pub lookup_locations: Vec<JwtAuthPluginLookupLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_authentication: Option<bool>,
#[serde(
skip_serializing_if = "Option::is_none",
default = "default_allowed_algorithms"
)]
#[schemars(with = "Option<Vec<String>>")]
pub allowed_algorithms: Option<Vec<Algorithm>>,
#[serde(default = "default_forward_claims_to_upstream_extensions")]
pub forward_claims_to_upstream_extensions: JwtClaimsForwardingConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes_claim: Option<String>,
}
impl JwtAuthConfig {
pub fn is_jwt_extensions_forwarding_enabled(&self) -> bool {
self.is_jwt_auth_enabled() && self.forward_claims_to_upstream_extensions.enabled
}
pub fn is_jwt_auth_enabled(&self) -> bool {
self.enabled
}
pub fn is_jwt_auth_disabled(&self) -> bool {
!self.is_jwt_auth_enabled()
}
}
fn default_enabled() -> bool {
false
}
impl Default for JwtAuthConfig {
fn default() -> Self {
JwtAuthConfig {
enabled: default_enabled(),
require_authentication: None,
lookup_locations: vec![],
jwks_providers: vec![],
forward_claims_to_upstream_extensions: default_forward_claims_to_upstream_extensions(),
audiences: None,
issuers: None,
allowed_algorithms: None,
scopes_claim: None,
}
}
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[non_exhaustive]
pub struct JwtClaimsForwardingConfig {
pub enabled: bool,
pub field_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_claims: Option<Vec<String>>,
}
fn default_forward_claims_to_upstream_extensions() -> JwtClaimsForwardingConfig {
JwtClaimsForwardingConfig {
enabled: false,
field_name: "jwt".to_string(),
include_claims: None,
}
}
#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)]
#[serde(tag = "source")]
#[non_exhaustive]
pub enum JwksProviderSourceConfig {
#[serde(rename = "file")]
#[schemars(title = "file")]
File {
#[serde(rename = "path")]
file: FilePath,
},
#[serde(rename = "remote")]
#[schemars(title = "remote")]
Remote {
url: String,
#[serde(
deserialize_with = "humantime_serde::deserialize",
serialize_with = "humantime_serde::serialize",
default = "default_polling_interval"
)]
#[schemars(with = "String")]
polling_interval: Option<Duration>,
prefetch: Option<bool>,
#[serde(
default,
with = "http_serde::header_map",
skip_serializing_if = "HeaderMap::is_empty"
)]
#[schemars(with = "HashMap<String, String>")]
headers: HeaderMap,
},
}
fn default_polling_interval() -> Option<Duration> {
Some(Duration::from_secs(10 * 60))
}
pub fn default_lookup_location() -> Vec<JwtAuthPluginLookupLocation> {
vec![JwtAuthPluginLookupLocation::Header {
name: "Authorization".into(),
prefix: Some("Bearer".to_string()),
}]
}
pub fn default_allowed_algorithms() -> Option<Vec<Algorithm>> {
Some(vec![
Algorithm::HS256,
Algorithm::HS384,
Algorithm::HS512,
Algorithm::RS256,
Algorithm::RS384,
Algorithm::RS512,
Algorithm::ES256,
Algorithm::ES384,
Algorithm::PS256,
Algorithm::PS384,
Algorithm::PS512,
Algorithm::EdDSA,
])
}
#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)]
#[serde(tag = "source")]
#[non_exhaustive]
pub enum JwtAuthPluginLookupLocation {
#[serde(rename = "header")]
#[schemars(title = "header")]
Header {
name: HttpHeaderName,
prefix: Option<String>,
},
#[serde(rename = "cookies")]
#[schemars(title = "cookies")]
Cookie { name: String },
}
#[cfg(test)]
mod tests {
use super::{JwksProviderSourceConfig, JwtAuthConfig};
#[test]
fn scopes_claim_defaults_to_none() {
let config = JwtAuthConfig::default();
assert_eq!(config.scopes_claim, None);
}
#[test]
fn scopes_claim_is_none_when_not_specified_in_config() {
let config = serde_json::from_str::<JwtAuthConfig>(r#"{"jwks_providers":[]}"#)
.expect("config should parse");
assert_eq!(config.scopes_claim, None);
}
#[test]
fn scopes_claim_parses_configured_claim_name() {
let config = serde_json::from_str::<JwtAuthConfig>(
r#"{"jwks_providers":[],"scopes_claim":"roles"}"#,
)
.expect("config should parse");
assert_eq!(config.scopes_claim, Some("roles".to_string()));
}
#[test]
fn remote_jwks_headers_default_to_empty() {
let config = serde_json::from_str::<JwtAuthConfig>(
r#"{"jwks_providers":[{"source":"remote","url":"https://example.com/jwks.json"}]}"#,
)
.expect("config should parse");
match &config.jwks_providers[0] {
JwksProviderSourceConfig::Remote { headers, .. } => assert!(headers.is_empty()),
other => panic!("expected a remote provider, got {other:?}"),
}
}
#[test]
fn remote_jwks_headers_are_parsed() {
let config = serde_json::from_str::<JwtAuthConfig>(
r#"{"jwks_providers":[{"source":"remote","url":"http://idp.internal/jwks.json","headers":{"Host":"auth.example.com","X-Api-Key":"secret"}}]}"#,
)
.expect("config should parse");
match &config.jwks_providers[0] {
JwksProviderSourceConfig::Remote { headers, .. } => {
assert_eq!(headers.len(), 2);
assert_eq!(headers.get("host").unwrap(), "auth.example.com");
assert_eq!(headers.get("x-api-key").unwrap(), "secret");
}
other => panic!("expected a remote provider, got {other:?}"),
}
}
#[test]
fn remote_jwks_rejects_invalid_header_name() {
let result = serde_json::from_str::<JwtAuthConfig>(
r#"{"jwks_providers":[{"source":"remote","url":"https://example.com/jwks.json","headers":{"Invalid Header":"value"}}]}"#,
);
assert!(result.is_err(), "a header name with a space must not parse");
}
}