// Copyright (C) 2019-2022 The TVL Community
//
// alcoholic_jwt is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! Implements a library for for **validation** of **RS256** JWTs
//! using keys from a JWKS. Nothing more, nothing less.
//!
//! The name of the library stems from the potential side-effects of
//! trying to use the other Rust libraries that are made for similar
//! purposes.
//!
//! This library is specifically aimed at developers that consume
//! tokens from services which provide their RSA public keys in
//! [JWKS][] format.
//!
//! ## Usage example (token with `kid`-claim)
//!
//! ```rust
//! # extern crate serde_json;
//! extern crate alcoholic_jwt;
//!
//! use alcoholic_jwt::{JWKS, Validation, validate, token_kid};
//!
//! # fn some_token_fetching_function() -> &'static str {
//! # "eyJraWQiOiI4ckRxOFB3MEZaY2FvWFdURVZRbzcrVGYyWXpTTDFmQnhOS1BDZWJhYWk0PSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoLnRlc3QuYXByaWxhLm5vIiwiaWF0IjoxNTM2MDUwNjkzLCJleHAiOjE1MzYwNTQyOTMsInN1YiI6IjQyIiwiZXh0Ijoic21va2V0ZXN0IiwicHJ2IjoiYXJpc3RpIiwic2NwIjoicHJvY2VzcyJ9.gOLsv98109qLkmRK6Dn7WWRHLW7o8W78WZcWvFZoxPLzVO0qvRXXRLYc9h5chpfvcWreLZ4f1cOdvxv31_qnCRSQQPOeQ7r7hj_sPEDzhKjk-q2aoNHaGGJg1vabI--9EFkFsGQfoS7UbMMssS44dgR68XEnKtjn0Vys-Vzbvz_CBSCH6yQhRLik2SU2jR2L7BoFvh4LGZ6EKoQWzm8Z-CHXLGLUs4Hp5aPhF46dGzgAzwlPFW4t9G4DciX1uB4vv1XnfTc5wqJch6ltjKMde1GZwLR757a8dJSBcmGWze3UNE2YH_VLD7NCwH2kkqr3gh8rn7lWKG4AUIYPxsw9CB"
//! # }
//!
//! # fn jwks_fetching_function() -> JWKS {
//! # let jwks_json = "{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"kid\":\"8rDq8Pw0FZcaoXWTEVQo7+Tf2YzSL1fBxNKPCebaai4=\",\"n\":\"l4UTgk1zr-8C8utt0E57DtBV6qqAPWzVRrIuQS2j0_hp2CviaNl5XzGRDnB8gwk0Hx95YOhJupAe6RNq5ok3fDdxL7DLvppJNRLz3Ag9CsmDLcbXgNEQys33fBJaPw1v3GcaFC4tisU5p-o1f5RfWwvwdBtdBfGiwT1GRvbc5sFx6M4iYjg9uv1lNKW60PqSJW4iDYrfqzZmB0zF1SJ0BL_rnQZ1Wi_UkFmNe9arM8W9tI9T3Ie59HITFuyVSTCt6qQEtSfa1e5PiBaVuV3qoFI2jPBiVZQ6LPGBWEDyz4QtrHLdECPPoTF30NN6TSVwwlRbCuUUrdNdXdjYe2dMFQ\",\"e\":\"DhaD5zC7mzaDvHO192wKT_9sfsVmdy8w8T8C9VG17_b1jG2srd3cmc6Ycw-0blDf53Wrpi9-KGZXKHX6_uIuJK249WhkP7N1SHrTJxO0sUJ8AhK482PLF09Qtu6cUfJqY1X1y1S2vACJZItU4Vjr3YAfiVGQXeA8frAf7Sm4O1CBStCyg6yCcIbGojII0jfh2vSB-GD9ok1F69Nmk-R-bClyqMCV_Oq-5a0gqClVS8pDyGYMgKTww2RHgZaFSUcG13KeLMQsG2UOB2OjSC8FkOXK00NBlAjU3d0Vv-IamaLIszO7FQBY3Oh0uxNOvIE9ofQyCOpB-xIK6V9CTTphxw\"}]}";
//! # serde_json::from_str(jwks_json).unwrap()
//! # }
//! #
//! // The function implied here would usually perform an HTTP-GET
//! // on the JWKS-URL for an authentication provider and deserialize
//! // the result into the `alcoholic_jwt::JWKS`-struct.
//! let jwks: JWKS = jwks_fetching_function();
//!
//! let token = some_token_fetching_function();
//!
//! // Several types of built-in validations are provided:
//! let validations = vec![
//! Validation::Issuer("auth.test.aprila.no".into()),
//! Validation::SubjectPresent,
//! ];
//!
//! // If a JWKS contains multiple keys, the correct KID first
//! // needs to be fetched from the token headers.
//! let kid = token_kid(&token)
//! .expect("Failed to decode token headers")
//! .expect("No 'kid' claim present in token");
//!
//! let jwk = jwks.find(&kid).expect("Specified key not found in set");
//!
//! validate(token, jwk, validations).expect("Token validation has failed!");
//! ```
//!
//! [JWKS]: https://tools.ietf.org/html/rfc7517
#[macro_use]
extern crate serde_derive;
extern crate base64;
extern crate openssl;
extern crate serde;
extern crate serde_json;
use base64::{Config, DecodeError, URL_SAFE_NO_PAD};
use openssl::bn::BigNum;
use openssl::error::ErrorStack;
use openssl::hash::MessageDigest;
use openssl::pkey::{PKey, Public};
use openssl::rsa::Rsa;
use openssl::sign::Verifier;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::error::Error;
use std::fmt::{self, Display};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(test)]
mod tests;
/// URL-safe character set without padding that allows trailing bits,
/// which appear in some JWT implementations.
///
/// Note: The functions on `base64::Config` are not marked `const`,
/// and the constructors are not exported, which is why this is
/// implemented as a function.
fn jwt_forgiving() -> Config {
URL_SAFE_NO_PAD.decode_allow_trailing_bits(true)
}
/// JWT algorithm used. The only supported algorithm is currently
/// RS256.
#[derive(Clone, Deserialize, Debug)]
enum KeyAlgorithm {
RS256,
}
/// Type of key contained in a JWT. The only supported key type is
/// currently RSA.
#[derive(Clone, Deserialize, Debug)]
enum KeyType {
RSA,
}
/// Representation of a single JSON Web Key. See [RFC
/// 7517](https://tools.ietf.org/html/rfc7517#section-4).
#[allow(dead_code)] // kty & alg only constrain deserialisation, but aren't used
#[derive(Clone, Debug, Deserialize)]
pub struct JWK {
kty: KeyType,
alg: Option,
kid: Option,
// Shared modulus
n: String,
// Public key exponent
e: String,
}
/// Representation of a set of JSON Web Keys. See [RFC
/// 7517](https://tools.ietf.org/html/rfc7517#section-5).
#[derive(Clone, Debug, Deserialize)]
pub struct JWKS {
// This is a vector instead of some kind of map-like structure
// because key IDs are in fact optional.
//
// Technically having multiple keys with the same KID would not
// violate the JWKS-definition either, but behaviour in that case
// is unspecified.
keys: Vec,
}
impl JWKS {
/// Attempt to find a JWK by its key ID.
pub fn find(&self, kid: &str) -> Option<&JWK> {
self.keys.iter().find(|jwk| jwk.kid == Some(kid.into()))
}
}
/// Representation of an undecoded JSON Web Token. See [RFC
/// 7519](https://tools.ietf.org/html/rfc7519).
struct JWT<'a>(&'a str);
/// Representation of a decoded and validated JSON Web Token.
///
/// Specific claim fields are only decoded internally in the library
/// for validation purposes, while it is generally up to the consumer
/// of the validated JWT what structure they would like to impose.
pub struct ValidJWT {
/// JOSE header of the JSON Web Token. Certain fields are
/// guaranteed to be present in this header, consult section 5 of
/// RFC7519 for more information.
pub headers: Value,
/// Claims (i.e. primary data) contained in the JSON Web Token.
/// While there are several registered and recommended headers
/// (consult section 4.1 of RFC7519), the presence of no field is
/// guaranteed in these.
pub claims: Value,
}
/// Possible token claim validations. This enumeration only covers
/// common use-cases, for other types of validations the user is
/// encouraged to inspect the claim set manually.
pub enum Validation {
/// Validate that the issuer ("iss") claim matches a specified
/// value.
Issuer(String),
/// Validate that the audience ("aud") claim matches a specified
/// value.
Audience(String),
/// Validate that a subject value is present.
SubjectPresent,
/// Validate that the expiry time of the token ("exp"-claim) has
/// not yet been reached.
NotExpired,
}
/// Possible results of a token validation.
#[derive(Debug)]
pub enum ValidationError {
/// Invalid number of token components (not a JWT?)
InvalidComponents,
/// Token segments had invalid base64-encoding.
InvalidBase64(DecodeError),
/// Decoding of the provided JWK failed.
InvalidJWK,
/// Signature validation failed, i.e. because of a non-matching
/// public key.
InvalidSignature,
/// An OpenSSL operation failed along the way at a point at which
/// a more specific error variant could not be constructed.
OpenSSL(ErrorStack),
/// JSON decoding into a provided type failed.
JSON(serde_json::Error),
/// One or more claim validations failed. This variant contains
/// human-readable validation errors.
InvalidClaims(Vec<&'static str>),
}
impl Error for ValidationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ValidationError::InvalidBase64(e) => Some(e),
ValidationError::OpenSSL(e) => Some(e),
ValidationError::JSON(e) => Some(e),
ValidationError::InvalidComponents
| ValidationError::InvalidJWK
| ValidationError::InvalidSignature
| ValidationError::InvalidClaims(_) => None,
}
}
}
impl Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::InvalidComponents => {
f.write_str("Invalid number of token components in JWT")
}
ValidationError::InvalidBase64(_) => f.write_str("Invalid Base64 encoding in JWT"),
ValidationError::InvalidJWK => f.write_str("JWK decoding failed"),
ValidationError::InvalidSignature => f.write_str("JWT signature validation failed"),
ValidationError::OpenSSL(e) => write!(f, "SSL error: {}", e),
ValidationError::JSON(e) => write!(f, "JSON error: {}", e),
ValidationError::InvalidClaims(errs) => {
write!(f, "Invalid claims: {}", errs.join(", "))
}
}
}
}
type JWTResult = Result;
impl From for ValidationError {
fn from(err: ErrorStack) -> Self {
ValidationError::OpenSSL(err)
}
}
impl From for ValidationError {
fn from(err: serde_json::Error) -> Self {
ValidationError::JSON(err)
}
}
impl From for ValidationError {
fn from(err: DecodeError) -> Self {
ValidationError::InvalidBase64(err)
}
}
/// Attempt to extract the `kid`-claim out of a JWT's header claims.
///
/// This function is normally used when a token provider has multiple
/// public keys in rotation at the same time that could all still have
/// valid tokens issued under them.
///
/// This is only safe if the key set containing the currently allowed
/// key IDs is fetched from a trusted source.
pub fn token_kid(token: &str) -> JWTResult