JSON web tokens

Decode the popular JWT format to verify user session tokens before forwarding trusted authentication data to your origin.

VCL

Use this solution in your VCL service (click RUN below to test this solution or clone it to make changes):

Compute@Edge

Use this solution in your Compute@Edge service:

  1. Rust
// Backend name configured in service
const BACKEND_NAME: &str = "httpbin.org";
// Block the request or not when time in token is invalid
const CFG_TIME_INVALID_BEHAVIOR: ConstraintNotMetPolicy = ConstraintNotMetPolicy::Block;
// Block the request or not when request path does not meet token path constraint
const CFG_PATH_INVALID_BEHAVIOR: ConstraintNotMetPolicy = ConstraintNotMetPolicy::Anonymous;
// Allow anonymous request or not
const CFG_ANON_ACCESS: AnonymousPolicy = AnonymousPolicy::Allow;
// Choose from 'cookie' or 'query'
const CFG_TOKEN_SOURCE: TokenSource = TokenSource::Cookie;
/// Token selection enum
#[allow(dead_code)]
#[derive(PartialEq)]
enum TokenSource {
/// Use a cookie as the source of the token.
Cookie,
/// Use the query string as the source of the token.
Query,
}
/// Behavior when constraint is not met
#[derive(PartialEq)]
enum ConstraintNotMetPolicy {
/// Do anonymous request.
Anonymous,
/// Block the request.
Block,
}
/// Anonymous policy enum
#[allow(dead_code)]
#[derive(PartialEq)]
enum AnonymousPolicy {
/// Allow a.
Allow,
/// Block an operation.
Deny,
}
/// Example JWT payload.
///
/// This struct contains the data that will be decoded from the JWT contents.
#[derive(Serialize, Deserialize, Debug)]
struct MyJwtPayload {
path: Option<String>,
uid: Option<String>,
nbf: Option<i64>,
exp: Option<i64>,
tag: Option<String>,
groups: Option<String>,
name: Option<String>,
admin: Option<bool>,
}
#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
// Log initial setup
log_fastly::init_simple("my_log", log::LevelFilter::Info);
fastly::log::set_panic_endpoint("my_log")?;
let url = req.get_url().clone();
// Verify JWT token signature
let payload = match verify_jwt_signature(&req) {
Ok(payload) => payload,
Err(e) => return req_with_verification_failed(req, &e.to_string()),
};
// Verify other contraints like path and time
if let Err(e) = verify_time_path_constraint(&payload, url.path()) {
return req_with_verification_failed(req, &e.to_string());
}
log::info!("JWT Token verfied successfully!");
// We passed all the verification
req.set_header("auth-state", "authenticated");
req.set_header("auth-userid", payload.uid.unwrap_or_default());
req.set_header("auth-groups", payload.groups.unwrap_or_default());
req.set_header("auth-name", payload.name.unwrap_or_default());
req.set_header(
"auth-is-admin",
if payload.admin == Some(true) {
"1"
} else {
"0"
},
);
// Before sending request to backend
// Remove jwt token from request, before requesting backend
remove_jwt_token_from_req(&mut req)?;
// Send request to backend
let resp = req.send(BACKEND_NAME)?;
// Check Tag availability in response
if let Some(required_tag) = payload.tag {
if !resp
.get_header("surrogate-key")
.and_then(|v| v.to_str().ok())
.map(|tags| tags.split_ascii_whitespace().any(|tag| tag == required_tag))
.unwrap_or_default()
{
return response_redirect(&url, "jwt:tag-missing");
}
}
Ok(resp)
}
/// Verify request token
fn verify_jwt_signature(req: &Request) -> Result<MyJwtPayload> {
let token = util::get_jwt_token(req)?;
let token_meta = Token::decode_metadata(&token)?;
let key_id = token_meta.key_id().unwrap_or("key1");
let key_str = util::get_key_pem(key_id).ok_or_else(|| anyhow!("jwt:no-key-{}", key_id))?;
log::info!("JWT algorithm {}, kid {}", token_meta.algorithm(), key_id);
// Verify token
// Stick to RS512 algorithm for better security, ignore algorithm info in the token
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
// https://github.com/jedisct1/rust-jwt-simple/blob/master/README.md
let key = RS512PublicKey::from_pem(key_str)?;
let my_payload = key.verify_token::<MyJwtPayload>(&token, None)?.custom;
Ok(my_payload)
}
/// Verify time and path information in the payload
fn verify_time_path_constraint(payload: &MyJwtPayload, req_path: &str) -> Result<()> {
// Check time constraint
if CFG_TIME_INVALID_BEHAVIOR == ConstraintNotMetPolicy::Block {
let now = chrono::Utc::now().timestamp();
if matches!(payload.nbf, Some(nbf) if now < nbf)
|| matches!(payload.exp, Some(exp) if exp < now)
{
return Err(anyhow!("jwt:time-out-of-bounds"));
}
}
// Check path constraint
if CFG_PATH_INVALID_BEHAVIOR == ConstraintNotMetPolicy::Block {
if let Some(jwt_path) = &payload.path {
if !glob::Pattern::new(jwt_path)?.matches(req_path) {
return Err(anyhow!("jwt:path-mismatch"));
}
}
}
Ok(())
}
/// Request handling in error case, make anonymous access or response redirect
fn req_with_verification_failed(mut req: Request, error_msg: &str) -> Result<Response> {
if CFG_ANON_ACCESS == AnonymousPolicy::Allow {
log::info!("Anon backend access, {}", error_msg);
// Send request to backend as anoymous
req.set_header("auth-state", "anonymous");
Ok(req.send(BACKEND_NAME)?)
} else {
log::info!("Response Redirect, {}", error_msg);
// Response to cli
response_redirect(req.get_url(), error_msg)
}
}
/// Remove JWT token from request
fn remove_jwt_token_from_req(req: &mut Request) -> Result<()> {
if CFG_TOKEN_SOURCE == TokenSource::Cookie {
util::remove_jwt_token_from_cookie(req)?;
} else {
util::remove_jwt_token_from_qs(req)?;
}
Ok(())
}
/// Response with redirect
fn response_redirect(url: &Url, error_msg: &str) -> Result<Response> {
let redirect = if let Some(query) = url.query() {
format!("{}?{}", url.path(), query)
} else {
url.path().to_string()
};
let location = format!(
"/login?{}",
serde_urlencoded::to_string(&[("return_to", redirect)])?
);
let resp = Response::from_status(StatusCode::TEMPORARY_REDIRECT)
.with_header(header::LOCATION, location)
.with_header("fastly-jwt-error", error_msg);
Ok(resp)
}
mod util {
/// Get JWT autentication from request
pub fn get_jwt_token(req: &Request) -> Result<String> {
if CFG_TOKEN_SOURCE == TokenSource::Cookie {
get_jwt_token_from_cookie(req).ok_or_else(|| anyhow!("jwt:no-token-in-cookie"))
} else {
get_jwt_token_from_qs(req).ok_or_else(|| anyhow!("jwt:no-token-in-qs"))
}
}
/// Get JWT autentication string from cookie
pub fn get_jwt_token_from_cookie(req: &Request) -> Option<String> {
let cookies: &str = req.get_header(header::COOKIE)?.to_str().ok()?;
let auth = cookies
.split(';')
.filter_map(|kv| Cookie::parse(kv.trim()).ok())
.find(|cookie| cookie.name() == "auth")?;
Some(auth.value().to_string())
}
/// Get JWT autentication string from query string
pub fn get_jwt_token_from_qs(req: &Request) -> Option<String> {
req.get_query::<std::collections::HashMap<String, String>>()
.ok()?
.get("auth")
.cloned()
}
/// Get PEM format key according to key id
pub fn get_key_pem(key_id: &str) -> Option<&'static str> {
match key_id {
"key1" => Some(KEY1),
"key2" => Some(KEY2),
_ => None,
}
}
pub fn remove_jwt_token_from_qs(req: &mut Request) -> Result<()> {
// Find token in the query string and remove the token parameter from query string
let mut qs_vec: Vec<(String, String)> = req.get_query()?;
qs_vec.retain(|x| x.0 != "auth");
// Set query without auth token
req.set_query(&qs_vec)?;
Ok(())
}
pub fn remove_jwt_token_from_cookie(req: &mut Request) -> Result<()> {
let cookie_val = req
.get_header(header::COOKIE)
.ok_or_else(|| anyhow!("jwt:no-cookie"))?;
let cookie_out: String = cookie_val
.to_str()?
.split(';')
.filter(|s| !matches!(Cookie::parse(*s), Ok(cookie) if cookie.name() == "auth"))
.collect::<Vec<_>>()
.join(";");
req.set_header(header::COOKIE, cookie_out);
Ok(())
}
}