bbjwt 0.4.2

A simple to use, well documented JWT validation library, mainly for validating OpenID Connect ID Tokens.
Documentation
///
/// Integration tests for bbjwt.
///
/// Uses `env_logger` to dump information. To see it, you could start tests like this:
///
/// ```sh
/// RUST_LOG=INFO cargo test -- --nocapture
/// ```
///
/// (c) Copyright 2022 by basebox GmbH. All rights reserved.

/* --- uses ------------------------------------------------------------------------------------- */

#[macro_use]
pub mod bb_common;

use std::fs::File;
use std::io::Read;

use bb_common::*;
use bbjwt::keystore::{EcCurve, KeyAlgorithm};
use bbjwt::{KeyStore, default_validations, validate_jwt};

const ISS: &str = "https://kc.basebox.health/realms/testing";

///
/// Load an asset from a file by its asset name (relative to test/assets/)
fn load_asset(name: &str) -> String {
  let pfn = path_to_asset_file(name);
  let mut file = File::open(&pfn).unwrap_or_else(|_| panic!("Failed to open asset {}", &pfn));
  let mut data = String::new();
  file.read_to_string(&mut data).unwrap();
  data
}

///
/// Validate a JWT with a correct and a wrong key. Naturally, the latter must fail.
async fn validate_jwt_with_keystores(
  jwt_name: &str,
  keystore_good: &KeyStore,
  keystore_bad: &KeyStore,
) {
  let jwt = load_asset(jwt_name);

  let jwt_decoded = validate_jwt(&jwt, &default_validations(ISS, None, None), keystore_good)
    .await
    .expect("Valid JWT did not validate");

  /* check some claims */
  assert_eq!(jwt_decoded.claims["nonce"].as_str().unwrap(), "UZ1BSZFvy7jKkj1o9p3r7w");
  assert_eq!(jwt_decoded.claims["sub"].as_str().unwrap(), "13529346-91b6-4268-aae1-f5ad8f44cf4d");
  assert_eq!(
    jwt_decoded.claims["iss"].as_str().unwrap(),
    "https://kc.basebox.health/realms/testing"
  );

  /* wrong keystore: must fail */
  let ret = validate_jwt(&jwt, &default_validations(ISS, None, None), keystore_bad).await;
  let err_msg = ret.unwrap_err().to_string();
  assert!(
    err_msg.contains("Invalid signature"),
    "Invalid key check did not fail with signature error."
  );
}

///
/// Validate unsupported algorithm; must panic.
#[tokio::test]
#[should_panic(expected = "Unsupported alg")]
async fn unsupported_alg_jwt() {
  let ks = KeyStore::new().await.unwrap();
  ks.add_rsa_pem_key(&load_asset("rsa.pub.key"), Some("key-1"), KeyAlgorithm::RS256)
    .await
    .expect("Failed to add RSA key");
  assert_eq!(ks.keys_len().await, 1);

  /* verify valid token */
  let jwt = load_asset("id_token_unsupported_alg.txt");
  validate_jwt(&jwt, &default_validations(ISS, None, None), &ks).await.unwrap();
}

///
/// Validate valid RSA256 JWT.
#[tokio::test]
async fn rsa256_valid_jwt() {
  let ks_good = KeyStore::new().await.unwrap();
  ks_good
    .add_rsa_pem_key(&load_asset("rsa.pub.key"), Some("key-1"), KeyAlgorithm::RS256)
    .await
    .expect("Failed to add RSA384 key");

  let ks_bad = KeyStore::new().await.unwrap();
  ks_bad
    .add_rsa_pem_key(&load_asset("rsa.wrong.pub.key"), Some("key-1"), KeyAlgorithm::RS256)
    .await
    .expect("Failed to add RSA384 key");

  validate_jwt_with_keystores("id_token_rsa256.txt", &ks_good, &ks_bad).await;
}

///
/// Validate expired RSA256 JWT with wrong issuer; all errors must be reported.
#[tokio::test]
async fn rsa256_invalid_claims_expired_jwt() {
  let ks = KeyStore::new().await.unwrap();
  ks.add_rsa_pem_key(&load_asset("rsa.pub.key"), Some("key-1"), KeyAlgorithm::RS256)
    .await
    .expect("Failed to add RSA key");
  assert_eq!(ks.keys_len().await, 1);

  /* verify expired token, must fail */
  let jwt = load_asset("id_token_rsa256_expired.txt");
  let ret = validate_jwt(
    &jwt,
    &default_validations("https://kc.basebox.health/realms/WRONG", None, None),
    &ks,
  )
  .await;

  if let Err(err) = ret {
    /* all errors must be in the error string */
    let msg = err.to_string();
    assert!(msg.contains("iss"), "iss error not reported");
    assert!(msg.contains("expired"), "expiration not reported");
  } else {
    /* no error */
    panic!("Invalid JWT validated ok!");
  }
}

///
/// Validate expired RSA256 JWT.
#[tokio::test]
#[should_panic(expected = "expired")]
async fn rsa256_valid_claims_expired_jwt() {
  let ks = KeyStore::new().await.unwrap();
  ks.add_rsa_pem_key(&load_asset("rsa.pub.key"), Some("key-1"), KeyAlgorithm::RS256)
    .await
    .expect("Failed to add RSA key");
  assert_eq!(ks.keys_len().await, 1);

  /* verify expired token, must fail */
  let jwt = load_asset("id_token_rsa256_expired.txt");
  validate_jwt(&jwt, &default_validations(ISS, None, None), &ks).await.unwrap();
}

///
/// Validate valid RSA384 JWT.
#[tokio::test]
async fn rsa384_valid_jwt() {
  let ks_good = KeyStore::new().await.unwrap();
  ks_good
    .add_rsa_pem_key(&load_asset("rsa.pub.key"), Some("key-1"), KeyAlgorithm::RS384)
    .await
    .expect("Failed to add RSA384 key");

  let ks_bad = KeyStore::new().await.unwrap();
  ks_bad
    .add_rsa_pem_key(&load_asset("rsa.wrong.pub.key"), Some("key-1"), KeyAlgorithm::RS384)
    .await
    .expect("Failed to add RSA384 key");

  validate_jwt_with_keystores("id_token_rsa384.txt", &ks_good, &ks_bad).await;
}

///
/// Validate valid RSA512 JWT.
#[tokio::test]
async fn rsa512_valid_jwt() {
  let ks = KeyStore::new().await.unwrap();
  ks.add_rsa_pem_key(&load_asset("rsa.pub.key"), Some("key-1"), KeyAlgorithm::RS512)
    .await
    .expect("Failed to add RSA key");
  assert_eq!(ks.keys_len().await, 1);

  /* verify valid token */
  let jwt = load_asset("id_token_rsa512.txt");
  validate_jwt(&jwt, &default_validations(ISS, None, None), &ks)
    .await
    .expect("Valid JWT did not validate");
}

///
/// Validate valid ES256 JWT.
#[tokio::test]
async fn es256_valid_jwt() {
  let ks_good = KeyStore::new().await.unwrap();
  ks_good
    .add_ec_pem_key(&load_asset("ec256.pub.key"), Some("key-1"), EcCurve::P256, KeyAlgorithm::ES256)
    .await
    .expect("Failed to add ec256 key");

  let ks_bad = KeyStore::new().await.unwrap();
  ks_bad
    .add_ec_pem_key(
      &load_asset("ec256.wrong.pub.key"),
      Some("key-1"),
      EcCurve::P256,
      KeyAlgorithm::ES256,
    )
    .await
    .expect("Failed to add EC256 key");

  validate_jwt_with_keystores("id_token_es256.txt", &ks_good, &ks_bad).await;
}

///
/// Validate expired ES256 JWT; must panic
#[tokio::test]
#[should_panic(expected = "expired")]
async fn es256_expired_jwt() {
  let ks = KeyStore::new().await.unwrap();
  ks.add_ec_pem_key(
    &load_asset("ec256.pub.key"),
    Some("key-1"),
    EcCurve::P256,
    KeyAlgorithm::ES256,
  )
  .await
  .expect("Failed to add EC key");
  assert_eq!(ks.keys_len().await, 1);

  /* verify valid token */
  let jwt = load_asset("id_token_es256_expired.txt");
  validate_jwt(&jwt, &default_validations(ISS, None, None), &ks).await.unwrap();
}

///
/// Validate expired ES256 JWT; must panic
#[tokio::test]
#[should_panic(expected = "SignatureInvalid")]
async fn es256_signature_invalid_jwt() {
  let ks = KeyStore::new().await.unwrap();
  ks.add_ec_pem_key(
    &load_asset("ec256.pub.key"),
    Some("key-1"),
    EcCurve::P256,
    KeyAlgorithm::ES256,
  )
  .await
  .expect("Failed to add EC key");
  assert_eq!(ks.keys_len().await, 1);

  /* verify valid token */
  let jwt = load_asset("id_token_es256_signature_invalid.txt");
  validate_jwt(&jwt, &default_validations(ISS, None, None), &ks).await.unwrap();
}

///
/// Validate ES384
#[tokio::test]
async fn es384_valid_jwt() {
  let ks_good = KeyStore::new().await.unwrap();
  ks_good
    .add_ec_pem_key(&load_asset("ec384.pub.key"), Some("key-1"), EcCurve::P384, KeyAlgorithm::ES384)
    .await
    .expect("Failed to add ec384 key");

  let ks_bad = KeyStore::new().await.unwrap();
  ks_bad
    .add_ec_pem_key(
      &load_asset("ec384.wrong.pub.key"),
      Some("key-1"),
      EcCurve::P384,
      KeyAlgorithm::ES384,
    )
    .await
    .expect("Failed to add EC384 key");

  validate_jwt_with_keystores("id_token_es384.txt", &ks_good, &ks_bad).await;
}

///
/// Validate Ed25519
#[tokio::test]
async fn ed25519_valid_jwt() {
  let ks_good = KeyStore::new().await.unwrap();
  ks_good
    .add_ec_pem_key(
      &load_asset("ed25519.pub.key"),
      Some("key-1"),
      EcCurve::Ed25519,
      KeyAlgorithm::EdDSA,
    )
    .await
    .expect("Failed to add Ed25519 key");

  let ks_bad = KeyStore::new().await.unwrap();
  ks_bad
    .add_ec_pem_key(
      &load_asset("ed25519.wrong.pub.key"),
      Some("key-1"),
      EcCurve::Ed25519,
      KeyAlgorithm::EdDSA,
    )
    .await
    .expect("Failed to add Ed25519 key");

  validate_jwt_with_keystores("id_token_ed25519.txt", &ks_good, &ks_bad).await;
}

///
/// Test loading OKP/Ed25519 key from JWK JSON.
#[tokio::test]
async fn load_ed25519_jwk() {
  let mut ks = KeyStore::new().await.unwrap();
  let jwk_json = load_asset("ed25519.pub.jwk.json");
  ks.add_key(&jwk_json).await.unwrap();

  assert_eq!(ks.keys_len().await, 1);

  /* get the key by name */
  ks.key_by_id(Some("key-1")).await.unwrap();

  /* get key with wrong name, must fail */
  assert!(ks.key_by_id(Some("No-key-with-that-name")).await.is_err());
}

///
/// Test loading elliptic curve key from JWK JSON.
#[tokio::test]
async fn load_ec_jwk() {
  let mut ks = KeyStore::new().await.unwrap();
  let jwk_json = load_asset("ec256.pub.jwk.json");
  ks.add_key(&jwk_json).await.unwrap();

  assert_eq!(ks.keys_len().await, 1);

  /* get the key by name */
  ks.key_by_id(Some("ec2561")).await.unwrap();

  /* get key with wrong name, must fail */
  assert!(ks.key_by_id(Some("No-key-with-that-name")).await.is_err());
}