// [...]
use pdk::jwt::*;
// [...]
async fn filter(
state: RequestState,
) -> Flow<()> {
let headers_state = state.into_headers_state().await;
// Extract token
let token = TokenProvider::bearer(headers_state.handler())?;
// [...]
Using JWT Library Functions
To view an example policy project that uses Flex Gateway Policy Development Kit (PDK)'s JWT library, see JWT Validation Policy Example. |
JSON Web Token (JWT) is a URL-secure method of representing claims to be transferred between two parties. The JWT token contains claims encoded in a JSON object as either the payload of a JSON Web Signature (JWS) or as a JSON web encryption (JWE) structure in plain text, which enables the claims to be digitally signed and protected with a message authentication code (MAC). Because the token is signed, you can trust the information and its source.
PDK’s JWT library provides a set of tools to validate the JWT signatures and to extract and validate the claims of all incoming requests.
Extract a JWT Token
PDK provides a TokenProvider
struct that has the bearer
function to extract JWT tokens that have the format Bearer <token>
in the authorization header. The following code demonstrates how to use the bearer
function:
If you are using a JWT token from a different source or with a different format, you can implement Rust logic to get any other part of the request. Use a DataWeave parameter to configure extraction to create a policy where you can dynamically change the JWT source.
Validate a JWT Signature
The JWT library provides a helper for performing signature validation of the incoming requests authorization tokens. Depending on the algorithm used by the policy, initialize a SignatureValidator
with the corresponding configuration:
pub fn new(algorithm: SigningAlgorithm, key_length: SigningKeyLength, key: String) -> Result<SignatureValidator, JWTError>
PDK supports the following signing algorithms and key lengths:
Signing Algorithm |
Signing Key Length |
RSA |
256, 384 and 512 |
HMAC |
256, 384 and 512 |
ES |
256 and 384 |
The SigningAlgorithm
enum defines the available signing algorithms:
pub enum SigningAlgorithm {
Rsa,
Hmac,
Es,
}
The SigningKeyLength
enum defines the available signing key lengths:
pub enum SigningKeyLength {
Len256,
Len384,
Len512,
}
A SignatureValidator
instance implements the SignatureValidation
trait. This SignatureValidation
trait exposes a validate
function that accepts tokens as String
parameters and returns a Result
that contains the parsed JWTClaims
or a JWTError
in the event of an error:
pub trait SignatureValidation {
fn validate(&self, token: String) -> Result<JWTClaims, JWTError>;
}
The following code example shows how to initialize the SignatureValidator
and validate an incoming token signed with a 256-byte HMAC algorithm:
use anyhow::Result;
use pdk::hl::*;
use pdk::jwt::*;
// [...]
async fn filter(
state: RequestState,
signature_validator: &SignatureValidator,
) -> Flow<()> {
let headers_state = state.into_headers_state().await;
// Extract token
let token = TokenProvider::bearer(headers_state.handler())?;
if token.is_err() {
return Flow::Break(Response::new(401).with_body("Bearer not found"));
}
// Validating signature
let claims = signature_validator.validate(token.unwrap());
if claims.is_err() {
return Flow::Break(Response::new(401).with_body("Invalid token"));
}
// [...]
}
#[entrypoint]
async fn configure(launcher: Launcher, Configuration(configuration): Configuration) -> Result<()> {
let config: Config = serde_json::from_slice(&configuration)?;
let signature_validator = SignatureValidator::new(
model::SigningAlgorithm::Hmac,
model::SigningKeyLength::Len256,
config.secret.clone(),
)?;
launcher
.launch(on_request(|request| {
filter(request, &signature_validator)
}))
.await?;
Ok(())
}
Because, the SignatureValidator
confirms that the policy configurations are correct during initialization, initializing the SignatureValidator
in the #[entrypoint]
function throws an early error when the policy is applied rather than when the API receives a request.
After initialization, the policy passes the SignatureValidator
to the on_request
wrapped function to perform validate
method on incoming requests.
Access Claims and Propagating to Headers
After the signature validation executes, the result contains the parsed JWT claims from the JWT token.
In case the signature is invalid or you choose not to validate the signature, PDK provides the JWTClaimsParser
to parse claims. This structure provides a parse
method that returns Result<JWTClaims, JWTError>
.
// Being "token" a String that contains a JWT token
let parsed_claims = JWTClaimsParser::parse(token);
if claims.is_err() {
return Flow::Break(Response::new(401).with_body("Invalid token"));
}
After you run the signature validation or the parsing method, use the following methods exposed by the JWTClaims
struct to access the JWT claims:
pub fn audience(&self) -> Option<Result<Vec<String>, JWTError>>
pub fn not_before(&self) -> Option<DateTime<Utc>>
pub fn expiration(&self) -> Option<DateTime<Utc>>
pub fn issued_at(&self) -> Option<DateTime<Utc>>
pub fn issuer(&self) -> Option<String>
pub fn jti(&self) -> Option<String>
pub fn nonce(&self) -> Option<String>
pub fn subject(&self) -> Option<String>
pub fn has_claim(&self, name: &str) -> bool
pub fn get_claim<T>(&self, name: &str) -> Option<T> where T: ValueRetrieval,
pub fn has_header(&self, name: &str) -> bool
pub fn get_header(&self, name: &str) -> Option<String>
pub fn get_claims(&self) -> pdk_script::Value
pub fn get_headers(&self) -> pdk_script::Value
The provided methods are designed to return each one of the standard JWT claims.
The get_claim
method can return any standard or custom claim. Because get_claim
supports different target variable types, the user must specify the output type. get_claim
supports String
, f64
, Vec<String>
, chrono::DateTime<chono::Utc>
, and serde_json::Value
output types. Because the claim might not exist in the token, you must wrap the type with an Option
, for example:
let some_custom_claim: Option<String> = claims.get_claim("username");
The following example shows how to parse a JWT token, get a custom claim, and propagate it to the request headers from a wrapped function:
async fn filter(
state: RequestState,
) -> Flow<()> {
let headers_state = state.into_headers_state().await;
// Extract token
let token = TokenProvider::bearer(headers_state.handler())?;
if token.is_err() {
return Flow::Break(Response::new(401).with_body("Bearer not found"));
}
// Being "token" a String that contains a JWT token
let parsed_claims = JWTClaimsParser::parse(token.unwrap());
if claims.is_err() {
return Flow::Break(Response::new(401).with_body("Invalid token"));
}
let claims = claims.unwrap();
let username: Option<String> = claims.get_claim("username");
if let Some(custom_claim) = some_custom_claim {
headers_state
.handler()
.set_header("username", custom_claim.as_str());
}
Flow::Continue(())
}
Validate Claims
JWT tokens typically contain a set of standard and non-standard claims that you can validate. Use the methods from Access Claims and Propagating to Headers to retrieve the claims present in the JWT tokens’s header or payload. Once retrieved, validate the value of the claims.
The following code example validates that the JWT token:
-
Is not expired by validating the
exp
claim. -
Has the correct audience value by validating the
aud
claim. -
Has the correct custom
role
claim by using a custom validator configured as a DataWeave expression policy parameter.
use chrono::Utc;
use pdk::hl::*;
use pdk::jwt::*;
use pdk::logger::info;
use pdk::script::Evaluator;
async fn filter(
state: RequestState,
mut custom_validator: Evaluator<'_>,
) -> Flow<()> {
let headers_state = state.into_headers_state().await;
let token = TokenProvider::bearer(headers_state.handler());
if token.is_err() {
return Flow::Break(Response::new(400).with_body("Bearer not found"));
}
// Being "token" a String that contains a JWT token
let parsed_claims = JWTClaimsParser::parse(token.unwrap());
if claims.is_err() {
return Flow::Break(Response::new(401).with_body("Invalid token"));
}
let claims = claims.unwrap();
// Validating token expiration
if let Some(exp) = claims.expiration() {
if exp < Utc::now() {
return Flow::Break(Response::new(400).with_body("token expired"));
}
} else {
return Flow::Break(Response::new(400).with_body("token missing exp claim"));
}
// Validating audience
if let Some(aud) = claims.audience() {
if !aud_value.iter_mut().any(|a| a == "myAudience") {
return Flow::Break(Response::new(400).with_body("token does not have the expected audience"));
}
}
// Custom claim validation
custom_validator.bind_vars("claimSet", claims.get_claims());
let result = custom_validator_role_claim.eval();
if !result.ok().and_then(|value| value.as_bool()).unwrap_or_default() {
return Flow::Break(Response::new(400).with_body("custom token validations failed."));
}
Flow::Continue(())
}
#[entrypoint]
async fn configure(launcher: Launcher, Configuration(configuration): Configuration) -> Result<()> {
let config: Config = serde_json::from_slice(&configuration)?;
launcher
.launch(on_request(|request| {
filter(request, config.custom_validator_role.evaluator())
}))
.await?;
Ok(())
}
In the above example, the custom role
claim is defined by a DataWeave expression configured as an input parameter in the gcl.yaml
schema definition as follows:
# [...]
spec:
extends:
- name: extension-definition
namespace: default
properties:
customValidatorRole:
type: string
format: dataweave
default: "#[vars.claimSet.role == 'superRole']"
# [...]
required:
- customValidatorRole