pub struct JwtValidator<R, V = DefaultVerifier> { /* private fields */ }Expand description
Validates a compact JWT that is neither a credential nor a presentation: an OAuth 2.0 access token (RFC 9068), an ID token, a request object.
The checks follow RFC 7519 §7.2 and the best current practices of RFC 8725, in this order:
- The token is a compact JWS (
CompactJws::decode), whosealgis one the options allow (RFC 8725 §3.1) and whose headertypis the one the options expect, if they expect one (RFC 8725 §3.11).typis compared as RFC 7515 §4.1.9 says: ignoring case and anapplication/prefix. - The payload is a JSON claims set.
- The key the header names (
KeyReference::from_jws, with theissof the claims) is resolved for theKeyRoleof the options and verifies the signature. - Under
IssuerBinding::KeyOfIssuer, the default, the key belongs toiss(KeyReference::binds_to):issis the DID of akidDID URL, the HTTPS issuer of a key set, or thedid:jwkor thumbprint of a key by value. issis the expected issuer, andaudincludes the expected audience, when the options name them.exp,nbfandiathold at the validation time, within the leeway.- Every claim the options require is present.
What else a token must say (a scope, a client_id, a nonce) is the application’s to
check, on the claims it gets back. Explicit typing is what keeps one kind of JWT from being
accepted as another (RFC 8725 §3.11); name the typ whenever the token has one.
§Examples
An authorization server signs an access token with the Ed25519 key of RFC 8037 Appendix
A.1, naming it by a DID URL while its iss is its HTTPS URL. The resource server pins that
one key: its resolver knows no other, so the key need not belong to iss. A resolver that
returns any key a token carries, such as JwkResolver, must never be
combined with IssuerBinding::Any: anyone could sign with their own key.
use identity_kit::jose::{DefaultVerifier, Jwk, JwsAlgorithm, JwsHeader, JwtClaims, SignError, Signer, encode_compact};
use identity_kit::resolver::{
IssuerBinding, JwtValidationError, JwtValidationOptions, JwtValidator, KeyReference,
KeyResolutionError, KeyResolver, KeyRole,
};
struct Server(ed25519_dalek::SigningKey);
#[async_trait::async_trait]
impl Signer for Server {
fn alg(&self) -> JwsAlgorithm { JwsAlgorithm::EdDSA }
fn kid(&self) -> Option<&str> { None }
async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, SignError> {
use ed25519_dalek::Signer as _;
Ok(self.0.sign(input).to_bytes().to_vec())
}
}
/// The authorization server's one key, under the one DID URL it signs with.
struct Pinned(Jwk);
#[async_trait::async_trait]
impl KeyResolver for Pinned {
async fn resolve_key(&self, key: &KeyReference, _: KeyRole) -> Result<Jwk, KeyResolutionError> {
match key {
KeyReference::DidUrl(url) if url.as_str() == "did:example:as#key-1" => Ok(self.0.clone()),
other => Err(KeyResolutionError::unsupported(other)),
}
}
}
let secret = [
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4,
0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
];
let server = Server(ed25519_dalek::SigningKey::from_bytes(&secret));
let key = Jwk::from_ed25519_public(server.0.verifying_key().as_bytes())?;
let header = JwsHeader::new(JwsAlgorithm::EdDSA)
.with_typ("at+jwt")
.with_kid("did:example:as#key-1");
let mut claims = JwtClaims::new()
.with_iss("https://as.example.com")
.with_sub("5ba552d67")
.with_aud("https://rs.example.com")
.with_jti("dbe39bf3a3ba4238a513f51d6e1691c4")
.with_claim("client_id", "s6BhdRkqt3")
.with_claim("scope", "openid profile");
claims.iat = Some(1_700_000_000);
claims.exp = Some(1_700_003_600);
let token = encode_compact(&header, &serde_json::to_vec(&claims)?, &server).await?;
// The key is the pinned one, not the DID of `iss`, so the binding is waived.
let validator = JwtValidator::new(Pinned(key.clone()), DefaultVerifier);
let options = JwtValidationOptions::new()
.with_typ("at+jwt")
.with_algorithm(JwsAlgorithm::EdDSA)
.with_issuer("https://as.example.com")
.with_audience("https://rs.example.com")
.with_issuer_binding(IssuerBinding::Any)
.with_required_claims(["exp", "iat", "jti", "sub", "client_id"])
.at(1_700_000_100);
let validated = validator.validate(&token, &options).await?;
assert_eq!(validated.claims.sub.as_deref(), Some("5ba552d67"));
assert_eq!(validated.key, key);
// A token naming any other key is refused.
let forged = encode_compact(
&JwsHeader::new(JwsAlgorithm::EdDSA).with_typ("at+jwt").with_kid("did:example:attacker#key-1"),
&serde_json::to_vec(&claims)?,
&server,
)
.await?;
let error = validator.validate(&forged, &options).await.unwrap_err();
assert!(matches!(error, JwtValidationError::KeyResolution { .. }));
// An hour and a minute later it has expired.
let error = validator.validate(&token, &options.at(1_700_003_660)).await.unwrap_err();
assert!(matches!(error, JwtValidationError::Expired { expired_at: 1_700_003_600, .. }));Implementations§
Source§impl<R: KeyResolver, V: JwsVerifier> JwtValidator<R, V>
impl<R: KeyResolver, V: JwsVerifier> JwtValidator<R, V>
Sourcepub fn new(resolver: R, verifier: V) -> Self
pub fn new(resolver: R, verifier: V) -> Self
A validator resolving keys with resolver and verifying signatures with verifier.
Sourcepub async fn validate(
&self,
token: &str,
options: &JwtValidationOptions,
) -> Result<ValidatedJwt, JwtValidationError>
pub async fn validate( &self, token: &str, options: &JwtValidationOptions, ) -> Result<ValidatedJwt, JwtValidationError>
Validates token, returning its claims as a JwtClaims set.
§Errors
The first JwtValidationError found, in the order JwtValidator lists the checks.
Sourcepub async fn validate_as<T: DeserializeOwned>(
&self,
token: &str,
options: &JwtValidationOptions,
) -> Result<ValidatedJwt<T>, JwtValidationError>
pub async fn validate_as<T: DeserializeOwned>( &self, token: &str, options: &JwtValidationOptions, ) -> Result<ValidatedJwt<T>, JwtValidationError>
Validates token, returning its claims read into T, once the registered claims have
been checked.
§Errors
As validate, and JwtValidationError::Claims when the payload
does not fit T.