use crate::credentials::internal::jwk_client::JwkClient;
use jsonwebtoken::Validation;
pub use serde_json::Map;
pub use serde_json::Value;
use std::time::Duration;
pub struct Builder {
audiences: Vec<String>,
email: Option<String>,
jwks_url: Option<String>,
clock_skew: Option<Duration>,
}
impl Builder {
pub fn new<I, S>(audiences: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let audiences = audiences
.into_iter()
.map(|s| s.into())
.collect::<Vec<String>>();
Self {
audiences,
email: None,
jwks_url: None,
clock_skew: None,
}
}
pub fn with_email<S: Into<String>>(mut self, email: S) -> Self {
self.email = Some(email.into());
self
}
pub fn with_jwks_url<S: Into<String>>(mut self, jwks_url: S) -> Self {
self.jwks_url = Some(jwks_url.into());
self
}
pub fn with_clock_skew(mut self, clock_skew: Duration) -> Self {
self.clock_skew = Some(clock_skew);
self
}
pub fn build(self) -> Verifier {
Verifier {
jwk_client: JwkClient::new(),
audiences: self.audiences.clone(),
email: self.email.clone(),
jwks_url: self.jwks_url.clone(),
clock_skew: self.clock_skew.unwrap_or_else(|| Duration::from_secs(10)),
}
}
}
#[derive(Debug)]
pub struct Verifier {
jwk_client: JwkClient,
audiences: Vec<String>,
email: Option<String>,
jwks_url: Option<String>,
clock_skew: Duration,
}
impl Verifier {
pub async fn verify(&self, token: &str) -> std::result::Result<Map<String, Value>, Error> {
let header = jsonwebtoken::decode_header(token).map_err(Error::decode)?;
let key_id = header
.kid
.ok_or_else(|| Error::invalid_field("kid", "kid header is missing"))?;
let mut validation = Validation::new(header.alg);
validation.leeway = self.clock_skew.as_secs();
validation.set_issuer(&["https://accounts.google.com", "accounts.google.com"]);
validation.set_audience(&self.audiences);
let expected_email = self.email.clone();
let jwks_url = self.jwks_url.clone();
let cert = self
.jwk_client
.get_or_load_cert(key_id, header.alg, jwks_url)
.await
.map_err(Error::load_cert)?;
let token = jsonwebtoken::decode::<Map<String, Value>>(&token, &cert, &validation)
.map_err(|e| match e.clone().into_kind() {
jsonwebtoken::errors::ErrorKind::InvalidIssuer => Error::invalid_field("iss", e),
jsonwebtoken::errors::ErrorKind::InvalidAudience => Error::invalid_field("aud", e),
jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(field) => {
Error::invalid_field(field.as_str(), e)
}
_ => Error::invalid(e),
})?;
let claims = token.claims;
if let Some(email) = expected_email {
let email_verified = claims["email_verified"]
.as_bool()
.ok_or(Error::invalid_field(
"email_verified",
"email_verified claim is missing",
))?;
if !email_verified {
return Err(Error::invalid_field(
"email_verified",
"email_verified claim value is `false`",
));
}
let token_email = claims["email"]
.as_str()
.ok_or_else(|| Error::invalid_field("email", "email claim is missing"))?;
if !email.eq(token_email) {
let err_msg = format!("expected `{email}`, but found `{token_email}`");
return Err(Error::invalid_field("email", err_msg));
}
}
Ok(claims)
}
}
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error(ErrorKind);
impl Error {
pub fn is_decode(&self) -> bool {
matches!(self.0, ErrorKind::Decode(_))
}
pub fn is_invalid(&self) -> bool {
matches!(self.0, ErrorKind::Invalid(_)) || matches!(self.0, ErrorKind::InvalidField(_, _))
}
pub fn is_load_cert(&self) -> bool {
matches!(self.0, ErrorKind::LoadingCertificate(_))
}
fn decode<T>(source: T) -> Error
where
T: Into<BoxError>,
{
Error(ErrorKind::Decode(source.into()))
}
fn load_cert<T>(source: T) -> Error
where
T: Into<BoxError>,
{
Error(ErrorKind::LoadingCertificate(source.into()))
}
fn invalid<T>(source: T) -> Error
where
T: Into<BoxError>,
{
Error(ErrorKind::Invalid(source.into()))
}
fn invalid_field<S: Into<String>, T>(field: S, source: T) -> Error
where
T: Into<BoxError>,
{
Error(ErrorKind::InvalidField(field.into(), source.into()))
}
}
#[derive(thiserror::Error, Debug)]
enum ErrorKind {
#[error("cannot decode JWT token: {0}")]
Decode(#[source] BoxError),
#[error("JWT token is invalid: {0}")]
Invalid(#[source] BoxError),
#[error("JWT token `{0}` field is invalid: {1}")]
InvalidField(String, #[source] BoxError),
#[error("Failed to fetch certificate: {0}")]
LoadingCertificate(#[source] BoxError),
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::credentials::idtoken::tests::{
generate_test_id_token, generate_test_id_token_es256, generate_test_id_token_with_claims,
};
use crate::credentials::internal::jwk_client::tests::{
create_es256_jwk_set_response, create_rsa256_jwk_set_response,
};
use httptest::matchers::{all_of, request};
use httptest::responders::{json_encoded, status_code};
use httptest::{Expectation, Server};
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use rsa::pkcs1::EncodeRsaPrivateKey;
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
type TestResult = anyhow::Result<()>;
#[tokio::test]
async fn test_verify_success() -> TestResult {
let server = Server::run();
let response = create_rsa256_jwk_set_response();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(response.clone())),
);
let audience = "https://example.com";
let token = generate_test_id_token(audience);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
let claims = verifier.verify(token).await?;
assert!(!claims.is_empty(), "{token:?}");
let claims = verifier.verify(token).await?;
assert!(!claims.is_empty(), "{token:?}");
Ok(())
}
#[tokio::test]
async fn test_verify_invalid_audience() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let token = generate_test_id_token(audience);
let token = token.as_str();
let verifier = Builder::new(["https://wrong-audience.com"])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
let result = verifier.verify(token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_invalid());
Ok(())
}
#[tokio::test]
async fn test_verify_multiple_audience_success() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audiences = ["https://example.com", "https://another_example.com"];
let verifier = Builder::new(audiences)
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
for audience in audiences {
let token = generate_test_id_token(audience);
let token = token.as_str();
let claims = verifier.verify(token).await?;
assert!(!claims.is_empty(), "{token:?}");
}
Ok(())
}
#[tokio::test]
async fn test_verify_invalid_issuer() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let mut claims = HashMap::new();
claims.insert("iss", "https://wrong-issuer.com".into());
let token = generate_test_id_token_with_claims(audience, claims);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
let result = verifier.verify(token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_invalid());
Ok(())
}
#[tokio::test]
async fn test_verify_email_success() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let email = "test@example.com";
let mut claims = HashMap::new();
claims.insert("email", email.into());
claims.insert("email_verified", true.into());
let token = generate_test_id_token_with_claims(audience, claims);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.with_email(email)
.build();
let claims = verifier.verify(token).await?;
assert_eq!(claims["email"].as_str().unwrap(), email);
Ok(())
}
#[tokio::test]
async fn test_verify_email_mismatch() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let email = "test@example.com";
let mut claims = HashMap::new();
claims.insert("email", email.into());
claims.insert("email_verified", true.into());
let token = generate_test_id_token_with_claims(audience, claims);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.with_email("wrong@example.com")
.build();
let result = verifier.verify(token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_invalid());
Ok(())
}
#[tokio::test]
async fn test_verify_expired_token() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let mut claims = HashMap::new();
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
claims.insert("exp", (now.as_secs() - 3600).into()); let token = generate_test_id_token_with_claims(audience, claims);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
let result = verifier.verify(token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_invalid());
Ok(())
}
#[tokio::test]
async fn test_verify_email_not_verified() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let email = "test@example.com";
let mut claims = HashMap::new();
claims.insert("email", email.into());
claims.insert("email_verified", false.into());
let token = generate_test_id_token_with_claims(audience, claims);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.with_email(email)
.build();
let result = verifier.verify(token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_invalid());
Ok(())
}
#[tokio::test]
async fn test_verify_clock_skew() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(create_rsa256_jwk_set_response())),
);
let audience = "https://example.com";
let mut claims = HashMap::new();
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
claims.insert("exp", (now.as_secs() - 5).into()); let token = generate_test_id_token_with_claims(audience, claims);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.with_clock_skew(Duration::from_secs(60))
.build();
let _claims = verifier.verify(token).await?;
Ok(())
}
#[tokio::test]
async fn test_verify_decode_error() -> TestResult {
let audience = "https://example.com";
let verifier = Builder::new([audience]).build();
let invalid_token = "invalid.token.format";
let result = verifier.verify(invalid_token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_decode());
Ok(())
}
#[tokio::test]
async fn test_verify_missing_kid() -> TestResult {
let header = Header::new(Algorithm::RS256);
let claims: HashMap<&str, Value> = HashMap::new();
let private_cert = crate::credentials::tests::RSA_PRIVATE_KEY
.to_pkcs1_der()
.expect("Failed to encode private key to PKCS#1 DER");
let private_key = EncodingKey::from_rsa_der(private_cert.as_bytes());
let token =
jsonwebtoken::encode(&header, &claims, &private_key).expect("failed to encode jwt");
let verifier = Builder::new(["https://example.com"]).build();
let result = verifier.verify(&token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_invalid());
Ok(())
}
#[tokio::test]
async fn test_verify_load_cert_error() -> TestResult {
let server = Server::run();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(status_code(404)),
);
let audience = "https://example.com";
let token = generate_test_id_token(audience);
let token = token.as_str();
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
let result = verifier.verify(token).await;
assert!(result.is_err(), "{result:?}");
assert!(result.unwrap_err().is_load_cert());
Ok(())
}
#[tokio::test]
async fn test_verify_es256_success() -> TestResult {
let server = Server::run();
let response = create_es256_jwk_set_response();
server.expect(
Expectation::matching(all_of![request::path("/certs"),])
.times(1)
.respond_with(json_encoded(response.clone())),
);
let audience = "https://example.com";
let token = generate_test_id_token_es256(audience);
let verifier = Builder::new([audience])
.with_jwks_url(format!("http://{}/certs", server.addr()))
.build();
let claims = verifier.verify(&token).await?;
assert!(!claims.is_empty(), "{token:?}");
Ok(())
}
}