use zeroize::Zeroizing;
use crate::error::{Error, Result};
use crate::header::Header;
use crate::json::{Object, Value};
use crate::jwa::SigningAlg;
use crate::jwk::Jwk;
use crate::jws::{ProtectedHeader, SignOptions, sign_compact};
use crate::util::b64url_encode;
use super::Claims;
use super::typ::{AT_JWT, JWT};
const FORBIDDEN_PARAMS: [&str; 5] = ["b64", "crit", "cty", "zip", "enc"];
const OWNED_PARAMS: [&str; 3] = ["alg", "typ", "kid"];
const SECRET_JWK_MEMBERS: [&str; 8] = ["k", "d", "p", "q", "dp", "dq", "qi", "oth"];
fn check_public_jwk(v: &Value) -> Result<()> {
let refused = || Error::InvalidHeader("jwk");
let object = v.as_object().ok_or_else(refused)?;
if SECRET_JWK_MEMBERS.iter().any(|n| object.contains(n)) {
return Err(refused());
}
match Jwk::from_object(object) {
Ok(key) if key.is_public() => Ok(()),
_ => Err(refused()),
}
}
#[derive(Debug, Clone)]
pub struct IssueOptions {
alg: SigningAlg,
typ: Option<String>,
params: Object,
}
impl IssueOptions {
pub fn new(alg: SigningAlg) -> Self {
Self {
alg,
typ: Some(JWT.to_owned()),
params: Object::new(),
}
}
pub fn typ(mut self, t: &str) -> Self {
self.typ = Some(t.to_owned());
self
}
pub fn no_typ(mut self) -> Self {
self.typ = None;
self
}
pub fn access_token(self) -> Self {
self.typ(AT_JWT)
}
pub fn header_param(mut self, name: &str, v: impl Into<Value>) -> Result<Self> {
if let Some(n) = FORBIDDEN_PARAMS.iter().find(|n| **n == name) {
return Err(Error::InvalidHeader(n));
}
if let Some(n) = OWNED_PARAMS.iter().find(|n| **n == name) {
return Err(Error::InvalidHeader(n));
}
let v = v.into();
if name == "jwk" {
check_public_jwk(&v)?;
}
let mut probe = Object::new();
probe.insert(name, v.clone());
Header::from_object(probe)?;
self.params.insert(name, v);
Ok(self)
}
fn protected_header(&self, kid: Option<&str>) -> Result<ProtectedHeader> {
let mut header = Object::new();
header.insert("alg", self.alg.as_str());
if let Some(typ) = &self.typ {
header.insert("typ", typ.as_str());
}
if let Some(kid) = kid {
header.insert("kid", kid);
}
for (name, value) in &self.params {
header.insert(name, value.clone());
}
let json = Zeroizing::new(header.to_json());
ProtectedHeader::from_encoded(&b64url_encode(json.as_bytes()))
}
}
fn check_issuable(alg: &SigningAlg) -> Result<()> {
use SigningAlg as A;
match alg {
A::HS256
| A::HS384
| A::HS512
| A::RS256
| A::RS384
| A::RS512
| A::PS256
| A::PS384
| A::PS512
| A::ES256
| A::ES384
| A::ES512
| A::ES256K
| A::EdDSA
| A::Ed25519 => Ok(()),
A::None => Err(Error::AlgorithmNotAllowed),
_ => Err(Error::UnsupportedAlgorithm),
}
}
pub fn sign(claims: &Claims, opts: &IssueOptions, key: &Jwk) -> Result<String> {
check_issuable(&opts.alg)?;
let protected = opts.protected_header(key.kid())?;
let payload = Zeroizing::new(claims.to_json());
sign_compact(&protected, payload.as_bytes(), key, &SignOptions::new())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jws::{Verified, verify_compact};
use crate::jwt::typ::{Layer, check_jwt_header};
use crate::jwt::validate::validate_claims;
use crate::jwt::{Audience, FixedClock, NumericDate, Policy};
const P256: &str = r#"{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI"}"#;
const ISSUER: &str = "https://issuer.example";
const AUDIENCE: &str = "https://api.example";
const ES256: [SigningAlg; 1] = [SigningAlg::ES256];
fn key() -> Jwk {
Jwk::from_json(P256.as_bytes()).expect("RFC 7515 A.3.1 key")
}
fn access_token_claims() -> Claims {
let mut c = Claims::new();
c.iss = Some(ISSUER.into());
c.exp = NumericDate::from_secs(1_300_819_380);
c.aud = Some(Audience::One(AUDIENCE.into()));
c.sub = Some("5ba552d67".into());
c.insert("client_id", "s6BhdRkqt3").expect("client_id");
c.iat = NumericDate::from_secs(1_300_818_000);
c.jti = Some("dbe39bf3a3ba4238a513f51d6e1691c4".into());
c
}
fn verify(token: &str, policy: &Policy<'_>) -> Verified {
let public = key().to_public().expect("public half");
verify_compact(token, &public, &policy.verify_options()).expect("JWS verifies")
}
fn check_header(v: &Verified, policy: &Policy<'_>) -> Result<()> {
let protected = v
.protected
.as_ref()
.expect("compact JWS has a protected header");
check_jwt_header(protected, v.unprotected.as_ref(), policy, Layer::Signed)
}
#[test]
fn access_token_passes_the_rfc9068_header_and_claim_checks() {
let clock = FixedClock(1_300_819_000);
let policy = Policy::access_token(&clock, &ES256, &[ISSUER], &[AUDIENCE]).expect("policy");
let opts = IssueOptions::new(SigningAlg::ES256).access_token();
let token = sign(&access_token_claims(), &opts, &key()).expect("issued");
let verified = verify(&token, &policy);
assert_eq!(check_header(&verified, &policy), Ok(()));
let claims = Claims::from_json(&verified.payload).expect("claims parse");
assert_eq!(claims, access_token_claims());
assert_eq!(validate_claims(&claims, &policy), Ok(()));
}
#[test]
fn the_same_claims_with_the_default_typ_fail_the_access_token_header_check() {
let clock = FixedClock(1_300_819_000);
let policy = Policy::access_token(&clock, &ES256, &[ISSUER], &[AUDIENCE]).expect("policy");
let token = sign(
&access_token_claims(),
&IssueOptions::new(SigningAlg::ES256),
&key(),
)
.expect("issued");
let err = check_header(&verify(&token, &policy), &policy).expect_err("typ JWT");
assert!(matches!(err, Error::InvalidHeader(_)), "{err:?}");
let untyped = sign(
&access_token_claims(),
&IssueOptions::new(SigningAlg::ES256).no_typ(),
&key(),
)
.expect("issued");
let err = check_header(&verify(&untyped, &policy), &policy).expect_err("no typ");
assert!(matches!(err, Error::InvalidHeader(_)), "{err:?}");
}
#[test]
fn an_access_token_fails_the_default_header_check() {
let clock = FixedClock(1_300_819_000);
let policy = Policy::new(&clock, &ES256).expect("policy");
let opts = IssueOptions::new(SigningAlg::ES256).access_token();
let token = sign(&access_token_claims(), &opts, &key()).expect("issued");
let err = check_header(&verify(&token, &policy), &policy).expect_err("typ at+jwt");
assert!(matches!(err, Error::InvalidHeader(_)), "{err:?}");
}
#[test]
fn issuable_algorithms_are_an_explicit_list() {
for alg in SigningAlg::KNOWN {
let expected = match alg {
SigningAlg::None => Err(Error::AlgorithmNotAllowed),
_ => Ok(()),
};
assert_eq!(check_issuable(alg), expected, "{alg:?}");
}
assert_eq!(
check_issuable(&SigningAlg::from("ES999")),
Err(Error::UnsupportedAlgorithm)
);
}
#[test]
fn protected_header_order_is_alg_typ_kid_then_extras() {
let opts = IssueOptions::new(SigningAlg::HS256)
.header_param("x5t#S256", "abc")
.expect("x5t#S256 is a string");
let protected = opts.protected_header(Some("k1")).expect("header");
let json = crate::util::b64url_decode(protected.encoded()).expect("base64url");
assert_eq!(
json,
br#"{"alg":"HS256","typ":"JWT","kid":"k1","x5t#S256":"abc"}"#
);
assert_eq!(protected.header().kid.as_deref(), Some("k1"));
assert_eq!(protected.header().x5t_s256.as_deref(), Some("abc"));
}
}