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;
    }

}
