use base64::{Engine, prelude::BASE64_STANDARD};
use clap::error::ErrorKind;
use crab_vault_auth::JwtConfig;
use jsonwebtoken::*;
use serde::{Deserialize, Serialize};
use crate::error::cli::{CliError, MultiCliError};
#[derive(Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct JwtConfigBuilder {
pub encoding: AlgKeyPair,
pub decoding: Vec<AlgKeyPair>,
pub validation: ValidationConfig,
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct AlgKeyPair {
algorithm: Algorithm,
form: KeyForm,
#[serde(alias = "path")]
key: String,
}
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum KeyForm {
#[default]
DerInline,
DerFile,
PemInline,
PemFile,
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ValidationConfig {
required_spec_claims: Vec<String>,
leeway: u64,
reject_tokens_expiring_in_less_than: u64,
validate_exp: bool,
validate_nbf: bool,
aud: Option<Vec<String>>,
iss: Option<Vec<String>>,
sub: Option<String>,
decode_algorithms: Vec<Algorithm>,
}
impl Default for JwtConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl JwtConfigBuilder {
pub fn new() -> Self {
Self {
encoding: AlgKeyPair::default(),
decoding: vec![AlgKeyPair::default()],
validation: ValidationConfig::default(),
}
}
pub fn build(self) -> Result<JwtConfig, MultiCliError> {
let mut errors = MultiCliError::new();
let decoding_key = self
.decoding
.iter()
.filter_map(|pair| match pair.build_as_decode_key() {
Ok(alg_key_pair) => Some(alg_key_pair),
Err(e) => {
errors.add(e);
None
}
})
.collect();
let encoding_key = match self.encoding.build_as_encode_key() {
Ok(alg_key_pair) => alg_key_pair.1,
Err(e) => {
errors.add(e);
return Err(errors);
}
};
if !errors.is_empty() {
return Err(errors);
}
let res = JwtConfig {
encoding_key,
decoding_key,
header: Header::new(self.encoding.algorithm),
validation: self.validation.into(),
};
if !res.decoding_key.contains_key(&self.encoding.algorithm) {
tracing::warn!(
"no decoding key provided for encoding algorithm {:?}; tokens signed by this server might not be verifiable",
self.encoding.algorithm
);
}
Ok(res)
}
}
impl AlgKeyPair {
fn get_key(&self) -> Result<Vec<u8>, CliError> {
let res = match self.form {
KeyForm::DerInline => BASE64_STANDARD.decode(self.key.clone()).map_err(|e| {
CliError::from(e).add_source(format!(
"while decoding the given jwt secrete key `{}` into binary form",
self.key
.get(0..4)
.map(|val| format!("{val}..."))
.unwrap_or(self.key.clone())
))
})?,
KeyForm::DerFile => std::fs::read(&self.key).map_err(|e| {
CliError::from(e).add_source(format!("while reading the der key from {}", self.key))
})?,
KeyForm::PemInline => self.key.clone().into_bytes(),
KeyForm::PemFile => std::fs::read(&self.key).map_err(|e| {
CliError::from(e).add_source(format!("while reading the pem key from {}", self.key))
})?,
};
if res.len() < 32 {
tracing::warn!(
"the secret key `{}` is too short to prevent brute force cracking",
self.key
.get(0..4)
.map(|val| format!("{val}..."))
.unwrap_or(self.key.clone())
)
}
Ok(res)
}
fn build_as_encode_key(&self) -> Result<(Algorithm, EncodingKey), CliError> {
if self.form.is_der() {
let build_from_der = match self.algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => EncodingKey::from_secret,
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => EncodingKey::from_rsa_der,
Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => EncodingKey::from_rsa_der,
Algorithm::ES256 | Algorithm::ES384 => EncodingKey::from_ec_der,
Algorithm::EdDSA => EncodingKey::from_ed_der,
};
Ok((
self.algorithm,
build_from_der(&self.get_key().map_err(|e| {
e.add_source("while building jwt encoding key from a der form".into())
})?),
))
} else if self.form.is_pem() {
let build_from_pem = match self.algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
return Err(CliError::new(
ErrorKind::Io,
format!(
"cannot use a .pem file `{}` to store a hmac secret key, you should use a .der file or a inline base64 encoded string",
self.key
),
Some("while building jwt encoding key from a der form".into()),
));
}
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => EncodingKey::from_rsa_pem,
Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => EncodingKey::from_rsa_pem,
Algorithm::ES256 | Algorithm::ES384 => EncodingKey::from_ec_pem,
Algorithm::EdDSA => EncodingKey::from_ed_pem,
};
Ok((
self.algorithm,
build_from_pem(&self.get_key().map_err(|e| {
e.add_source("while building jwt encoding key from a pem form".into())
})?)
.map_err(|e| {
CliError::new(
ErrorKind::Io,
e.to_string(),
Some("while building jwt encoding key from a pem form".into()),
)
})?,
))
} else {
unreachable!(
"Sylvan, 你加了新的变体但是没有添加相应的条件判断,去检查你的 is_der 和 is_pem 方法是否包含了所有的情况"
)
}
}
fn build_as_decode_key(&self) -> Result<(Algorithm, DecodingKey), CliError> {
if self.form.is_der() {
let build_from_der = match self.algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => DecodingKey::from_secret,
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => DecodingKey::from_rsa_der,
Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => DecodingKey::from_rsa_der,
Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_der,
Algorithm::EdDSA => DecodingKey::from_ed_der,
};
Ok((
self.algorithm,
build_from_der(&self.get_key().map_err(|e| {
e.add_source("while building jwt encoding key from a der form".into())
})?),
))
} else if self.form.is_pem() {
let build_from_pem = match self.algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
return Err(CliError::new(
ErrorKind::Io,
format!(
"cannot use a .pem file `{}` to store a hmac secret key, you should use a .der file or a inline base64 encoded string",
self.key
),
Some("while building jwt decoding key from a pem form".into()),
));
}
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => DecodingKey::from_rsa_pem,
Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => DecodingKey::from_rsa_pem,
Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem,
Algorithm::EdDSA => DecodingKey::from_ed_pem,
};
Ok((
self.algorithm,
build_from_pem(&self.get_key()?).map_err(|e| {
CliError::new(
ErrorKind::Io,
e.to_string(),
Some("while building jwt decoding key from a pem form".into()),
)
})?,
))
} else {
unreachable!(
"Sylvan, 你加了新的变体但是没有添加相应的条件判断,去检查你的 is_der 和 is_pem 方法是否包含了所有的情况"
)
}
}
}
impl KeyForm {
fn is_der(&self) -> bool {
matches!(self, KeyForm::DerInline | KeyForm::DerFile)
}
fn is_pem(&self) -> bool {
matches!(self, KeyForm::PemInline | KeyForm::PemFile)
}
}
impl ValidationConfig {
pub fn new(alg: Algorithm) -> Self {
let required_claims = vec!["exp".into()];
Self {
required_spec_claims: required_claims,
leeway: 0,
reject_tokens_expiring_in_less_than: 0,
validate_exp: true,
validate_nbf: false,
iss: None,
aud: None,
sub: None,
decode_algorithms: vec![alg],
}
}
}
impl Default for ValidationConfig {
fn default() -> Self {
Self::new(Algorithm::default())
}
}
impl From<ValidationConfig> for Validation {
fn from(value: ValidationConfig) -> Self {
let ValidationConfig {
required_spec_claims,
leeway,
reject_tokens_expiring_in_less_than,
validate_exp,
validate_nbf,
aud,
iss,
sub,
decode_algorithms: algorithms,
} = value;
let mut validation = Validation::default();
validation.required_spec_claims = required_spec_claims.into_iter().collect();
validation.leeway = leeway;
validation.reject_tokens_expiring_in_less_than = reject_tokens_expiring_in_less_than;
validation.validate_exp = validate_exp;
validation.validate_nbf = validate_nbf;
validation.validate_aud = matches!(&aud, Some(val) if !val.is_empty());
validation.aud = aud.map(|val| val.into_iter().collect());
validation.iss = iss.map(|val| val.into_iter().collect());
validation.sub = sub;
validation.algorithms = algorithms;
validation
}
}