Skip to content

Instantly share code, notes, and snippets.

@Panman82
Last active September 16, 2021 15:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Panman82/d2a176319307c3955fc16127cb815692 to your computer and use it in GitHub Desktop.
Save Panman82/d2a176319307c3955fc16127cb815692 to your computer and use it in GitHub Desktop.
ColdFusion component which has functions to work with JSON Web Tokens.
component
displayName = "JSON Web Token"
hint = "Utility library to work with JSON Web Tokens."
{
// Supported algorithms
variables.algorithms = {
"HS256" = "HmacSHA256",
"HS384" = "HmacSHA384",
"HS512" = "HmacSHA512"
};
public jwt function init( string algorithm = "HS512", string secret = "", struct claims = {} )
hint = "Arguments passed in here are purposely kept private in the 'variables' scope."
{
variables.algorithm = arguments.algorithm;
variables.secret = arguments.secret;
variables.claims = arguments.claims;
return this;
} // init()
public string function extract( struct headers = {} )
description = "Extract the JSON Web Token from the Authorization header."
hint = "When 'headers' are not passed in, they will be retrieved from the HTTP request."
{
// See "hint" above
if ( arguments.headers.isEmpty() ) {
arguments.headers = GetHttpRequestData().headers;
}
// Check for the "Authorization" header in the struct of headers
if ( !arguments.headers.keyExists( "Authorization" ) ) {
throw(
type = "jwt",
message = "JWT extract: Authorization header does not exist",
detail = "The JSON Web Token must be in an Authorization header.",
extendedInfo = "headers: #arguments.headers.keyList(', ')#"
);
}
// Header must begin with "Bearer"
if ( arguments.headers.Authorization.listFirst(" ") != "Bearer" ) {
throw(
type = "jwt",
message = "JWT extract: Authorization header missing Bearer",
detail = "The Authorization header should begin with 'Bearer'.",
extendedInfo = "authorization: #arguments.headers.Authorization#"
);
}
// Get the JWT part of the Authorization header
if ( arguments.headers.Authorization.listRest(" ").trim() == "" ) {
throw(
type = "jwt",
message = "JWT extract: Authorization header missing token",
detail = "The token should be after 'Bearer'.",
extendedInfo = "authorization: #arguments.headers.Authorization#"
);
}
// Return the JWT part of the Authorization header
return arguments.headers.Authorization.listRest(" ").trim();
} // extract()
public string function encode( required struct payload, string algorithm = variables.algorithm, string secret = variables.secret )
description = "Encode the payload (claims) into a signed JSON Web Token using an HMAC algorithm."
hint = "The 'algorithm' and 'secret' can be set during object initialization. Ex: 'new jwt(algorithm, secret)'"
{
// Standard JWT header..
local.header = {
"typ" = "JWT",
"alg" = arguments.algorithm
};
// Add "expected claims" from init() to the payload
for ( local.claim in variables.claims ) {
if ( !arguments.payload.keyExists( local.claim ) ) {
arguments.payload[ local.claim ] = variables.claims[ local.claim ];
}
}
// Serialize the structs
local.header = SerializeJSON( local.header );
local.payload = SerializeJSON( arguments.payload );
// Base64 encode segments
local.header = variables.base64Encode( local.header );
local.payload = variables.base64Encode( local.payload );
// URL escape the Base64 segments
local.message = variables.base64Escape( local.header );
local.message &= ".";
local.message &= variables.base64Escape( local.payload );
// Sign the message (first two segments)
try {
local.signature = variables.sign( local.message, arguments.algorithm, arguments.secret );
} catch ( jwt e ) {
rethrow;
}
// Append the signature to the JWT
return local.message & "." & variables.base64Escape( local.signature );
} // encode()
public struct function decode( required string token, string secret = variables.secret )
description = "Decode a JSON Web Token, validating the signature, and returning the payload (claims)."
hint = "The 'secret' can be set during object initialization. Ex: 'new jwt(secret='AbcXyz')'"
{
// JWT must have three separate segments
if ( arguments.token.listLen( "." ) != 3 ) {
throw(
type = "jwt",
message = "JWT decode: Invalid token format",
detail = "The JSON Web Token must have three segments separated by periods.",
extendedInfo = "token: #arguments.token#"
);
}
// Separate each segment
local.header = arguments.token.listGetAt( 1, "." );
local.payload = arguments.token.listGetAt( 2, "." );
local.signature = arguments.token.listGetAt( 3, "." );
// Unescape the Base64Url segments
local.header = variables.base64Unescape( local.header );
local.payload = variables.base64Unescape( local.payload );
local.signature = variables.base64Unescape( local.signature );
// Decode the Base64 segments
local.header = variables.base64Decode( local.header );
local.payload = variables.base64Decode( local.payload );
// Ensure segments are valid json
if ( !IsJSON( local.header ) ) {
throw(
type = "jwt",
message = "JWT decode: Header invalid JSON",
detail = "The JSON Web Token header is not valid JSON.",
extendedInfo = "header: #local.header#"
);
}
if ( !IsJSON( local.payload ) ) {
throw(
type = "jwt",
message = "JWT decode: Payload invalid JSON",
detail = "The JSON Web Token payload is not valid JSON.",
extendedInfo = "payload: #local.payload#"
);
}
// Deserialize the structs
local.header = DeserializeJSON( local.header );
local.payload = DeserializeJSON( local.payload );
// JWT header must contain the algorithm used
if ( !local.header.keyExists( "alg" ) ) {
throw(
type = "jwt",
message = "JWT decode: Header missing alg",
detail = "The JSON Web Token header does not have 'alg' to indicate what algorithm was used.",
extendedInfo = "keys: #local.header.keyList(', ')#"
);
}
// Resign the message with our secret to compare signatures
try {
local.message = arguments.token.listDeleteAt( 3, "." );
local.resigned = variables.sign( local.message, local.header.alg, arguments.secret );
} catch ( jwt e ) {
rethrow;
}
// Compare JWT signature to the expected signature (case sensitive)
if ( Compare( local.signature, local.resigned ) != 0 ) {
throw(
type = "jwt",
message = "JWT decode: Signature validation failed",
detail = "The JSON Web Token signature segment does not match the expected signature.",
extendedInfo = "signature: #local.signature#"
);
}
// Everything passed, payload is valid
return local.payload;
} // decode()
public void function verify( required struct payload, struct claims = variables.claims )
description = "Verifies the 'payload' claims match the expected 'claims'. Additionally verifies the iat, nbf, and exp timestamps are valid."
hint = "The expected 'claims' can be set during object initialization. Ex: 'new jwt(claims={})'"
{
// Check for an "issued at" timestamp
if ( arguments.payload.keyExists("iat") && variables.epochToDate( arguments.payload.iat ) > Now() ) {
throw(
type = "jwt",
message = "JWT verify: Token not issued yet",
detail = "The JSON Web Token 'iat' claim timestamp is in the future.",
extendedInfo = "iat: #variables.epochToDate(arguments.payload.iat).dateTimeFormat('long')# now: #Now().dateTimeFormat('long')#"
);
}
// Check for an "not before" timestamp
if ( arguments.payload.keyExists("nbf") && variables.epochToDate( arguments.payload.nbf ) > Now() ) {
throw(
type = "jwt",
message = "JWT verify: Token not active yet",
detail = "The JSON Web Token 'nbf' claim timestamp is in the future.",
extendedInfo = "nbf: #variables.epochToDate(arguments.payload.nbf).dateTimeFormat('long')# now: #Now().dateTimeFormat('long')#"
);
}
// Check for an "expiration" timestamp
if ( arguments.payload.keyExists("exp") && variables.epochToDate( arguments.payload.exp ) < Now() ) {
throw(
type = "jwt",
message = "JWT verify: Token expired",
detail = "The JSON Web Token 'exp' claim timestamp has passed.",
extendedInfo = "exp: #variables.epochToDate(arguments.payload.exp).dateTimeFormat('long')# now: #Now().dateTimeFormat('long')#"
);
}
// Check the "expected" claims passed in..
for ( local.claim in arguments.claims ) {
if ( !arguments.payload.keyExists( local.claim ) ) {
throw(
type = "jwt",
message = "JWT verify: Missing claim #local.claim#",
detail = "The expected JSON Web Token claim '#local.claim#' does not exist in the payload.",
extendedInfo = "payload claims: #arguments.payload.keyList(', ')#"
);
}
if ( arguments.payload[ local.claim ] != arguments.claims[ local.claim ] ) {
throw(
type = "jwt",
message = "JWT verify: Claim #local.claim# invalid",
detail = "The JSON Web Token claim '#local.claim#' does not have the expected value.",
extendedInfo = "#local.claim#: #arguments.payload[ local.claim ]#"
);
}
}
} // verify()
private string function sign( required string message, required string algorithm, required string secret )
description = "Signs the first two segments of the JSON Web Token and returns the base64 encoded result."
{
// Make sure the requested algorithm is supported
if ( !variables.algorithms.keyExists( arguments.algorithm ) ) {
throw(
type = "jwt",
message = "JWT sign: Invalid algorithm",
detail = "The algorithm requested (#arguments.algorithm#) is not supported.",
extendedInfo = "supported algorithms: #variables.algorithms.keyList(', ')#"
);
}
// Foolish people, the secret cannot be blank
if ( arguments.secret.trim() == "" ) {
throw(
type = "jwt",
message = "JWT sign: Invalid secret",
detail = "The secret cannot be blank."
);
}
// Break down strings into bytes for use with Java libs
local.messageBytes = CharsetDecode( arguments.message, "utf-8" );
local.secretBytes = CharsetDecode( arguments.secret.trim(), "utf-8" );
// Create Java objects for signing
local.algorithm = variables.algorithms[ arguments.algorithm ];
local.key = CreateObject( "java", "javax.crypto.spec.SecretKeySpec" ).init( local.secretBytes, local.algorithm );
local.mac = CreateObject( "java", "javax.crypto.Mac" ).getInstance( JavaCast( "string", local.algorithm ) );
local.mac.init( local.key );
// Create the signature for the message
local.signatureBytes = local.mac.doFinal( local.messageBytes );
// Base64 encode the signature
return BinaryEncode( local.signatureBytes, "base64" );
} // sign()
private string function base64Encode( required string segment )
description = "Takes a serialized segment string and encodes it in base64 string."
{
local.segment = CharsetDecode( arguments.segment, "utf-8" );
return BinaryEncode( local.segment, "base64" );
} // base64Encode()
private string function base64Decode( required string segment )
description = "Takes a base64 segment string and decodes it back to a serialized string."
{
local.segment = BinaryDecode( arguments.segment, "base64" );
return CharsetEncode( local.segment, "utf-8" );
} // base64Decode()
private string function base64Escape( required string segment )
description = "Takes a base64 segment string and URL escapes the proper characters."
{
local.segment = arguments.segment.replace( "+", "-", "all" );
local.segment = local.segment.replace( "/", "_", "all" );
local.segment = local.segment.replace( "=", "", "all" );
return local.segment;
} // base64Escape()
private string function base64Unescape( required string segment )
description = "Takes a URL escaped base64 segment and un-escapes the proper characters."
{
local.segment = arguments.segment.replace( "-", "+", "all" );
local.segment = local.segment.replace( "_", "/", "all" );
local.segment &= RepeatString( "=", 4 - ( local.segment.len() % 4 ) );
return local.segment;
} // base64Unescape()
private date function epochToDate( required numeric epoch )
description = "Takes a JavaScript epoch number and converts it to a Date type."
{
// JavaScript epoch stamps are an hour off of ColdFusion epoch
return CreateObject( "java", "java.util.Date" ).init( arguments.epoch * 1000 );
} // epochToDate()
} // component
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment