From 448d1597822bfeb740626971c17acfded3f47f23 Mon Sep 17 00:00:00 2001 From: John Berquist Date: Fri, 16 Aug 2019 14:29:56 -0700 Subject: [PATCH] Initial commit --- .cfformat.json | 51 +++ .gitattributes | 2 + .gitignore | 1 + LICENSE | 21 + ModuleConfig.cfc | 10 + README.md | 227 +++++++++++ box.json | 43 ++ models/encodingUtils.cfc | 283 +++++++++++++ models/jwt.cfc | 262 ++++++++++++ tests/Application.cfc | 15 + tests/index.cfm | 17 + tests/sampleJWK/sampleEC384Private.json | 5 + tests/sampleJWK/sampleEC384Public.json | 6 + tests/sampleJWK/sampleECPrivate.json | 5 + tests/sampleJWK/sampleECPublic.json | 6 + tests/sampleJWK/sampleRSAPrivate.json | 12 + tests/sampleJWK/sampleRSAPublic.json | 7 + tests/sampleKeys/sampleEC.crt | 11 + tests/sampleKeys/sampleEC.key | 5 + tests/sampleKeys/sampleEC.pub | 4 + tests/sampleKeys/sampleEC384.key | 6 + tests/sampleKeys/sampleEC384.pub | 5 + tests/sampleKeys/sampleRSA.crt | 19 + tests/sampleKeys/sampleRSA.key | 28 ++ tests/sampleKeys/sampleRSA.pub | 9 + tests/sampleKeys/unsupported/sampleEC.key | 5 + .../unsupported/sampleECWithParams.key | 8 + tests/sampleKeys/unsupported/sampleRSA.key | 27 ++ tests/sampleTokens/ES256Token.txt | 1 + tests/sampleTokens/ES384Token.txt | 1 + tests/sampleTokens/RS512Token.txt | 1 + tests/server-2016.json | 5 + tests/server-2018.json | 5 + tests/server.json | 5 + tests/specs/encodingUtilsSpec.cfc | 156 ++++++++ tests/specs/jwtSpec.cfc | 374 ++++++++++++++++++ 36 files changed, 1648 insertions(+) create mode 100644 .cfformat.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 ModuleConfig.cfc create mode 100644 README.md create mode 100644 box.json create mode 100644 models/encodingUtils.cfc create mode 100644 models/jwt.cfc create mode 100644 tests/Application.cfc create mode 100644 tests/index.cfm create mode 100644 tests/sampleJWK/sampleEC384Private.json create mode 100644 tests/sampleJWK/sampleEC384Public.json create mode 100644 tests/sampleJWK/sampleECPrivate.json create mode 100644 tests/sampleJWK/sampleECPublic.json create mode 100644 tests/sampleJWK/sampleRSAPrivate.json create mode 100644 tests/sampleJWK/sampleRSAPublic.json create mode 100644 tests/sampleKeys/sampleEC.crt create mode 100644 tests/sampleKeys/sampleEC.key create mode 100644 tests/sampleKeys/sampleEC.pub create mode 100644 tests/sampleKeys/sampleEC384.key create mode 100644 tests/sampleKeys/sampleEC384.pub create mode 100644 tests/sampleKeys/sampleRSA.crt create mode 100644 tests/sampleKeys/sampleRSA.key create mode 100644 tests/sampleKeys/sampleRSA.pub create mode 100644 tests/sampleKeys/unsupported/sampleEC.key create mode 100644 tests/sampleKeys/unsupported/sampleECWithParams.key create mode 100644 tests/sampleKeys/unsupported/sampleRSA.key create mode 100644 tests/sampleTokens/ES256Token.txt create mode 100644 tests/sampleTokens/ES384Token.txt create mode 100644 tests/sampleTokens/RS512Token.txt create mode 100644 tests/server-2016.json create mode 100644 tests/server-2018.json create mode 100644 tests/server.json create mode 100644 tests/specs/encodingUtilsSpec.cfc create mode 100644 tests/specs/jwtSpec.cfc diff --git a/.cfformat.json b/.cfformat.json new file mode 100644 index 0000000..83c7ce7 --- /dev/null +++ b/.cfformat.json @@ -0,0 +1,51 @@ +{ + "array.empty_padding": true, + "array.multiline.element_count": 4, + "array.multiline.leading_comma": false, + "array.multiline.leading_comma.padding": true, + "array.multiline.min_length": 80, + "array.padding": true, + "binary_operators.padding": true, + "brackets.padding": true, + "comment.asterisks": "indent", + "for_loop_semicolons.padding": true, + "function_anonymous.empty_padding": false, + "function_anonymous.group_to_block_spacing": "spaced", + "function_anonymous.multiline.element_count": 4, + "function_anonymous.multiline.leading_comma": false, + "function_anonymous.multiline.leading_comma.padding": true, + "function_anonymous.multiline.min_length": 40, + "function_anonymous.padding": true, + "function_call.empty_padding": false, + "function_call.multiline.element_count": 4, + "function_call.multiline.leading_comma": false, + "function_call.multiline.leading_comma.padding": true, + "function_call.multiline.min_length": 40, + "function_call.padding": true, + "function_declaration.empty_padding": false, + "function_declaration.group_to_block_spacing": "spaced", + "function_declaration.multiline.element_count": 4, + "function_declaration.multiline.leading_comma": false, + "function_declaration.multiline.leading_comma.padding": true, + "function_declaration.multiline.min_length": 40, + "function_declaration.padding": true, + "indent_size": 4, + "keywords.block_to_keyword_spacing": "spaced", + "keywords.empty_group_spacing": false, + "keywords.group_to_block_spacing": "spaced", + "keywords.padding_inside_group": true, + "keywords.spacing_to_block": "spaced", + "keywords.spacing_to_group": true, + "max_columns": 120, + "parentheses.padding": true, + "strings.attributes.quote": "double", + "strings.quote": "single", + "struct.empty_padding": true, + "struct.multiline.element_count": 0, + "struct.multiline.leading_comma": false, + "struct.multiline.leading_comma.padding": true, + "struct.multiline.min_length": 0, + "struct.padding": true, + "struct.separator": ": ", + "tab_indent": false +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4cab1f4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8029c6d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +testbox/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..017c0cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc new file mode 100644 index 0000000..946c56d --- /dev/null +++ b/ModuleConfig.cfc @@ -0,0 +1,10 @@ +component { + + this.title = 'jwt-cfml'; + this.author = 'John Berquist'; + this.webURL = 'https://github.com/jcberquist/jwt-cfml'; + this.description = 'This module supports encoding and decoding JSON Web Tokens.'; + + function configure() {} + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..86c51ad --- /dev/null +++ b/README.md @@ -0,0 +1,227 @@ +# jwt-cfml + +**jwt-cfml** is a CFML (Lucee and ColdFusion) library for encoding and decoding JSON Web Tokens. + +It supports the following algorithms: + +- HS256 +- HS384 +- HS512 +- RS256 +- RS384 +- RS512 +- ES256 +- ES384 +- ES512 + +In the case of the `RS` and `ES` algorithms, asymmetric keys are expected to be provided in unencrypted PEM or JWK format (in the latter case first deserialize the JWK to a CFML struct). When use PEM, private keys need to be encoded in PKCS#8 format. + +If your private key is not currently in this format, conversion should be straightforward: + +```bash +$ openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8 +``` + +When decoding tokens, either a public key or certificate can be provided. (If a certificate is provided, the public key will be extracted from it.) + +*You can pre-parse your encoded keys and pass the returned Java classes to the `encode()` and `decode()` methods, to avoid having them parsed on every method call. See [Parsing Asymmetric Keys](https://github.com/jcberquist/jwt-cfml-dev/blob/master/README.md#parsing-asymmetric-keys) below.* + +## Installation + +Installation is done via CommandBox: + +```bash +$ box install jwt-cfml +``` + +`jwt-cfml` will be installed into a `jwtcfml` package directory by default. + +*Alternatively the git repository can be cloned into the desired directory.* + +### Standalone + +Once the library has been installed, the core `jwt` component can be instantiated directly: + +```cfc +jwt = new path.to.jwtcfml.models.jwt(); +``` + +### ColdBox Module + +You can make use of the library via the injection DSL: `jwt@jwtcfml` + +## Usage + +### Encoding tokens: + +```cfc +payload = {'key': 'value'}; +secret = 'secret'; +token = jwt.encode(payload, secret, 'HS256'); +``` + +```cfc +pemPrivateKey = ' +-----BEGIN PRIVATE KEY----- +... +-----END PRIVATE KEY----- +'; +token = jwt.encode(payload, pemPrivateKey, 'RS256'); +``` + +```cfc +jwk = { + "alg": "RS256", + "d": "...", + "dp": "...", + "dq": "...", + "e": "AQAB", + "kty": "RSA", + "n": "...", + "p": "...", + "q": "...", + "qi": "..." +}; +token = jwt.encode(payload, jwk, 'RS256'); +``` + +When a token is encoded, a header is automatically included containing +`"typ"` set to `"JWT"` and `"alg"` set to the passed in algorithm. If you +need to add additional headers a fourth argument, `headers`, is available +for this: + +```cfc +token = jwt.encode(payload, pemPrivateKey, 'RS256', {'kid': 'abc123'}); +``` + +If your token payload contains `"iat"`, `"exp"`, or `"nbf"` claims, you can +set these to CFML date objects, and they will automatically be converted to +UNIX timestamps in the generated token for you. + +```cfc +payload = {'iat': now()}; +token = jwt.encode(payload, secret, 'HS256'); +``` + +### Decoding tokens: + +```cfc +token = 'eyJ0e...'; +secret = 'secret'; +payload = jwt.decode(token, secret, 'HS256'); +``` + +```cfc +token = 'eyJ0e...'; +pemPublicKey = ' +-----BEGIN PUBLIC KEY----- +... +-----END PUBLIC KEY----- +'; +payload = jwt.decode(token, pemPublicKey, 'RS256'); +``` + +```cfc +token = 'eyJ0e...'; +pemCertificate = ' +-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +'; +payload = jwt.decode(token, pemCertificate, 'RS256'); +``` + +```cfc +token = 'eyJ0e...'; +jwk = { + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "...", + "use": "sig" +} +payload = jwt.decode(token, jwk, 'RS256'); +``` + +*Note: This library does not rely solely on the algorithm specified in the token header. You **must** specify the allowed algorithms (either as a string or an array) when calling `decode()`. The algorithm in the token header must match one of the allowed algorithms.* + +If the decoded payload contains `"iat"`, `"exp"`, or `"nbf"` claims, they will be automatically converted from UNIX timestamps to CFML date objects for you. + +#### Getting the token header + +If you need to get the token header before decoding (e.g. you need a `"kid"` from it), you can use the `jwt.getHeader()` method. This will return the token header as a struct. + +```cfc +token = 'eyJ0e...'; +header = jwt.getHeader(token); + +``` + +#### Token validity + +If a token signature is invalid, the `jwt.decode()` method will throw an error. Further, if the payload contains a `"exp"` or `"nbf"` claim these will be verified as well. + +If you also wish to verify an audience or issuer claim, you can pass valid claims into the decode method: + +```cfc +claims = { + "iss": "somissuer", + "aud": "someaudience" // this can also be an array +}; + +payload = jwt.decode(token, pemCertificate, 'RS256', claims); +``` + +This argument can also be used to ignore the `"exp"` and `"nbf"` claims or to validate them against a timestamp other than the current time: + +```cfc +claims = { + // `exp` will be validated against 1 min in the past instead of the current time + "exp": dateAdd('n', -1, now()), + // `nbf` will be ignored + "nbf": false +}; + +payload = jwt.decode(token, pemCertificate, 'RS256', claims); +``` + +#### Unverified Payload + +If you need to get the payload without doing any verification at all you can pass `verify=false` into the decode method: + +```cfc +jwt.decode(token = token, verify = false); +``` + +### Parsing Asymmetric Keys + +Every time a PEM key or JWK is passed into `encode()` and `decode()` it must be converted to binary data and then the appropriate Java class created. You can avoid this (minor) overhead by parsing your key upfront, and then passing the generated Java key class directly into `encode()` and `decode()`: + +```cfc +pemCertificate = ' +-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +'; +publicKey = jwt.parsePEMEncodedKey(pemCertificate); +payload = jwt.decode(token, publicKey, 'RS256'); +``` + +```cfc +jwk = { + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "AN...", + "use": "sig" +}; +publicKey = jwt.parseJWK(jwk); +payload = jwt.decode(token, publicKey, 'RS256'); +``` + +### Acknowledgments + +- +- +- +- diff --git a/box.json b/box.json new file mode 100644 index 0000000..017fc25 --- /dev/null +++ b/box.json @@ -0,0 +1,43 @@ +{ + "name":"JWT CFML", + "version":"1.0.0", + "author":"John Berquist", + "location":"forgeboxStorage", + "homepage":"https://github.com/jcberquist/jwt-cfml", + "documentation":"", + "repository":{ + "type":"", + "URL":"" + }, + "bugs":"", + "slug":"jwt-cfml", + "packageDirectory": "jwtcfml", + "shortDescription":"JWT CFML is a CFML (Lucee and ColdFusion) library for using JSON Web Tokens.", + "description":"", + "instructions":"", + "changelog":"", + "type":"modules", + "keywords":[ + "jwt" + ], + "private":false, + "projectURL":"", + "license":[ + { + "type":"MIT", + "URL":"" + } + ], + "contributors":[], + "dependencies":{}, + "devDependencies":{ + "testbox":"^3.0.0" + }, + "installPaths":{ + "testbox":"testbox/" + }, + "scripts": { + "tests": "start tests/server.json && start tests/server-2018.json && start tests/server-2016.json", + "format": "cfformat models,tests --overwrite" + } +} diff --git a/models/encodingUtils.cfc b/models/encodingUtils.cfc new file mode 100644 index 0000000..0a5cf2f --- /dev/null +++ b/models/encodingUtils.cfc @@ -0,0 +1,283 @@ +component { + + public any function init() { + variables.utcBaseDate = createObject( 'java', 'java.util.Date' ).init( javacast( 'int', 0 ) ); + variables.ECParameterSpecCache = { }; + } + + function convertDateToUnixTimestamp( required date dateToConvert ) { + return dateDiff( 's', utcBaseDate, parseDateTime( dateToConvert ) ); + } + + function convertUnixTimestampToDate( required numeric timestamp ) { + return dateAdd( 's', timestamp, utcBaseDate ); + } + + function base64UrlToBinary( base64url ) { + var base64 = base64url.replace( '-', '+', 'all' ).replace( '_', '/', 'all' ); + var padded = base64 & repeatString( '=', 4 - ( len( base64 ) % 4 ) ); + return binaryDecode( padded, 'base64' ); + } + + function binaryToBase64Url( source ) { + return binaryEncode( source, 'base64' ) + .replace( '+', '-', 'all' ) + .replace( '/', '_', 'all' ) + .replace( '=', '', 'all' ); + } + + /** + * The INTEGER encoding for DER consists of a 02 tag a length encoding of the value + * and then a signed, minimum sized, big endian encoding of the encoded number. + * + * - https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature + */ + function derEncodeIntegerBytes( byteArray ) { + // first remove any padding + for ( var i = 1; i <= arrayLen( byteArray ); i++ ) { + if ( byteArray[ i ] != 0 ) break; + } + var unpadded = arraySlice( byteArray, i ); + + // add sign if negative + if ( unpadded[ 1 ] < 0 ) { + unpadded.prepend( 0 ); + } + + // if len > 127 the length encoding will be wrong, but that won't happen for the supported signature sizes + var derEncoded = [ 2, unpadded.len() ]; + + derEncoded.append( unpadded, true ); + + return derEncoded; + } + + + /** + * The SEQUENCE encoding is simply a tag set to the byte value 30, the + * length encoding and then the concatenation of the two INTEGER + * structures. + * + * https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature + * + * Also see: + * https://crypto.stackexchange.com/questions/57731/ecdsa-signature-rs-to-asn1-der-encoding-question + */ + function convertP1363ToDER( signature ) { + var split = len( signature ) / 2; + var r = derEncodeIntegerBytes( arraySlice( signature, 1, split ) ); + var s = derEncodeIntegerBytes( arraySlice( signature, split + 1, split ) ); + + var DERSignature = [ 48 ]; + + var length = r.len() + s.len(); + + if ( length > 255 ) { + throw( + type = 'jwtcfml.InvalidSignature', + message = 'Invalid P1363 key.', + detail = 'The P1363 signature is too long.' + ); + } + + /* + The length is simply a single byte if it is smaller than 128 (or hex 80) + of the size. If it is larger then it is two byte: one byte set to 81, + which indicates that one length byte will follow, and one byte + containing the actual value. + + https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature + */ + if ( length > 127 ) { + DERSignature.append( -127 ); + length -= 256; + } + DERSignature.append( length ); + + DERSignature.append( r, true ); + DERSignature.append( s, true ); + + return javacast( 'byte[]', DERSignature ); + } + + function convertDERtoP1363( required any signature, required string algorithm ) { + // extract the two integers from the DER signature + // assuming a 02 tag byte followed by a single length byte since we should not see + // anything larger in the supported algorithms + var start = 3; + while ( signature[ start ] != 2 ) start++; + var r = arraySlice( signature, start + 2, signature[ start + 1 ] ); + var s = arraySlice( signature, start + 2 + r.len() + 2 ); + + if ( r[ 1 ] == 0 ) r = arraySlice( r, 2 ); + if ( s[ 1 ] == 0 ) s = arraySlice( s, 2 ); + + var lengthMap = { + ES256: 32, + ES384: 48, + ES512: 64 + }; + + var P1363Signature = [ ]; + + for ( var i = 1; i <= lengthMap[ algorithm ] - r.len(); i++ ) P1363Signature.append( 0 ); + P1363Signature.append( r, true ); + + for ( var i = 1; i <= lengthMap[ algorithm ] - s.len(); i++ ) P1363Signature.append( 0 ); + P1363Signature.append( s, true ); + + return javacast( 'byte[]', P1363Signature ); + } + + function parsePEMEncodedKey( required string pemKey ) { + if ( reFind( '^-----BEGIN (RSA|EC) (PARAMETERS|PRIVATE)', pemKey ) ) { + throw( + type = 'jwtcfml.InvalidPrivateKey', + message = 'Invalid private key format.', + detail = 'Please encode your private key in PKCS8 format, e.g.: `openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8' + ) + } + + var binaryKey = binaryDecode( + trim( pemKey ).reReplace( '-----[A-Z\s]+-----', '', 'all' ).reReplace( '[\r\n]', '', 'all' ), + 'base64' + ); + + if ( find( '-----BEGIN CERTIFICATE-----', pemKey ) ) { + var bis = createObject( 'java', 'java.io.ByteArrayInputStream' ).init( binaryKey ); + return createObject( 'java', 'java.security.cert.CertificateFactory' ) + .getInstance( 'X.509' ) + .generateCertificate( bis ) + .getPublicKey(); + } + + if ( find( '-----BEGIN PUBLIC KEY-----', pemKey ) ) { + var publicKeySpec = createObject( 'java', 'java.security.spec.X509EncodedKeySpec' ).init( binaryKey ); + try { + return createObject( 'java', 'java.security.KeyFactory' ) + .getInstance( 'RSA' ) + .generatePublic( publicKeySpec ); + } catch ( any e ) { + } + try { + return createObject( 'java', 'java.security.KeyFactory' ) + .getInstance( 'EC' ) + .generatePublic( publicKeySpec ); + } catch ( any e ) { + } + } + + if ( find( '-----BEGIN PRIVATE KEY-----', pemKey ) ) { + var privateKeySpec = createObject( 'java', 'java.security.spec.PKCS8EncodedKeySpec' ).init( binaryKey ); + try { + return createObject( 'java', 'java.security.KeyFactory' ) + .getInstance( 'RSA' ) + .generatePrivate( privateKeySpec ); + } catch ( any e ) { + } + try { + return createObject( 'java', 'java.security.KeyFactory' ) + .getInstance( 'EC' ) + .generatePrivate( privateKeySpec ); + } catch ( any e ) { + } + } + + throw( + type = 'jwtcfml.InvalidPEMKey', + message = 'Invalid PEM key.', + detail = 'Please ensure you are using an RSA or EC public or private key or certificate.' + ) + } + + function parseJWK( required struct jwk ) { + if ( jwk.kty == 'RSA' ) { + if ( jwk.keyExists( 'd' ) ) { + try { + var bigInts = bigIntegers( jwk, [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ] ); + var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateCrtKeySpec' ).init( + bigInts.n, + bigInts.e, + bigInts.d, + bigInts.p, + bigInts.q, + bigInts.dp, + bigInts.dq, + bigInts.qi + ); + var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' ); + return kf.generatePrivate( keySpec ); + } catch ( any e ) { + } + + try { + var bigInts = bigIntegers( jwk, [ 'n', 'd' ] ); + var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateKeySpec' ).init( + bigInts.n, + bigInts.d + ); + var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' ); + return kf.generatePrivate( keySpec ); + } catch ( any e ) { + } + } else { + try { + var bigInts = bigIntegers( jwk, [ 'n', 'e' ] ); + var ks = createObject( 'java', 'java.security.spec.RSAPublicKeySpec' ).init( bigInts.n, bigInts.e ); + var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' ); + return kf.generatePublic( ks ); + } catch ( any e ) { + } + } + } + + if ( jwk.kty == 'EC' ) { + var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'EC' ); + var ECParameterSpec = getECParameterSpec( jwk.crv ); + + if ( jwk.keyExists( 'd' ) ) { + var bigInts = bigIntegers( jwk, [ 'd' ] ); + var ks = createObject( 'java', 'java.security.spec.ECPrivateKeySpec' ).init( + bigInts.d, + ECParameterSpec + ); + return kf.generatePrivate( ks ); + } else { + var bigInts = bigIntegers( jwk, [ 'x', 'y' ] ); + var ECPoint = createObject( 'java', 'java.security.spec.ECPoint' ).init( bigInts.x, bigInts.y ); + var ks = createObject( 'java', 'java.security.spec.ECPublicKeySpec' ).init( ECPoint, ECParameterSpec ); + return kf.generatePublic( ks ); + } + } + + throw( + type = 'jwtcfml.InvalidJWK', + message = 'Invalid JWK key.', + detail = 'Please ensure you are using an valid JWK RSA or EC public or private key.' + ) + } + + private function bigIntegers( jwk, keys ) { + var bigInts = { }; + for ( var key in keys ) { + bigInts[ key ] = createObject( 'java', 'java.math.BigInteger' ).init( 1, base64UrlToBinary( jwk[ key ] ) ); + } + return bigInts; + } + + private function getECParameterSpec( crv ) { + if ( !variables.ECParameterSpecCache.keyExists( crv ) ) { + var kpg = createObject( 'java', 'java.security.KeyPairGenerator' ).getInstance( 'EC' ); + var ecgp = createObject( 'java', 'java.security.spec.ECGenParameterSpec' ).init( + 'secp#crv.listLast( '-' )#r1' + ); + kpg.initialize( ecgp ); + variables.ECParameterSpecCache[ crv ] = kpg + .generateKeyPair() + .getPublic() + .getParams(); + } + return variables.ECParameterSpecCache[ crv ]; + } + +} diff --git a/models/jwt.cfc b/models/jwt.cfc new file mode 100644 index 0000000..7cfe61c --- /dev/null +++ b/models/jwt.cfc @@ -0,0 +1,262 @@ +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 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; + } + +} diff --git a/tests/Application.cfc b/tests/Application.cfc new file mode 100644 index 0000000..34a2e7b --- /dev/null +++ b/tests/Application.cfc @@ -0,0 +1,15 @@ +component { + + rootPath = getDirectoryFromPath( getCurrentTemplatePath() ) + .replace( '\', '/', 'all' ) + .replaceNoCase( 'tests/', '' ); + + this.mappings[ '/testbox' ] = rootPath & '/testbox'; + this.mappings[ '/models' ] = rootPath & '/models'; + + public boolean function onRequestStart( String targetPage ) { + setting requestTimeout="9999"; + return true; + } + +} diff --git a/tests/index.cfm b/tests/index.cfm new file mode 100644 index 0000000..1853728 --- /dev/null +++ b/tests/index.cfm @@ -0,0 +1,17 @@ + + +testbox = new testbox.system.Testbox(); +param name="url.reporter" default="simple"; +param name="url.directory" default="specs"; +args = {reporter: url.reporter, directory: url.directory}; +if (structKeyExists(url, 'bundles')) args.bundles = url.bundles; +results = testBox.run(argumentCollection = args); + + + + +

+ #server.coldfusion.productname# #structKeyExists(server, 'lucee') ? server.lucee.version : server.coldfusion.productversion# +

+#trim(results)# +
diff --git a/tests/sampleJWK/sampleEC384Private.json b/tests/sampleJWK/sampleEC384Private.json new file mode 100644 index 0000000..741fb72 --- /dev/null +++ b/tests/sampleJWK/sampleEC384Private.json @@ -0,0 +1,5 @@ +{ + "crv": "P-384", + "kty": "EC", + "d": "AOMh3Is6yjedOi6TS8maYRTk2fFOKoFE4f7aqVxxw2fxbc1PDaLRb_YTZeBTOU8gyw" +} diff --git a/tests/sampleJWK/sampleEC384Public.json b/tests/sampleJWK/sampleEC384Public.json new file mode 100644 index 0000000..36f322f --- /dev/null +++ b/tests/sampleJWK/sampleEC384Public.json @@ -0,0 +1,6 @@ +{ + "crv": "P-384", + "kty": "EC", + "x": "bYtC5AenYjyswReNKf92ZylzSQ3oTDRKuEhsYvpsuyMJNpRLCWyOnXw7RlTwvgpx", + "y": "VGYjCX0Z0LCRj1VmEc08DxPp7ln-vm_eb8wK-xbnACWiDA73ZMlodv3Foo7Tcaxv" +} diff --git a/tests/sampleJWK/sampleECPrivate.json b/tests/sampleJWK/sampleECPrivate.json new file mode 100644 index 0000000..a47a6cc --- /dev/null +++ b/tests/sampleJWK/sampleECPrivate.json @@ -0,0 +1,5 @@ +{ + "crv": "P-256", + "kty": "EC", + "d": "ao0goNJKSAJOXfKbYmgS5UM4CpF5-40TmGcAMo1koQ4" +} diff --git a/tests/sampleJWK/sampleECPublic.json b/tests/sampleJWK/sampleECPublic.json new file mode 100644 index 0000000..a28722e --- /dev/null +++ b/tests/sampleJWK/sampleECPublic.json @@ -0,0 +1,6 @@ +{ + "crv": "P-256", + "kty": "EC", + "x": "ANxLaruRUsNrYI9MiiH-oK9OQAH2O5tJ72Y5zRSzyrS1", + "y": "AJ-raaFowSNRVNpr4sR8lfSXNX7dYOqSYQSAGhuZGGTD" +} diff --git a/tests/sampleJWK/sampleRSAPrivate.json b/tests/sampleJWK/sampleRSAPrivate.json new file mode 100644 index 0000000..d8bbcc2 --- /dev/null +++ b/tests/sampleJWK/sampleRSAPrivate.json @@ -0,0 +1,12 @@ +{ + "alg": "RS256", + "d": "cIz_j-HixfHYUYOWsol3vOmQWZPBcs-CtysVeURfVzDwlcYSXGGbZpvGlZzfHEcqh_9yl5e74jDRWtBZBmVvpIGZ_T2GOaFJPDlxz1x7fJayouiadRDqvbziiAJgzpmHbtKvLZ1hTDVYTnlDpd0p7C6_lZjqIDJefmuq4GczjVP-q76qrL6wzz54WTI22iMk0IB528uEABDKfWY-RKDtdxVokl8dTXMXMRrT6HIq1y1H9TOLhYSROHHM9npGybC-w6cBA7Q0dpUlVvdBXt78FErWJOkjx5zBm36QHcEee5H8pTY6Blu8YYCfxYZJxZDhJ7F1VcFxKT3tS7JTzHRbiQ", + "dp": "AKX9zQDeNnAYZeHWesmUVu5FmNokFdQdlWEp8OGu4cFBjOED3YYsbaX32ynYzFgk47uLaCQj_QKfOPEh79nJhmqZG8G7UfXkqFtF_9Vd2bERyiz1qq6spDjCCPyMmTWupjidHbgWPJK9hzWnlMclfS_deC06L545jsbTqhvR4GHZ", + "dq": "ANWK5PBrS-7y-lVJfBaIVATrokBnamgcWkEVxAI-W7WPE6p84MCUCy6d3xLYLokWmL1IfJUd50uq_u5ow9UxFOZZxP2E_TxGHT1Q1aedXZ_LnYwh9SbcGp2qg0qseqqHLgrqD8hFVttvAlGo1pkxO7ctcIp_KbpP0mFWSG18FvU", + "e": "AQAB", + "kty": "RSA", + "n": "ANzdh51XtFOGyDLtjRT3TUrwWW7NtycbvL_U7PRLN5fv7oNbzx0iwr9tqX_YqrtQFhHgHdtcsXPWLJ_5SK5UQgdqDdWy5dD6GL1yZ98t3kvEDQsd2gniLiaF8lEHoh9G-Uume-PLtsMSrzagspDGMaLHb37L6Wk55tktGvG8QNKGdDR5wOzB2h02fXxfXROQq9mVm3Rn_6UZP2o46i6-g3Zp304VeCWjXRmy8saxEt1pAVY8LlAL65YTYSqjrkwd1UiY7chR2_AQmJ5FkFweBoPafmbGLUWAM5juTNcuwf5x5mBIzddBz4S9sCaPBb8BYjVw3WUTBW_R7q1xCNr63Cc", + "p": "APUfmtaaCM3mm4cqmOKTiN0dsMOlLsVj2PPWHENH5MYshWEe1z2vxcbDcE7vH3Rd1ssWbPNDootjWg7ImzarQJs8SA09JJQT1UR-4DN8kXLnH1mAQ2ZH3hEoFrI0mDSnHthPUSIaMTLAKsAaRYk7Hbn23PeLsQuFO17G2jnMhjd7", + "q": "AOaqX5M90wOBhj7vmcoCnCPqBZYvKwCRkA6DzVVX491YV7yPK3dPtkoqLzFhfbXCkrd_tD44sZfXRLKqVqzHvSDfPNuWHw9oEurcbzm7-LJm3_cWJhz3oooDYStoh2NiIQWApxkz-rzIxFLQpHbaAyQW2ePYy1ByAuhoaZo2kThF", + "qi": "AMrKrgN9RzxggKOcbe-mKOx2SNLw9SBDN_FWOsXJw_HLXW2uToImf7RIVyThhKUYdpwJMCjFsrDsKjcYIVVhsiV2FxNqtYUi-jf3-ec4muRTmFERwhAZiDD-dPCHZ2W748K3a4WGH7ybQ3u9dOU1TGc-zWOO-3hhXd09U1EaWd4U" +} diff --git a/tests/sampleJWK/sampleRSAPublic.json b/tests/sampleJWK/sampleRSAPublic.json new file mode 100644 index 0000000..ca3b487 --- /dev/null +++ b/tests/sampleJWK/sampleRSAPublic.json @@ -0,0 +1,7 @@ +{ + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "ANzdh51XtFOGyDLtjRT3TUrwWW7NtycbvL_U7PRLN5fv7oNbzx0iwr9tqX_YqrtQFhHgHdtcsXPWLJ_5SK5UQgdqDdWy5dD6GL1yZ98t3kvEDQsd2gniLiaF8lEHoh9G-Uume-PLtsMSrzagspDGMaLHb37L6Wk55tktGvG8QNKGdDR5wOzB2h02fXxfXROQq9mVm3Rn_6UZP2o46i6-g3Zp304VeCWjXRmy8saxEt1pAVY8LlAL65YTYSqjrkwd1UiY7chR2_AQmJ5FkFweBoPafmbGLUWAM5juTNcuwf5x5mBIzddBz4S9sCaPBb8BYjVw3WUTBW_R7q1xCNr63Cc", + "use": "sig" +} diff --git a/tests/sampleKeys/sampleEC.crt b/tests/sampleKeys/sampleEC.crt new file mode 100644 index 0000000..59d6a13 --- /dev/null +++ b/tests/sampleKeys/sampleEC.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBgjCCASmgAwIBAgIUdorHUF77VCfTuzOEBszNueKCOGswCgYIKoZIzj0EAwIw +FzEVMBMGA1UEAwwMc2FtcGxlRUMuY3J0MB4XDTE5MDgxMjIzMTUyMloXDTIxMDgx +MTIzMTUyMlowFzEVMBMGA1UEAwwMc2FtcGxlRUMuY3J0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAE3Etqu5FSw2tgj0yKIf6gr05AAfY7m0nvZjnNFLPKtLWfq2mh +aMEjUVTaa+LEfJX0lzV+3WDqkmEEgBobmRhkw6NTMFEwHQYDVR0OBBYEFEjIIzTU +0SLrLJKl9o14jQH/1lJcMB8GA1UdIwQYMBaAFEjIIzTU0SLrLJKl9o14jQH/1lJc +MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgPJvHVJw4oemer7i/ +lHCSDKLw1IEGtwhR8/AFjyCLo9YCIH4USuW7jrnBdXSZnW/GR51ew0wYxeWDVNwN +Aq1OWcre +-----END CERTIFICATE----- diff --git a/tests/sampleKeys/sampleEC.key b/tests/sampleKeys/sampleEC.key new file mode 100644 index 0000000..f5c6d0f --- /dev/null +++ b/tests/sampleKeys/sampleEC.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgao0goNJKSAJOXfKb +YmgS5UM4CpF5+40TmGcAMo1koQ6hRANCAATcS2q7kVLDa2CPTIoh/qCvTkAB9jub +Se9mOc0Us8q0tZ+raaFowSNRVNpr4sR8lfSXNX7dYOqSYQSAGhuZGGTD +-----END PRIVATE KEY----- diff --git a/tests/sampleKeys/sampleEC.pub b/tests/sampleKeys/sampleEC.pub new file mode 100644 index 0000000..f045ab0 --- /dev/null +++ b/tests/sampleKeys/sampleEC.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3Etqu5FSw2tgj0yKIf6gr05AAfY7 +m0nvZjnNFLPKtLWfq2mhaMEjUVTaa+LEfJX0lzV+3WDqkmEEgBobmRhkww== +-----END PUBLIC KEY----- diff --git a/tests/sampleKeys/sampleEC384.key b/tests/sampleKeys/sampleEC384.key new file mode 100644 index 0000000..3f4bbca --- /dev/null +++ b/tests/sampleKeys/sampleEC384.key @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDjIdyLOso3nTouk0vJ +mmEU5NnxTiqBROH+2qlcccNn8W3NTw2i0W/2E2XgUzlPIMuhZANiAARti0LkB6di +PKzBF40p/3ZnKXNJDehMNEq4SGxi+my7Iwk2lEsJbI6dfDtGVPC+CnFUZiMJfRnQ +sJGPVWYRzTwPE+nuWf6+b95vzAr7FucAJaIMDvdkyWh2/cWijtNxrG8= +-----END PRIVATE KEY----- diff --git a/tests/sampleKeys/sampleEC384.pub b/tests/sampleKeys/sampleEC384.pub new file mode 100644 index 0000000..e721fdc --- /dev/null +++ b/tests/sampleKeys/sampleEC384.pub @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbYtC5AenYjyswReNKf92ZylzSQ3oTDRK +uEhsYvpsuyMJNpRLCWyOnXw7RlTwvgpxVGYjCX0Z0LCRj1VmEc08DxPp7ln+vm/e +b8wK+xbnACWiDA73ZMlodv3Foo7Tcaxv +-----END PUBLIC KEY----- diff --git a/tests/sampleKeys/sampleRSA.crt b/tests/sampleKeys/sampleRSA.crt new file mode 100644 index 0000000..31e9244 --- /dev/null +++ b/tests/sampleKeys/sampleRSA.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIUF2ktIPZMoxVF+Zqutw/1SiV8tcMwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNc2FtcGxlUlNBLmNydDAeFw0xOTA4MTIyMzE0MTNaFw0y +MTA4MTEyMzE0MTNaMBgxFjAUBgNVBAMMDXNhbXBsZVJTQS5jcnQwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc3YedV7RThsgy7Y0U901K8FluzbcnG7y/ +1Oz0SzeX7+6DW88dIsK/bal/2Kq7UBYR4B3bXLFz1iyf+UiuVEIHag3VsuXQ+hi9 +cmffLd5LxA0LHdoJ4i4mhfJRB6IfRvlLpnvjy7bDEq82oLKQxjGix29+y+lpOebZ +LRrxvEDShnQ0ecDswdodNn18X10TkKvZlZt0Z/+lGT9qOOouvoN2ad9OFXglo10Z +svLGsRLdaQFWPC5QC+uWE2Eqo65MHdVImO3IUdvwEJieRZBcHgaD2n5mxi1FgDOY +7kzXLsH+ceZgSM3XQc+EvbAmjwW/AWI1cN1lEwVv0e6tcQja+twnAgMBAAGjUzBR +MB0GA1UdDgQWBBShD+I6pR+ylK5pnS3sM2Di0FCwiTAfBgNVHSMEGDAWgBShD+I6 +pR+ylK5pnS3sM2Di0FCwiTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQAZdepAovemmC74+ErbadHZcp0qFV/QcFx3vBaMRAwY8UMYhzeK0ugD3JNU +D0SVLd0ZvrcyB1oZfmMACHaSUyyWSCRmzybF4kGmsV0ubYwJqlJtrsTqDKdZmTDA +ItFE7q97lIO0k5P2JS/wJaCJ7T6+9DuSQzDfq5R8/Yx+CW/7rL7++P1cF7c0matN +hM8AbKHJ4wuLHcHCO8B7Ai1PhQ7YrIL8TZho4JfToNqbmMNvFGvpA5n+lDLi228l +mvoyv/Ae3OMmpQt1oBGy6TJzC2KnXCBy1hQ+7hSH7EFMlVjDtj5L2Ak3NX0jqmi9 +RwY7TV32DfKx4SGOSumuh33d0jY9 +-----END CERTIFICATE----- diff --git a/tests/sampleKeys/sampleRSA.key b/tests/sampleKeys/sampleRSA.key new file mode 100644 index 0000000..ca62d69 --- /dev/null +++ b/tests/sampleKeys/sampleRSA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDc3YedV7RThsgy +7Y0U901K8FluzbcnG7y/1Oz0SzeX7+6DW88dIsK/bal/2Kq7UBYR4B3bXLFz1iyf ++UiuVEIHag3VsuXQ+hi9cmffLd5LxA0LHdoJ4i4mhfJRB6IfRvlLpnvjy7bDEq82 +oLKQxjGix29+y+lpOebZLRrxvEDShnQ0ecDswdodNn18X10TkKvZlZt0Z/+lGT9q +OOouvoN2ad9OFXglo10ZsvLGsRLdaQFWPC5QC+uWE2Eqo65MHdVImO3IUdvwEJie +RZBcHgaD2n5mxi1FgDOY7kzXLsH+ceZgSM3XQc+EvbAmjwW/AWI1cN1lEwVv0e6t +cQja+twnAgMBAAECggEAcIz/j+HixfHYUYOWsol3vOmQWZPBcs+CtysVeURfVzDw +lcYSXGGbZpvGlZzfHEcqh/9yl5e74jDRWtBZBmVvpIGZ/T2GOaFJPDlxz1x7fJay +ouiadRDqvbziiAJgzpmHbtKvLZ1hTDVYTnlDpd0p7C6/lZjqIDJefmuq4GczjVP+ +q76qrL6wzz54WTI22iMk0IB528uEABDKfWY+RKDtdxVokl8dTXMXMRrT6HIq1y1H +9TOLhYSROHHM9npGybC+w6cBA7Q0dpUlVvdBXt78FErWJOkjx5zBm36QHcEee5H8 +pTY6Blu8YYCfxYZJxZDhJ7F1VcFxKT3tS7JTzHRbiQKBgQD1H5rWmgjN5puHKpji +k4jdHbDDpS7FY9jz1hxDR+TGLIVhHtc9r8XGw3BO7x90XdbLFmzzQ6KLY1oOyJs2 +q0CbPEgNPSSUE9VEfuAzfJFy5x9ZgENmR94RKBayNJg0px7YT1EiGjEywCrAGkWJ +Ox259tz3i7ELhTtexto5zIY3ewKBgQDmql+TPdMDgYY+75nKApwj6gWWLysAkZAO +g81VV+PdWFe8jyt3T7ZKKi8xYX21wpK3f7Q+OLGX10Syqlasx70g3zzblh8PaBLq +3G85u/iyZt/3FiYc96KKA2EraIdjYiEFgKcZM/q8yMRS0KR22gMkFtnj2MtQcgLo +aGmaNpE4RQKBgQCl/c0A3jZwGGXh1nrJlFbuRZjaJBXUHZVhKfDhruHBQYzhA92G +LG2l99sp2MxYJOO7i2gkI/0CnzjxIe/ZyYZqmRvBu1H15KhbRf/VXdmxEcos9aqu +rKQ4wgj8jJk1rqY4nR24FjySvYc1p5THJX0v3XgtOi+eOY7G06ob0eBh2QKBgADV +iuTwa0vu8vpVSXwWiFQE66JAZ2poHFpBFcQCPlu1jxOqfODAlAsund8S2C6JFpi9 +SHyVHedLqv7uaMPVMRTmWcT9hP08Rh09UNWnnV2fy52MIfUm3BqdqoNKrHqqhy4K +6g/IRVbbbwJRqNaZMTu3LXCKfym6T9JhVkhtfBb1AoGBAMrKrgN9RzxggKOcbe+m +KOx2SNLw9SBDN/FWOsXJw/HLXW2uToImf7RIVyThhKUYdpwJMCjFsrDsKjcYIVVh +siV2FxNqtYUi+jf3+ec4muRTmFERwhAZiDD+dPCHZ2W748K3a4WGH7ybQ3u9dOU1 +TGc+zWOO+3hhXd09U1EaWd4U +-----END PRIVATE KEY----- diff --git a/tests/sampleKeys/sampleRSA.pub b/tests/sampleKeys/sampleRSA.pub new file mode 100644 index 0000000..db8b8ba --- /dev/null +++ b/tests/sampleKeys/sampleRSA.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3N2HnVe0U4bIMu2NFPdN +SvBZbs23Jxu8v9Ts9Es3l+/ug1vPHSLCv22pf9iqu1AWEeAd21yxc9Ysn/lIrlRC +B2oN1bLl0PoYvXJn3y3eS8QNCx3aCeIuJoXyUQeiH0b5S6Z748u2wxKvNqCykMYx +osdvfsvpaTnm2S0a8bxA0oZ0NHnA7MHaHTZ9fF9dE5Cr2ZWbdGf/pRk/ajjqLr6D +dmnfThV4JaNdGbLyxrES3WkBVjwuUAvrlhNhKqOuTB3VSJjtyFHb8BCYnkWQXB4G +g9p+ZsYtRYAzmO5M1y7B/nHmYEjN10HPhL2wJo8FvwFiNXDdZRMFb9HurXEI2vrc +JwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/sampleKeys/unsupported/sampleEC.key b/tests/sampleKeys/unsupported/sampleEC.key new file mode 100644 index 0000000..0434b79 --- /dev/null +++ b/tests/sampleKeys/unsupported/sampleEC.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILjLYxXi532Ylm1BH3prwm+BH3WydlVMwe1zd0wWjPrcoAoGCCqGSM49 +AwEHoUQDQgAEmAFJznwVEzPgU6G4IzMzIBS7A9E6vDNp7hSnmaFl27zK1AdYlBWP +vX7BiwRUAkM4VsYYt2G+LqqVuj7tIrgDew== +-----END EC PRIVATE KEY----- diff --git a/tests/sampleKeys/unsupported/sampleECWithParams.key b/tests/sampleKeys/unsupported/sampleECWithParams.key new file mode 100644 index 0000000..63778e6 --- /dev/null +++ b/tests/sampleKeys/unsupported/sampleECWithParams.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIGqNIKDSSkgCTl3ym2JoEuVDOAqRefuNE5hnADKNZKEOoAoGCCqGSM49 +AwEHoUQDQgAE3Etqu5FSw2tgj0yKIf6gr05AAfY7m0nvZjnNFLPKtLWfq2mhaMEj +UVTaa+LEfJX0lzV+3WDqkmEEgBobmRhkww== +-----END EC PRIVATE KEY----- diff --git a/tests/sampleKeys/unsupported/sampleRSA.key b/tests/sampleKeys/unsupported/sampleRSA.key new file mode 100644 index 0000000..353f618 --- /dev/null +++ b/tests/sampleKeys/unsupported/sampleRSA.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3N2HnVe0U4bIMu2NFPdNSvBZbs23Jxu8v9Ts9Es3l+/ug1vP +HSLCv22pf9iqu1AWEeAd21yxc9Ysn/lIrlRCB2oN1bLl0PoYvXJn3y3eS8QNCx3a +CeIuJoXyUQeiH0b5S6Z748u2wxKvNqCykMYxosdvfsvpaTnm2S0a8bxA0oZ0NHnA +7MHaHTZ9fF9dE5Cr2ZWbdGf/pRk/ajjqLr6DdmnfThV4JaNdGbLyxrES3WkBVjwu +UAvrlhNhKqOuTB3VSJjtyFHb8BCYnkWQXB4Gg9p+ZsYtRYAzmO5M1y7B/nHmYEjN +10HPhL2wJo8FvwFiNXDdZRMFb9HurXEI2vrcJwIDAQABAoIBAHCM/4/h4sXx2FGD +lrKJd7zpkFmTwXLPgrcrFXlEX1cw8JXGElxhm2abxpWc3xxHKof/cpeXu+Iw0VrQ +WQZlb6SBmf09hjmhSTw5cc9ce3yWsqLomnUQ6r284ogCYM6Zh27Sry2dYUw1WE55 +Q6XdKewuv5WY6iAyXn5rquBnM41T/qu+qqy+sM8+eFkyNtojJNCAedvLhAAQyn1m +PkSg7XcVaJJfHU1zFzEa0+hyKtctR/Uzi4WEkThxzPZ6RsmwvsOnAQO0NHaVJVb3 +QV7e/BRK1iTpI8ecwZt+kB3BHnuR/KU2OgZbvGGAn8WGScWQ4SexdVXBcSk97Uuy +U8x0W4kCgYEA9R+a1poIzeabhyqY4pOI3R2ww6UuxWPY89YcQ0fkxiyFYR7XPa/F +xsNwTu8fdF3WyxZs80Oii2NaDsibNqtAmzxIDT0klBPVRH7gM3yRcucfWYBDZkfe +ESgWsjSYNKce2E9RIhoxMsAqwBpFiTsdufbc94uxC4U7XsbaOcyGN3sCgYEA5qpf +kz3TA4GGPu+ZygKcI+oFli8rAJGQDoPNVVfj3VhXvI8rd0+2SiovMWF9tcKSt3+0 +Pjixl9dEsqpWrMe9IN8825YfD2gS6txvObv4smbf9xYmHPeiigNhK2iHY2IhBYCn +GTP6vMjEUtCkdtoDJBbZ49jLUHIC6GhpmjaROEUCgYEApf3NAN42cBhl4dZ6yZRW +7kWY2iQV1B2VYSnw4a7hwUGM4QPdhixtpffbKdjMWCTju4toJCP9Ap848SHv2cmG +apkbwbtR9eSoW0X/1V3ZsRHKLPWqrqykOMII/IyZNa6mOJ0duBY8kr2HNaeUxyV9 +L914LTovnjmOxtOqG9HgYdkCgYAA1Yrk8GtL7vL6VUl8FohUBOuiQGdqaBxaQRXE +Aj5btY8TqnzgwJQLLp3fEtguiRaYvUh8lR3nS6r+7mjD1TEU5lnE/YT9PEYdPVDV +p51dn8udjCH1JtwanaqDSqx6qocuCuoPyEVW228CUajWmTE7ty1win8puk/SYVZI +bXwW9QKBgQDKyq4DfUc8YICjnG3vpijsdkjS8PUgQzfxVjrFycPxy11trk6CJn+0 +SFck4YSlGHacCTAoxbKw7Co3GCFVYbIldhcTarWFIvo39/nnOJrkU5hREcIQGYgw +/nTwh2dlu+PCt2uFhh+8m0N7vXTlNUxnPs1jjvt4YV3dPVNRGlneFA== +-----END RSA PRIVATE KEY----- diff --git a/tests/sampleTokens/ES256Token.txt b/tests/sampleTokens/ES256Token.txt new file mode 100644 index 0000000..8f1e5c8 --- /dev/null +++ b/tests/sampleTokens/ES256Token.txt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.A8tObqrWfNuG4Ln382_mr7U8728xt26F5Shj47x85eKu3BLsduNDwnMXBlVuewZ1ibCx0jtuKE8e1LDyXMiH6A diff --git a/tests/sampleTokens/ES384Token.txt b/tests/sampleTokens/ES384Token.txt new file mode 100644 index 0000000..5f328e3 --- /dev/null +++ b/tests/sampleTokens/ES384Token.txt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.uoMh_N27Uk0iMJf4PLUTGGia9CWhm_oRWcw3DTt313_vzlFbx4pWtvVtKRaGINdn3htARw3qwBknDLW-TwVY3BIwJRANs9JEXaOVjlIBlXu4UkT3XBsCgD6u1P3TUxmO diff --git a/tests/sampleTokens/RS512Token.txt b/tests/sampleTokens/RS512Token.txt new file mode 100644 index 0000000..7528fc7 --- /dev/null +++ b/tests/sampleTokens/RS512Token.txt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.ztKnr2euWi8g-ag6S0omMKSBsXwDXY3_0THc8UVKZ65xMgHEY6ZadXS7ksn82uZyBb9FnhBIcaMXXk0JrMUShZI26zK_dJHaHItCiMqMQlTVBojV5FUQFuia5EcSCYJpOryoh94UsRPOswv0yE2ylw0sUiYiA8befWUsRlWPJu7_RcOzsC7o60p-4znLiT1sta9KYlvA2bF5tDr8ale-RRMyUeua-4dNsAB2JdZEIKuK2GiCdiug1Km6HWW2vsRpVcal_r4C8EWzGlIUJyJ8Y07DOSQDrgHkH-JETQiTKXCEjC79kST9gk2YBoVuVxaCz83EiaA16ol3YkTCAJ6Y-w diff --git a/tests/server-2016.json b/tests/server-2016.json new file mode 100644 index 0000000..74698c5 --- /dev/null +++ b/tests/server-2016.json @@ -0,0 +1,5 @@ +{ + "app":{ + "cfengine":"adobe@2016" + } +} diff --git a/tests/server-2018.json b/tests/server-2018.json new file mode 100644 index 0000000..a45e22c --- /dev/null +++ b/tests/server-2018.json @@ -0,0 +1,5 @@ +{ + "app":{ + "cfengine":"adobe@2018" + } +} diff --git a/tests/server.json b/tests/server.json new file mode 100644 index 0000000..587d2b7 --- /dev/null +++ b/tests/server.json @@ -0,0 +1,5 @@ +{ + "app":{ + "cfengine":"lucee@5" + } +} diff --git a/tests/specs/encodingUtilsSpec.cfc b/tests/specs/encodingUtilsSpec.cfc new file mode 100644 index 0000000..fee8a98 --- /dev/null +++ b/tests/specs/encodingUtilsSpec.cfc @@ -0,0 +1,156 @@ +component extends=testbox.system.BaseSpec { + + function run() { + describe( 'The encodingUtils component', function() { + var encodingUtils = new models.encodingUtils(); + + it( 'can convert dates to UNIX timestamps', function() { + var dt = parseDateTime( '2019-09-01T00:00:00Z' ); + var ut = encodingUtils.convertDateToUnixTimestamp( dt ); + expect( ut ).toBe( 1567296000 ); + } ); + + it( 'can convert UNIX timestamps to dates', function() { + var dt = encodingUtils.convertUnixTimestampToDate( 1567296000 ); + expect( dt ).toBe( parseDateTime( '2019-09-01T00:00:00Z' ) ); + } ); + + it( 'can convert binary data to Base64 URL encoding', function() { + var binaryData = binaryDecode( 'gIecWZe5dS6rVhI7MXgoxeZ/IcUjJ5qZ9+2GUuR3ejk=', 'base64' ); + var encoded = encodingUtils.binaryToBase64Url( binaryData ); + expect( encoded ).toBe( 'gIecWZe5dS6rVhI7MXgoxeZ_IcUjJ5qZ9-2GUuR3ejk' ); + } ); + + it( 'can convert Base64 URL encoded data to binary', function() { + var encoded = 'gIecWZe5dS6rVhI7MXgoxeZ_IcUjJ5qZ9-2GUuR3ejk'; + var converted = encodingUtils.base64UrlToBinary( encoded ); + var binaryData = binaryDecode( 'gIecWZe5dS6rVhI7MXgoxeZ/IcUjJ5qZ9+2GUuR3ejk=', 'base64' ); + expect( converted ).toBe( binaryData ); + } ); + + it( 'can convert an EC P1363 signature to an ASN.1 DER signature', function() { + var P1363Data = binaryDecode( + 'tyh+VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP/3cYHBw7AhHale5wky6+sVA==', + 'base64' + ); + var DERData = binaryDecode( + 'MEYCIQC3KH5V+7MjELIZgOWQEDsN/KOuqZIe7qlDaGhm4WpRIgIhAM81jY3SakdveeTkrXsdY//dxgcHDsCEdqV7nCTLr6xU', + 'base64' + ); + expect( encodingUtils.convertP1363ToDER( P1363Data ) ).toBe( DERData ); + } ); + + it( 'can convert an EC ASN.1 DER signature to an P1363 signature', function() { + var P1363Data = binaryDecode( + 'tyh+VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP/3cYHBw7AhHale5wky6+sVA==', + 'base64' + ); + var DERData = binaryDecode( + 'MEYCIQC3KH5V+7MjELIZgOWQEDsN/KOuqZIe7qlDaGhm4WpRIgIhAM81jY3SakdveeTkrXsdY//dxgcHDsCEdqV7nCTLr6xU', + 'base64' + ); + expect( encodingUtils.convertDERtoP1363( DERData, 'ES256' ) ).toBe( P1363Data ); + } ); + + describe( 'the parsePEMEncodedKey() method', function() { + it( 'throws a jwtcfml.InvalidPrivateKey exception when given a non PKCS8 format RSA or EC private key', function() { + var rsaKey = fileRead( expandPath( '/sampleKeys/unsupported/sampleRSA.key' ) ); + expect( function() { + encodingUtils.parsePEMEncodedKey( rsaKey ); + } ).toThrow( type = 'jwtcfml.InvalidPrivateKey' ); + + var ecKey = fileRead( expandPath( '/sampleKeys/unsupported/sampleEC.key' ) ); + expect( function() { + encodingUtils.parsePEMEncodedKey( ecKey ); + } ).toThrow( type = 'jwtcfml.InvalidPrivateKey' ); + + var ecKeyWithParams = fileRead( expandPath( '/sampleKeys/unsupported/sampleECWithParams.key' ) ); + expect( function() { + encodingUtils.parsePEMEncodedKey( ecKeyWithParams ); + } ).toThrow( type = 'jwtcfml.InvalidPrivateKey' ); + } ); + + it( 'can parse an RSA private key in PKCS8 Format', function() { + var rsaKey = fileRead( expandPath( '/sampleKeys/sampleRSA.key' ) ); + var key = encodingUtils.parsePEMEncodedKey( rsaKey ); + expect( key.getAlgorithm() ).toBe( 'RSA' ); + expect( key.getFormat() ).toBe( 'PKCS##8' ); + } ); + + it( 'can parse an RSA public key', function() { + var rsaKey = fileRead( expandPath( '/sampleKeys/sampleRSA.pub' ) ); + var key = encodingUtils.parsePEMEncodedKey( rsaKey ); + expect( key.getAlgorithm() ).toBe( 'RSA' ); + expect( key.getFormat() ).toBe( 'X.509' ); + } ); + + it( 'can parse an RSA certificate', function() { + var rsaCert = fileRead( expandPath( '/sampleKeys/sampleRSA.crt' ) ); + var key = encodingUtils.parsePEMEncodedKey( rsaCert ); + expect( key.getAlgorithm() ).toBe( 'RSA' ); + expect( key.getFormat() ).toBe( 'X.509' ); + } ); + + it( 'can parse an EC private key in PKCS8 Format', function() { + var ecKey = fileRead( expandPath( '/sampleKeys/sampleEC.key' ) ); + var key = encodingUtils.parsePEMEncodedKey( ecKey ); + expect( key.getAlgorithm() ).toBe( 'EC' ); + expect( key.getFormat() ).toBe( 'PKCS##8' ); + } ); + + it( 'can parse an EC public key', function() { + var ecKey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) ); + var key = encodingUtils.parsePEMEncodedKey( ecKey ); + expect( key.getAlgorithm() ).toBe( 'EC' ); + expect( key.getFormat() ).toBe( 'X.509' ); + } ); + + it( 'can parse an EC certificate', function() { + var ecCert = fileRead( expandPath( '/sampleKeys/sampleEC.crt' ) ); + var key = encodingUtils.parsePEMEncodedKey( ecCert ); + expect( key.getAlgorithm() ).toBe( 'EC' ); + expect( key.getFormat() ).toBe( 'X.509' ); + } ); + } ); + + describe( 'the parseJWK() method', function() { + it( 'can parse an RSA public key', function() { + var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPublic.json' ) ) ); + var key = encodingUtils.parseJWK( jwk ); + expect( key.getAlgorithm() ).toBe( 'RSA' ); + expect( key.getFormat() ).toBe( 'X.509' ); + } ); + + + it( 'can parse an RSA private key', function() { + var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPrivate.json' ) ) ); + var key = encodingUtils.parseJWK( jwk ); + expect( key.getAlgorithm() ).toBe( 'RSA' ); + expect( key.getFormat() ).toBe( 'PKCS##8' ); + + jwk = jwk.filter( function( k, v ) { + return arrayFind( [ 'kty', 'n', 'd' ], k ); + } ); + var key = encodingUtils.parseJWK( jwk ); + expect( key.getAlgorithm() ).toBe( 'RSA' ); + expect( key.getFormat() ).toBe( 'PKCS##8' ); + } ); + + it( 'can parse an EC public key', function() { + var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPublic.json' ) ) ); + var key = encodingUtils.parseJWK( jwk ); + expect( key.getAlgorithm() ).toBe( 'EC' ); + expect( key.getFormat() ).toBe( 'X.509' ); + } ); + + it( 'can parse an EC private key', function() { + var jwk = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPrivate.json' ) ) ); + var key = encodingUtils.parseJWK( jwk ); + expect( key.getAlgorithm() ).toBe( 'EC' ); + expect( key.getFormat() ).toBe( 'PKCS##8' ); + } ); + } ); + } ); + } + +} diff --git a/tests/specs/jwtSpec.cfc b/tests/specs/jwtSpec.cfc new file mode 100644 index 0000000..50746f9 --- /dev/null +++ b/tests/specs/jwtSpec.cfc @@ -0,0 +1,374 @@ +component extends=testbox.system.BaseSpec { + + function run() { + describe( 'The jwt component', function() { + var jwt = new models.jwt(); + + it( 'can return the unverified header', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'; + var header = jwt.getHeader( token ); + expect( header ).toBe( { + 'alg': 'HS256', + 'typ': 'JWT' + } ); + } ); + + describe( 'The encode() method', function() { + it( 'throws an error if the algorithm specified is not in the algorithm array', function() { + var key = 'secret'; + expect( function() { + payload = jwt.encode( { }, key, 'RS' ); + } ).toThrow( 'jwtcfml.InvalidAlgorithm' ); + } ); + + it( 'supports HS algorithms', function() { + var payload = { + 'name': 'John Doe' + }; + var key = 'secret'; + var token = jwt.encode( payload, key, 'HS256' ); + var expectedToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.GQIdMj0gO4DCPcon_oRn1nFMjfGzA4sOPRIIhRRorLs'; + expect( token ).toBe( expectedToken ); + } ); + + it( 'supports RS algorithms', function() { + var payload = { + 'name': 'John Doe' + }; + var expectedToken = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim(); + + var key = fileRead( expandPath( '/sampleKeys/sampleRSA.key' ) ); + var token = jwt.encode( payload, key, 'RS512' ); + expect( token ).toBe( expectedToken ); + + var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPrivate.json' ) ) ); + var token = jwt.encode( payload, key, 'RS512' ); + expect( token ).toBe( expectedToken ); + } ); + + it( 'supports ES algorithms', function() { + var payload = { + 'name': 'John Doe' + }; + + var key = fileRead( expandPath( '/sampleKeys/sampleEC.key' ) ); + var token = jwt.encode( payload, key, 'ES256' ); + + // EC signatures change every time so just decode and verify that + var publickey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) ); + + var payload = jwt.decode( token, publickey, 'ES256' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPrivate.json' ) ) ); + var token = jwt.encode( payload, key, 'ES256' ); + var payload = jwt.decode( token, publickey, 'ES256' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'supports using Java private key classes', function() { + var payload = { + 'name': 'John Doe' + }; + + var key = jwt.parsePEMEncodedKey( fileRead( expandPath( '/sampleKeys/sampleEC.key' ) ) ); + var token = jwt.encode( payload, key, 'ES256' ); + + // EC signatures change every time so just decode and verify that + var publickey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) ); + var payload = jwt.decode( token, publickey, 'ES256' ); + + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var key = jwt.parseJWK( + deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPrivate.json' ) ) ) + ); + var token = jwt.encode( payload, key, 'ES256' ); + + // EC signatures change every time so just decode and verify that + var publickey = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) ); + var payload = jwt.decode( token, publickey, 'ES256' ); + + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'supports adding extra headers', function() { + var payload = { + 'name': 'John Doe' + }; + var key = 'secret'; + var token = jwt.encode( + payload, + key, + 'HS256', + { + 'kid': '123abc' + } + ); + var header = jwt.getHeader( token ); + expect( header ).toBe( { + 'typ': 'JWT', + 'alg': 'HS256', + 'kid': '123abc' + } ); + } ); + + it( 'supports converting CFML dates to UNIX timestamps for "iat", "exp", and "nbf" claims', function() { + var encodingUtils = new models.encodingUtils(); + var ts = now(); + var ut = encodingUtils.convertDateToUnixTimestamp( ts ); + + var payload = { + 'iat': ts, + 'exp': ts, + 'nbf': ts + }; + var token = jwt.encode( payload, 'secret', 'HS256' ); + + var decodedPayload = deserializeJSON( + charsetEncode( encodingUtils.base64UrlToBinary( listGetAt( token, 2, '.' ) ), 'utf-8' ) + ); + + expect( decodedPayload ).toBe( { + 'iat': ut, + 'exp': ut, + 'nbf': ut + } ); + } ); + } ); + + describe( 'The decode() method', function() { + it( 'throws an error if the algorithm specified in header is not in the algorithm array', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'; + var key = 'secret'; + expect( function() { + payload = jwt.decode( token, key, 'RS256' ); + } ).toThrow( 'jwtcfml.InvalidAlgorithm' ); + } ); + + it( 'throws an error if the signature is invalid', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'; + var key = 'secret2'; + expect( function() { + payload = jwt.decode( token, key, 'HS256' ); + } ).toThrow( 'jwtcfml.InvalidSignature' ); + } ); + + + it( 'allows verification to be bypassed', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'; + var key = 'secret2'; + expect( function() { + payload = jwt.decode( + token = token, + key = key, + algorithms = 'HS256', + verify = false + ); + } ).notToThrow( 'jwtcfml.InvalidSignature' ); + } ); + + it( 'supports HS algorithms', function() { + var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.GQIdMj0gO4DCPcon_oRn1nFMjfGzA4sOPRIIhRRorLs'; + var key = 'secret'; + var payload = jwt.decode( token, key, 'HS256' ); + + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'supports RS algorithms', function() { + var token = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim(); + + var key = fileRead( expandPath( '/sampleKeys/sampleRSA.pub' ) ); + var payload = jwt.decode( token, key, 'RS512' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleRSAPublic.json' ) ) ); + var payload = jwt.decode( token, key, 'RS512' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'supports ES algorithms', function() { + var token = fileRead( expandPath( '/sampleTokens/ES256Token.txt' ) ).trim(); + + var key = fileRead( expandPath( '/sampleKeys/sampleEC.pub' ) ); + var payload = jwt.decode( token, key, 'ES256' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPublic.json' ) ) ); + var payload = jwt.decode( token, key, 'ES256' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var token = fileRead( expandPath( '/sampleTokens/ES384Token.txt' ) ).trim(); + + var key = fileRead( expandPath( '/sampleKeys/sampleEC384.pub' ) ); + var payload = jwt.decode( token, key, 'ES384' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var key = deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleEC384Public.json' ) ) ); + var payload = jwt.decode( token, key, 'ES384' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'supports using PEM certificates when decoding', function() { + var token = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim(); + var key = fileRead( expandPath( '/sampleKeys/sampleRSA.crt' ) ); + var payload = jwt.decode( token, key, 'RS512' ); + + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var token = fileRead( expandPath( '/sampleTokens/ES256Token.txt' ) ).trim(); + var key = fileRead( expandPath( '/sampleKeys/sampleEC.crt' ) ); + var payload = jwt.decode( token, key, 'ES256' ); + + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'supports using java public key classes when decoding', function() { + var token = fileRead( expandPath( '/sampleTokens/RS512Token.txt' ) ).trim(); + var key = jwt.parsePEMEncodedKey( fileRead( expandPath( '/sampleKeys/sampleRSA.crt' ) ) ); + var payload = jwt.decode( token, key, 'RS512' ); + + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var token = fileRead( expandPath( '/sampleTokens/ES256Token.txt' ) ).trim(); + + var key = jwt.parsePEMEncodedKey( fileRead( expandPath( '/sampleKeys/sampleEC.crt' ) ) ); + var payload = jwt.decode( token, key, 'ES256' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + + var key = jwt.parseJWK( + deserializeJSON( fileRead( expandPath( '/sampleJWK/sampleECPublic.json' ) ) ) + ); + var payload = jwt.decode( token, key, 'ES256' ); + expect( payload ).toBe( { + 'name': 'John Doe' + } ); + } ); + + it( 'verifies the "exp" claim', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjU0MjMwMDB9.0F2ysJpLbaf3hFAQ6zwZoQ1L2pBYgzdOHOdz0he5GWo'; + var key = 'secret'; + expect( function() { + jwt.decode( token, key, 'HS256' ); + } ).toThrow( 'jwtcfml.ExpiredSignature' ); + } ); + + it( 'verifies the "nbf" claim', function() { + var payload = { + nbf: dateAdd( 'h', 1, now() ) + }; + var token = jwt.encode( payload, 'secret', 'HS256' ); + expect( function() { + jwt.decode( token, 'secret', 'HS256' ); + } ).toThrow( 'jwtcfml.NotBeforeException' ); + } ); + + it( 'verifies the "iss" claim', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0VEVTVCJ9.WT1ydOldEYxXVxM91LHwk4gW1fwQMS9zJ3n9SbUbOwE'; + var key = 'secret'; + expect( function() { + jwt.decode( + token, + key, + 'HS256', + { + 'iss': 'testtest' + } + ); + } ).toThrow( 'jwtcfml.InvalidIssuer' ); + expect( function() { + jwt.decode( + token, + key, + 'HS256', + { + 'iss': 'testTEST' + } + ); + } ).notToThrow(); + } ); + + it( 'verifies the "aud" claim', function() { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhIn0.F5RHqgQAiCrHsrVEJO4ZQjQ5CY4L3AKH1nClXHa0JeU'; + var key = 'secret'; + expect( function() { + jwt.decode( + token, + key, + 'HS256', + { + 'aud': 'b' + } + ); + } ).toThrow( 'jwtcfml.InvalidAudience' ); + expect( function() { + jwt.decode( + token, + key, + 'HS256', + { + 'aud': [ 'a', 'b' ] + } + ); + } ).notToThrow(); + } ); + + it( 'supports converting UNIX timestamps to CFML dates for "iat", "exp", and "nbf" claims', function() { + var encodingUtils = new models.encodingUtils(); + var ts = now(); + var ut = encodingUtils.convertDateToUnixTimestamp( ts ); + + var payload = { + 'iat': ut, + 'exp': ut, + 'nbf': ut + }; + var token = jwt.encode( payload, 'secret', 'HS256' ); + var decodedPayload = jwt.decode( + token, + 'secret', + 'HS256', + { + exp: false + } + ); + for ( var key in payload ) { + expect( decodedPayload[ key ] ).toBe( ts ); + } + } ); + } ); + } ); + } + +}