component { variables.algorithmMap = { HS256: 'HmacSHA256', HS384: 'HmacSHA384', HS512: 'HmacSHA512', RS256: 'SHA256withRSA', RS384: 'SHA384withRSA', RS512: 'SHA512withRSA', ES256: 'SHA256withECDSA', ES384: 'SHA384withECDSA', ES512: 'SHA512withECDSA' }; public any function init() { variables.encodingUtils = new encodingUtils(); variables.jss = createObject( 'java', 'java.security.Signature' ); variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' ); return this; } public string function encode( required struct payload, required any key, required string algorithm, struct headers = { } ) { if ( !algorithmMap.keyExists( algorithm ) ) { throw( type = 'jwtcfml.InvalidAlgorithm', message = 'Invalid JWT Algorithm.', detail = 'The passed in algorithm is not supported.' ); } var header = { }; header.append( headers ); header.append( { 'typ': 'JWT', 'alg': algorithm } ); var duplicatedPayload = duplicate( payload ); for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) { duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] ); } } var stringToSignParts = [ encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ), encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) ) ]; var stringToSign = stringToSignParts.toList( '.' ); return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) ); } public struct function decode( required string token, any key, any algorithms = [ ], struct claims = { }, boolean verify = true ) { var parts = listToArray( token, '.' ); if ( arrayLen( parts ) != 3 ) { throw( type = 'jwtcfml.InvalidToken', message = 'Invalid JWT.', detail = 'The passed in token does not have three `.` delimited parts.' ); } algorithms = isArray( algorithms ) ? algorithms : [ algorithms ]; var decoded = { header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ), payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) ) }; if ( verify ) { if ( !algorithms.find( decoded.header.alg ) || !algorithmMap.keyExists( decoded.header.alg ) ) { throw( type = 'jwtcfml.InvalidAlgorithm', message = 'Unsupported or invalid algorithm', detail = 'The passed in token does not have an algorithm declaration or its declared algorithm (#decoded.header.alg#) does not match the specified algorithms of #serializeJSON( algorithms )#.' ); } var stringToSign = parts[ 1 ] & '.' & parts[ 2 ]; var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] ); if ( !verifySignature( stringToSign, key, signature, decoded.header.alg ) ) { throw( type = 'jwtcfml.InvalidSignature', message = 'Signature is Invalid', detail = 'The signature of the passed in token is invalid.' ); } var baseClaims = { 'exp': true, 'nbf': true }; baseClaims.append( claims ); verifyClaims( decoded.payload, baseClaims ); } for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { if ( decoded.payload.keyExists( claim ) ) { decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] ); } } return decoded.payload; } public struct function getHeader( required string token ) { return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) ); } public function parsePEMEncodedKey( required string pemKey ) { return encodingUtils.parsePEMEncodedKey( pemKey ); } public function parseJWK( required struct jwk ) { return encodingUtils.parseJWK( jwk ); } private function sign( message, key, algorithm ) { if ( left( algorithm, 1 ) == 'H' ) { var sig = binaryDecode( hmac( message, key, algorithmMap[ algorithm ], 'utf-8' ), 'hex' ); } else { if ( isSimpleValue( key ) ) { key = encodingUtils.parsePEMEncodedKey( key ); } else if ( isStruct( key ) ) { key = encodingUtils.parseJWK( key ); } var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); jssInstance.initSign( key ); jssInstance.update( charsetDecode( message, 'utf-8' ) ); var sig = jssInstance.sign(); if ( left( algorithm, 1 ) == 'E' ) { sig = encodingUtils.convertDERtoP1363( sig, algorithm ); } } return sig; } private function verifySignature( message, key, signature, algorithm ) { if ( left( algorithm, 1 ) == 'H' ) { var sig = binaryDecode( hmac( message, key, algorithmMap[ algorithm ], 'utf-8' ), 'hex' ); return MessageDigest.isEqual( signature, sig ); } if ( left( algorithm, 1 ) == 'E' ) { signature = encodingUtils.convertP1363ToDER( signature ); } if ( isSimpleValue( key ) ) { key = encodingUtils.parsePEMEncodedKey( key ); } else if ( isStruct( key ) ) { key = encodingUtils.parseJWK( key ); } var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); jssInstance.initVerify( key ); jssInstance.update( charsetDecode( message, 'utf-8' ) ); return jssInstance.verify( signature ); } private function verifyClaims( payload, claims ) { if ( structKeyExists( payload, 'exp' ) && !verifyDateClaim( payload.exp, claims.exp, -1 ) ) { throw( type = 'jwtcfml.ExpiredSignature', message = 'Token has expired', detail = 'The passed in token has expired.' ); } if ( structKeyExists( payload, 'nbf' ) && !verifyDateClaim( payload.nbf, claims.nbf, 1 ) ) { throw( type = 'jwtcfml.NotBeforeException', message = 'Token is not valid', detail = 'The passed in token has not yet become valid.' ); } if ( structKeyExists( claims, 'iss' ) ) { if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) { throw( type = 'jwtcfml.InvalidIssuer', message = 'Token has an invalid issuer', detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.' ); } } if ( structKeyExists( claims, 'aud' ) ) { var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ]; if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) { throw( type = 'jwtcfml.InvalidAudience', message = 'Token has an invalid audience', detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.' ); } } } private function verifyDateClaim( payloadDate, claim, failState ) { var pd = encodingUtils.convertUnixTimestampToDate( payloadDate ); var cd = claim; if ( !isBoolean( cd ) || cd ) { if ( isNumeric( cd ) ) { cd = encodingUtils.convertUnixTimestampToDate( cd ); } else if ( !isDate( cd ) ) { cd = now(); } return dateCompare( pd, cd ) != failState; } return true; } }