#![cfg(feature = "validator")]
extern crate asap;
extern crate asap_deps_keyserver as keyserver;
extern crate base64;
extern crate chrono;
#[cfg(not(any(feature = "fips", feature = "generator-fips")))]
extern crate jsonwebtoken as jwt;
#[cfg(any(feature = "fips", feature = "generator-fips"))]
extern crate jsonwebtoken_aws_lc as jwt;
extern crate reqwest;
extern crate serde;
#[macro_use]
extern crate serde_json;
use crate::jwt::TokenData;
use crate::keyserver::Keyserver;
use base64::Engine;
use chrono::Utc;
use std::collections::HashMap;
use std::env;
use std::thread;
use std::time::Duration;
use asap::claims::{Aud, Claims, ExtraClaims, DEFAULT_TOKEN_LIFESPAN, REQUIRED_CLAIMS};
use asap::generator::Generator;
use asap::validator::{Validator, ValidatorBuilder};
const PRIVATE_KEY_01: &[u8] = include_bytes!("../support/keys/service01/1530402390-private.der");
const ISS_01: &str = "service01";
const ISS_02: &str = "service02";
const KID_01: &str = "service01/1530402390-public.der";
const KID_02: &str = "service02/1530402393-public.der";
fn get_validator_builder(keyserver_url: &str) -> ValidatorBuilder {
let resource_server_audience = String::from(ISS_01);
Validator::builder(String::from(keyserver_url), resource_server_audience)
}
fn mock_claims() -> Claims {
Claims {
aud: Aud::One(ISS_01.to_string()),
iss: ISS_01.to_string(),
jti: "some-random-jti".to_string(),
iat: Utc::now().timestamp(),
exp: Utc::now().timestamp() + 60,
extra_claims: None,
}
}
fn mock_extra_claims() -> Option<ExtraClaims> {
let mut extra_claims = HashMap::new();
extra_claims.insert("object".to_string(), json!({ "one": ["two", "three"] }));
extra_claims.insert("number".to_string(), json!(123));
extra_claims.insert("string".to_string(), json!("one two three"));
extra_claims.insert("array".to_string(), json!(["one", "two", "three"]));
extra_claims.insert("bool".to_string(), json!(true));
extra_claims.insert("null".to_string(), json!(null));
Some(extra_claims)
}
fn default_aud() -> Aud {
Aud::One(ISS_01.to_string())
}
fn default_generator() -> Generator {
Generator::new(
ISS_01.to_string(),
KID_01.to_string(),
PRIVATE_KEY_01.to_vec(),
)
}
fn validate_claims(
token_data: TokenData<Claims>,
expected_aud: Aud,
extra_claims: Option<ExtraClaims>,
) {
let now = Utc::now().timestamp();
assert_eq!(token_data.claims.aud, expected_aud);
assert_eq!(&token_data.claims.iss, ISS_01);
assert_eq!(token_data.claims.jti.len(), 20);
assert_eq!(
token_data.claims.exp - token_data.claims.iat,
DEFAULT_TOKEN_LIFESPAN
);
assert!(token_data.claims.iat > now - 2 && token_data.claims.iat < now + 2);
assert_eq!(token_data.claims.extra_claims, extra_claims);
}
fn setup_env(keyserver: &Keyserver) {
env::set_var("ASAP_SERVER_AUDIENCE", ISS_01);
env::set_var("ASAP_KEYSERVER_URL", keyserver.url());
env::set_var("ASAP_FALLBACK_KEYSERVER_URL", keyserver.url());
env::set_var("ASAP_KEY_ID", KID_01);
env::set_var("ASAP_ISSUER", ISS_01);
env::set_var(
"ASAP_PRIVATE_KEY",
include_str!("../support/keys/service01/1530402390-private.pem"),
);
}
fn teardown_env() {
env::remove_var("ASAP_SERVER_AUDIENCE");
env::remove_var("ASAP_KEYSERVER_URL");
env::remove_var("ASAP_FALLBACK_KEYSERVER_URL");
env::remove_var("ASAP_KEY_ID");
env::remove_var("ASAP_ISSUER");
env::remove_var("ASAP_PRIVATE_KEY");
}
#[tokio::test]
async fn keyserver_works() {
let keyserver = Keyserver::start();
assert_eq!(keyserver.count().await, "0");
for _ in 0..100 {
reqwest::get(&format!("{}{}", keyserver.url(), KID_01))
.await
.unwrap();
}
assert_eq!(keyserver.count().await, "100");
}
#[tokio::test]
async fn it_works() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let token = generator.token(default_aud(), None).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
validate_claims(token_data, default_aud(), None);
let token = generator.token(default_aud(), mock_extra_claims()).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
validate_claims(token_data, default_aud(), mock_extra_claims());
}
#[tokio::test]
async fn instantiates_from_environment() {
let keyserver = Keyserver::start();
{
setup_env(&keyserver);
let generator = Generator::from_env().unwrap();
let validator = Validator::from_env().unwrap().build();
let token = generator.token(default_aud(), mock_extra_claims()).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
validate_claims(token_data, default_aud(), mock_extra_claims());
teardown_env()
}
{
setup_env(&keyserver);
env::set_var(
"ASAP_KEYSERVER_URL",
"http://not-a-real-server/".to_string(),
);
let generator = Generator::from_env().unwrap();
let validator = Validator::from_env().unwrap().build();
let token = generator.token(default_aud(), mock_extra_claims()).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
validate_claims(token_data, default_aud(), mock_extra_claims());
teardown_env()
}
}
#[tokio::test]
async fn validates_nbf_is_after_current_time() {
let keyserver = Keyserver::start();
let now = Utc::now().timestamp();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
{
let mut extra_claims = HashMap::new();
extra_claims.insert("nbf".to_string(), json!(now + 30));
let token = generator.token(default_aud(), Some(extra_claims)).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert!(format!("{}", e).starts_with("Immature jwt signature")),
}
}
{
let mut extra_claims = HashMap::new();
extra_claims.insert("nbf".to_string(), json!(now - 30));
let token = generator.token(default_aud(), Some(extra_claims)).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
}
}
#[tokio::test]
async fn validates_exp_is_before_current_time() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
generator.set_max_lifespan(1);
let token = generator.token(default_aud(), None).unwrap();
thread::sleep(Duration::from_secs(2));
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert!(format!("{}", e).starts_with("Expired jwt signature")),
}
}
#[tokio::test]
async fn validates_if_max_lifespan_is_exceeded() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
generator.set_max_lifespan(3601);
let token = generator.token(default_aud(), None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"Token contained a lifespan greater than the \
`max_lifespan` (hard limit of 3600 seconds)"
),
}
}
#[tokio::test]
async fn validates_if_custom_max_lifespan_is_exceeded() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url())
.max_lifespan(60)
.build();
generator.set_max_lifespan(30);
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
generator.set_max_lifespan(120);
let token = generator.token(default_aud(), None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"Token contained a lifespan greater than the \
`max_lifespan` (hard limit of 3600 seconds)"
),
}
}
#[tokio::test]
async fn validates_if_encounters_unrecognized_audience() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
let aud = Aud::One("not-whitelisted".to_string());
let token = generator.token(aud, None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"Resource server audience not found in `aud` claims of \
token [\"not-whitelisted\"]"
),
}
}
#[tokio::test]
async fn works_with_aud_as_vec() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let aud = Aud::Many(vec!["foo".to_string(), ISS_01.to_string()]);
let token = generator.token(aud, None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
let aud = Aud::Many(vec!["foo".to_string(), "bar".to_string()]);
let token = generator.token(aud, None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"Resource server audience not found in `aud` claims of \
token [\"foo\", \"bar\"]"
),
}
}
#[tokio::test]
async fn works_with_aud_as_string() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
let token = generator.token(default_aud(), None).unwrap();
match validator.decode(&token, &["foobar"]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"Unknown or unauthorized subject \"service01\". The `sub` \
claim (or `iss`) must exist in `whitelisted_issuers` [\"foobar\"]"
),
}
}
#[tokio::test]
async fn iss_is_assumed_if_sub_is_undefined() {
let keyserver = Keyserver::start();
let mut extra_claims = HashMap::new();
extra_claims.insert("sub".to_string(), json!(ISS_02));
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let token_with_sub = generator.token(default_aud(), Some(extra_claims)).unwrap();
let token_no_sub = generator.token(default_aud(), None).unwrap();
let whitelisted_issuers = vec![ISS_02];
let _ = validator
.decode(&token_with_sub, &whitelisted_issuers)
.await
.unwrap();
match validator.decode(&token_no_sub, &whitelisted_issuers).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"Unknown or unauthorized subject \"service01\". \
The `sub` claim (or `iss`) must exist in `whitelisted_issuers` [\"service02\"]"
),
}
}
#[tokio::test]
async fn validates_kid_is_owned_by_isser() {
let keyserver = Keyserver::start();
let validator = get_validator_builder(keyserver.url()).build();
let generator = default_generator();
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
let generator = Generator::new(
ISS_02.to_string(),
KID_01.to_string(),
PRIVATE_KEY_01.to_vec(),
);
let token = generator.token(default_aud(), None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
format!("{}", e),
"JWT header did not contain a valid `kid` claim. \
As per ASAP spec, the `kid` claim must start with \"$iss/\" where $iss is the issuer \
(kid: \"service01/1530402390-public.der\", iss: \"service02\")"
),
}
}
#[tokio::test]
async fn it_rejects_duplicate_jti_claims() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url())
.validate_jti(true)
.build();
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert!(format!("{}", e).starts_with("Duplicate `jti` encountered: ")),
}
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
}
#[tokio::test]
async fn it_fails_with_wrong_public_key() {
let keyserver = Keyserver::start();
let generator = Generator::new(
ISS_01.to_string(),
KID_02.to_string(),
PRIVATE_KEY_01.to_vec(),
);
let validator = get_validator_builder(keyserver.url()).build();
let token = generator.token(default_aud(), None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(format!("{}", e), "InvalidSignature"),
}
}
#[tokio::test]
async fn it_fails_with_no_public_key() {
let keyserver = Keyserver::start();
let generator = Generator::new(
ISS_01.to_string(),
"not-a-kid".to_string(),
PRIVATE_KEY_01.to_vec(),
);
let validator = get_validator_builder(keyserver.url()).build();
let token = generator.token(default_aud(), None).unwrap();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(format!("{}", e), "Failed to retrieve public key from keyserver: \"Failed to fetch a key from any keyserver\"")
}
}
#[tokio::test]
async fn it_uses_the_fallback_keyserver() {
let keyserver = Keyserver::start();
let generator = default_generator();
let invalid_keyserver = "http://not-a-real-server:1234/".to_string();
let valid_keyserver = keyserver.url().to_string();
let validator = Validator::builder(invalid_keyserver, ISS_01.to_string())
.fallback_keyserver(valid_keyserver)
.build();
let token = generator.token(default_aud(), None).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
validate_claims(token_data, default_aud(), None);
}
#[tokio::test]
async fn it_fetches_key_from_cache() {
let server = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(server.url()).build();
let token = generator.token(default_aud(), None).unwrap();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
assert_eq!(server.count().await, "1");
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
assert_eq!(server.count().await, "1");
}
#[tokio::test]
async fn it_does_not_fetch_expired_key_from_cache() {
let server = Keyserver::start();
let generator = default_generator();
let token = generator.token(default_aud(), None).unwrap();
let validator = get_validator_builder(server.url())
.cache_duration(Duration::from_nanos(0))
.build();
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
assert_eq!(server.count().await, "1");
let _ = validator.decode(&token, &[ISS_01]).await.unwrap();
assert_eq!(server.count().await, "2");
}
#[tokio::test]
async fn it_allows_extra_claims() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let mut extra_claims = HashMap::new();
extra_claims.insert("foo".to_string(), json!("foo"));
extra_claims.insert("bar".to_string(), json!(1234));
extra_claims.insert("baz".to_string(), json!(["baz", "bop"]));
let token = generator.token(default_aud(), Some(extra_claims)).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
let decoded_extra_claims = token_data.claims.extra_claims.unwrap();
assert_eq!(decoded_extra_claims.get("foo").unwrap(), &json!("foo"));
assert_eq!(decoded_extra_claims.get("bar").unwrap(), &json!(1234));
assert_eq!(
decoded_extra_claims.get("baz").unwrap(),
&json!(["baz", "bop"])
);
}
#[tokio::test]
async fn it_does_not_override_required_claims() {
let keyserver = Keyserver::start();
let generator = default_generator();
let validator = get_validator_builder(keyserver.url()).build();
let mut extra_claims = HashMap::new();
for key in &REQUIRED_CLAIMS {
extra_claims.insert(key.to_string(), json!(key));
}
let token = generator.token(default_aud(), Some(extra_claims)).unwrap();
let token_data = validator.decode(&token, &[ISS_01]).await.unwrap();
assert!(token_data.claims.aud != Aud::One("aud".to_string()));
assert!(token_data.claims.iss != "iss");
assert!(token_data.claims.jti != "jti");
let now = Utc::now().timestamp();
assert!(token_data.claims.iat > now - 2 && token_data.claims.iat < now + 2);
assert_eq!(
token_data.claims.exp - token_data.claims.iat,
DEFAULT_TOKEN_LIFESPAN
);
}
#[test]
fn it_caches_tokens_when_enabled() {
let generator = default_generator();
generator.enable_token_caching(10, ::std::time::Duration::from_millis(1000));
let token_1 = generator.token(default_aud(), None).unwrap();
let token_2 = generator.token(default_aud(), None).unwrap();
assert_eq!(token_1, token_2);
}
#[test]
fn it_caches_tokens_with_extra_claims_when_enabled() {
let generator = default_generator();
generator.enable_token_caching(10, ::std::time::Duration::from_millis(1000));
let mut extra_claims = HashMap::new();
extra_claims.insert("foo".to_string(), json!("foo"));
extra_claims.insert("bar".to_string(), json!(1234));
extra_claims.insert("baz".to_string(), json!(["baz", "bop"]));
let token_1 = generator
.token(default_aud(), Some(extra_claims.clone()))
.unwrap();
let token_2 = generator
.token(default_aud(), Some(extra_claims.clone()))
.unwrap();
assert_eq!(token_1, token_2);
}
#[test]
fn it_does_not_return_expired_cached_tokens() {
let cache_duration = ::std::time::Duration::from_millis(100);
let generator = default_generator();
generator.enable_token_caching(10, cache_duration);
let token_1 = generator.token(default_aud(), None).unwrap();
::std::thread::sleep(cache_duration);
let token_2 = generator.token(default_aud(), None).unwrap();
assert!(token_1 != token_2);
}
#[test]
fn it_regenerates_cached_tokens_when_they_are_different() {
let generator = default_generator();
generator.enable_token_caching(10, ::std::time::Duration::from_millis(1000));
let token_1 = generator.token(Aud::One(ISS_01.to_string()), None).unwrap();
let token_2 = generator.token(Aud::One(ISS_02.to_string()), None).unwrap();
assert!(token_1 != token_2);
let mut extra_claims_1 = HashMap::new();
extra_claims_1.insert("foo".to_string(), json!("foo"));
let mut extra_claims_2 = HashMap::new();
extra_claims_2.insert("bar".to_string(), json!("bar"));
let token_1 = generator
.token(default_aud(), Some(extra_claims_1))
.unwrap();
let token_2 = generator
.token(default_aud(), Some(extra_claims_2))
.unwrap();
assert!(token_1 != token_2);
}
#[tokio::test]
async fn it_rejects_unsigned_tokens() {
let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
&serde_json::to_string(&json!({
"typ": "JWT",
"kid": KID_01.to_string()
}))
.unwrap(),
);
let body = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(&serde_json::to_string(&mock_claims()).unwrap());
let token = header + "." + &body;
let keyserver = Keyserver::start();
let validator = get_validator_builder(keyserver.url()).build();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!("InvalidToken", format!("{}", e)),
}
match validator.decode(&(token + "."), &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!(
"JSON error: missing field `alg` at line 1 column 53",
format!("{}", e)
),
}
}
#[tokio::test]
async fn it_rejects_tokens_with_missing_signatures() {
fn split_two(token: String) -> (String, String) {
let mut i = token.rsplitn(2, '.');
match (i.next(), i.next(), i.next()) {
(Some(a), Some(b), None) => (a.to_string(), b.to_string()),
_ => panic!("Unexpected split: {:?}", i),
}
}
let mut header = jwt::Header::new(jwt::Algorithm::RS256);
header.kid = Some(KID_01.to_string());
let token = jwt::encode(
&header,
&mock_claims(),
&jwt::EncodingKey::from_rsa_der(&PRIVATE_KEY_01.to_vec()),
)
.unwrap();
let (_signature, signing_input) = split_two(token);
let keyserver = Keyserver::start();
let validator = get_validator_builder(keyserver.url()).build();
match validator.decode(&signing_input, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!("InvalidToken", format!("{}", e)),
}
match validator
.decode(&(signing_input.to_string() + "."), &[ISS_01])
.await
{
Ok(_) => panic!("Validation should fail."),
Err(e) => assert_eq!("InvalidSignature", format!("{}", e)),
}
}
#[tokio::test]
async fn it_rejects_tokens_signed_with_unsupported_alg() {
let unsupported_algs = vec![
(
jwt::Algorithm::HS256,
jwt::EncodingKey::from_secret(b"some-secret"),
),
(
jwt::Algorithm::HS384,
jwt::EncodingKey::from_secret(b"some-secret"),
),
(
jwt::Algorithm::HS512,
jwt::EncodingKey::from_secret(b"some-secret"),
),
(
jwt::Algorithm::RS384,
jwt::EncodingKey::from_rsa_der(&PRIVATE_KEY_01.to_vec()),
),
(
jwt::Algorithm::RS512,
jwt::EncodingKey::from_rsa_der(&PRIVATE_KEY_01.to_vec()),
),
];
for (alg, key) in unsupported_algs {
println!("{:?}", alg);
let mut header = jwt::Header::new(alg);
header.kid = Some(KID_01.to_string());
let token = jwt::encode(&header, &mock_claims(), &key).unwrap();
let keyserver = Keyserver::start();
let validator = get_validator_builder(keyserver.url()).build();
match validator.decode(&token, &[ISS_01]).await {
Ok(_) => panic!("Validation should fail."),
Err(e) => {
let err_msg = format!("{}", e);
assert_eq!(err_msg, "InvalidAlgorithm")
}
}
}
}