From 5f53d4253ade0064ee0c6eb1a284e86eaa9828ea Mon Sep 17 00:00:00 2001 From: Chanaka Lakmal Date: Mon, 2 Aug 2021 13:50:47 +0530 Subject: [PATCH] Add HMAC signature support for JWT --- ballerina/jwt_commons.bal | 11 +++- ballerina/jwt_issuer.bal | 45 +++++++++++-- ballerina/jwt_validator.bal | 57 +++++++++++++++-- ballerina/tests/jwt_issuer_test.bal | 75 ++++++++++++++++++++++ ballerina/tests/jwt_validator_test.bal | 87 ++++++++++++++++++++++++++ ballerina/tests/test_utils.bal | 58 +++++++++++++++++ changelog.md | 5 ++ 7 files changed, 326 insertions(+), 12 deletions(-) diff --git a/ballerina/jwt_commons.bal b/ballerina/jwt_commons.bal index 265f2f8a..eca1d21d 100644 --- a/ballerina/jwt_commons.bal +++ b/ballerina/jwt_commons.bal @@ -17,7 +17,7 @@ import ballerina/jballerina.java; # Represents the cryptographic algorithms used to secure the JWS. -public type SigningAlgorithm RS256|RS384|RS512|NONE; +public type SigningAlgorithm RS256|RS384|RS512|HS256|HS384|HS512|NONE; # The `RSA-SHA256` algorithm. public const RS256 = "RS256"; @@ -28,6 +28,15 @@ public const RS384 = "RS384"; # The `RSA-SHA512` algorithm. public const RS512 = "RS512"; +# The `HMAC-SHA256` algorithm. +public const HS256 = "HS256"; + +# The `HMAC-SHA384` algorithm. +public const HS384 = "HS384"; + +# The `HMAC-SHA512` algorithm. +public const HS512 = "HS512"; + # Unsecured JWS (no signing). public const NONE = "none"; diff --git a/ballerina/jwt_issuer.bal b/ballerina/jwt_issuer.bal index e72e59f6..17f4bac7 100644 --- a/ballerina/jwt_issuer.bal +++ b/ballerina/jwt_issuer.bal @@ -41,7 +41,7 @@ public type IssuerConfig record {| # Represents JWT signature configurations. # # + algorithm - Cryptographic signing algorithm for JWS -# + config - KeyStore configurations or private key configurations +# + config - KeyStore configurations, private key configurations or shared key configurations public type IssuerSignatureConfig record {| SigningAlgorithm algorithm = RS256; record {| @@ -51,7 +51,7 @@ public type IssuerSignatureConfig record {| |} | record {| string keyFile; string keyPassword?; - |} config?; + |} | string config?; |}; # Issues a JWT based on the provided configurations. JWT will be signed (JWS) if `crypto:KeyStore` information is @@ -81,6 +81,8 @@ public isolated function issue(IssuerConfig issuerConfig) returns string|Error { var config = signatureConfig?.config; if (config is ()) { return prepareError("Signing JWT requires keystore information or private key information."); + } else if (config is string) { + return hmacJwtAssertion(jwtAssertion, algorithm, config); } else if (config?.keyStore is crypto:KeyStore) { crypto:KeyStore keyStore = config?.keyStore; string keyAlias = config?.keyAlias; @@ -111,7 +113,7 @@ isolated function signJwtAssertion(string jwtAssertion, SigningAlgorithm alg, cr if (signature is byte[]) { return (jwtAssertion + "." + encodeBase64Url(signature)); } else { - return prepareError("Private key signing failed for SHA256 algorithm.", signature); + return prepareError("RSA private key signing failed for SHA256 algorithm.", signature); } } RS384 => { @@ -119,7 +121,7 @@ isolated function signJwtAssertion(string jwtAssertion, SigningAlgorithm alg, cr if (signature is byte[]) { return (jwtAssertion + "." + encodeBase64Url(signature)); } else { - return prepareError("Private key signing failed for SHA384 algorithm.", signature); + return prepareError("RSA private key signing failed for SHA384 algorithm.", signature); } } RS512 => { @@ -127,7 +129,40 @@ isolated function signJwtAssertion(string jwtAssertion, SigningAlgorithm alg, cr if (signature is byte[]) { return (jwtAssertion + "." + encodeBase64Url(signature)); } else { - return prepareError("Private key signing failed for SHA512 algorithm.", signature); + return prepareError("RSA private key signing failed for SHA512 algorithm.", signature); + } + } + _ => { + return prepareError("Unsupported signing algorithm '" + alg.toString() + "'."); + } + } +} + +isolated function hmacJwtAssertion(string jwtAssertion, SigningAlgorithm alg, string secret) + returns string|Error { + match (alg) { + HS256 => { + byte[]|crypto:Error signature = crypto:hmacSha256(jwtAssertion.toBytes(), secret.toBytes()); + if (signature is byte[]) { + return (jwtAssertion + "." + encodeBase64Url(signature)); + } else { + return prepareError("HMAC secret key signing failed for SHA256 algorithm.", signature); + } + } + HS384 => { + byte[]|crypto:Error signature = crypto:hmacSha384(jwtAssertion.toBytes(), secret.toBytes()); + if (signature is byte[]) { + return (jwtAssertion + "." + encodeBase64Url(signature)); + } else { + return prepareError("HMAC secret key signing failed for SHA384 algorithm.", signature); + } + } + HS512 => { + byte[]|crypto:Error signature = crypto:hmacSha512(jwtAssertion.toBytes(), secret.toBytes()); + if (signature is byte[]) { + return (jwtAssertion + "." + encodeBase64Url(signature)); + } else { + return prepareError("HMAC secret key signing failed for SHA512 algorithm.", signature); } } _ => { diff --git a/ballerina/jwt_validator.bal b/ballerina/jwt_validator.bal index d1eff08a..9f2b6584 100644 --- a/ballerina/jwt_validator.bal +++ b/ballerina/jwt_validator.bal @@ -51,6 +51,7 @@ public type ValidatorConfig record { # + jwksConfig - JWKS configurations # + certFile - Public certificate file # + trustStoreConfig - JWT TrustStore configurations +# + secret - HMAC secret configuration public type ValidatorSignatureConfig record {| record {| string url; @@ -62,6 +63,7 @@ public type ValidatorSignatureConfig record {| crypto:TrustStore trustStore; string certAlias; |} trustStoreConfig?; + string secret?; |}; # Represents the configurations of the client used to call the JWKS endpoint. @@ -205,6 +207,12 @@ isolated function parseHeader(map headerMap) returns Header|Error { header.alg = RS384; } else if (headerMap[key] == "RS512") { header.alg = RS512; + } else if (headerMap[key] == "HS256") { + header.alg = HS256; + } else if (headerMap[key] == "HS384") { + header.alg = HS384; + } else if (headerMap[key] == "HS512") { + header.alg = HS512; } else { return prepareError("Unsupported signing algorithm '" + headerMap[key].toString() + "'."); } @@ -306,6 +314,7 @@ isolated function validateSignature(string jwt, Header header, Payload payload, var jwksConfig = validatorSignatureConfig?.jwksConfig; string? certFile = validatorSignatureConfig?.certFile; var trustStoreConfig = validatorSignatureConfig?.trustStoreConfig; + string? secret = validatorSignatureConfig?.secret; if !(jwksConfig is ()) { string? kid = header?.kid; if (kid is string) { @@ -316,7 +325,7 @@ isolated function validateSignature(string jwt, Header header, Payload payload, return prepareError("No JWK found for kid '" + kid + "'."); } crypto:PublicKey publicKey = check getPublicKeyByJwks(jwk); - boolean signatureValidation = check assertSignature(alg, assertion, signature, publicKey); + boolean signatureValidation = check assertRsaSignature(alg, assertion, signature, publicKey); if (!signatureValidation) { return prepareError("JWT signature validation with JWKS configurations has failed."); } @@ -329,7 +338,7 @@ isolated function validateSignature(string jwt, Header header, Payload payload, if (!validateCertificate(publicKey)) { return prepareError("Public key certificate validity period has passed."); } - boolean signatureValidation = check assertSignature(alg, assertion, signature, publicKey); + boolean signatureValidation = check assertRsaSignature(alg, assertion, signature, publicKey); if (!signatureValidation) { return prepareError("JWT signature validation with public key configurations has failed."); } @@ -344,13 +353,18 @@ isolated function validateSignature(string jwt, Header header, Payload payload, if (!validateCertificate(publicKey)) { return prepareError("Public key certificate validity period has passed."); } - boolean signatureValidation = check assertSignature(alg, assertion, signature, publicKey); + boolean signatureValidation = check assertRsaSignature(alg, assertion, signature, publicKey); if (!signatureValidation) { return prepareError("JWT signature validation with TrustStore configurations has failed."); } } else { return prepareError("Failed to decode public key.", publicKey); } + } else if !(secret is ()) { + boolean signatureValidation = check assertHmacSignature(alg, assertion, signature, secret); + if (!signatureValidation) { + return prepareError("JWT signature validation with shared secret has failed."); + } } } } @@ -474,8 +488,8 @@ isolated function getJwksResponse(string url, ClientConfiguration clientConfig) 'class: "io.ballerina.stdlib.jwt.JwksClient" } external; -isolated function assertSignature(SigningAlgorithm alg, byte[] assertion, byte[] signaturePart, - crypto:PublicKey publicKey) returns boolean|Error { +isolated function assertRsaSignature(SigningAlgorithm alg, byte[] assertion, byte[] signaturePart, + crypto:PublicKey publicKey) returns boolean|Error { match (alg) { RS256 => { boolean|crypto:Error result = crypto:verifyRsaSha256Signature(assertion, signaturePart, publicKey); @@ -502,7 +516,38 @@ isolated function assertSignature(SigningAlgorithm alg, byte[] assertion, byte[] } } } - return prepareError("Unsupported JWS algorithm '" + alg.toString() + "'."); + return prepareError("Unsupported RSA algorithm '" + alg.toString() + "'."); +} + +isolated function assertHmacSignature(SigningAlgorithm alg, byte[] assertion, byte[] signaturePart, + string secret) returns boolean|Error { + match (alg) { + HS256 => { + byte[]|crypto:Error signature = crypto:hmacSha256(assertion, secret.toBytes()); + if (signature is byte[]) { + return signature == signaturePart; + } else { + return prepareError("HMAC secret key validation failed for SHA256 algorithm.", signature); + } + } + HS384 => { + byte[]|crypto:Error signature = crypto:hmacSha384(assertion, secret.toBytes()); + if (signature is byte[]) { + return signature == signaturePart; + } else { + return prepareError("HMAC secret key validation failed for SHA384 algorithm.", signature); + } + } + HS512 => { + byte[]|crypto:Error signature = crypto:hmacSha512(assertion, secret.toBytes()); + if (signature is byte[]) { + return signature == signaturePart; + } else { + return prepareError("HMAC secret key validation failed for SHA512 algorithm.", signature); + } + } + } + return prepareError("Unsupported HMAC algorithm '" + alg.toString() + "'."); } isolated function validateUsername(Payload payload, string usernameConfig) returns Error? { diff --git a/ballerina/tests/jwt_issuer_test.bal b/ballerina/tests/jwt_issuer_test.bal index 73840ba5..c22bf527 100644 --- a/ballerina/tests/jwt_issuer_test.bal +++ b/ballerina/tests/jwt_issuer_test.bal @@ -311,6 +311,81 @@ isolated function testIssueJwtWithSigningAlgorithmRS512() { } } +@test:Config {} +isolated function testIssueJwtWithSigningAlgorithmHS256() { + IssuerConfig issuerConfig = { + username: "John", + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + expTime: 600, + signatureConfig: { + algorithm: HS256, + config: "s3cr3t" + } + }; + + string|Error result = issue(issuerConfig); + if (result is string) { + test:assertTrue(result.startsWith("eyJhbGciOiJIUzI1NiIsICJ0eXAiOiJKV1QifQ.")); + string header = "{\"alg\":\"HS256\", \"typ\":\"JWT\"}"; + string payload = "{\"iss\":\"wso2\", \"sub\":\"John\", \"aud\":[\"ballerina\", \"ballerinaSamples\"]"; + assertDecodedJwt(result, header, payload); + } else { + string? errMsg = result.message(); + test:assertFail(msg = errMsg is string ? errMsg : "Error in generated JWT."); + } +} + +@test:Config {} +isolated function testIssueJwtWithSigningAlgorithmHS384() { + IssuerConfig issuerConfig = { + username: "John", + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + expTime: 600, + signatureConfig: { + algorithm: HS384, + config: "s3cr3t" + } + }; + + string|Error result = issue(issuerConfig); + if (result is string) { + test:assertTrue(result.startsWith("eyJhbGciOiJIUzM4NCIsICJ0eXAiOiJKV1QifQ.")); + string header = "{\"alg\":\"HS384\", \"typ\":\"JWT\"}"; + string payload = "{\"iss\":\"wso2\", \"sub\":\"John\", \"aud\":[\"ballerina\", \"ballerinaSamples\"]"; + assertDecodedJwt(result, header, payload); + } else { + string? errMsg = result.message(); + test:assertFail(msg = errMsg is string ? errMsg : "Error in generated JWT."); + } +} + +@test:Config {} +isolated function testIssueJwtWithSigningAlgorithmHS512() { + IssuerConfig issuerConfig = { + username: "John", + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + expTime: 600, + signatureConfig: { + algorithm: HS512, + config: "s3cr3t" + } + }; + + string|Error result = issue(issuerConfig); + if (result is string) { + test:assertTrue(result.startsWith("eyJhbGciOiJIUzUxMiIsICJ0eXAiOiJKV1QifQ.")); + string header = "{\"alg\":\"HS512\", \"typ\":\"JWT\"}"; + string payload = "{\"iss\":\"wso2\", \"sub\":\"John\", \"aud\":[\"ballerina\", \"ballerinaSamples\"]"; + assertDecodedJwt(result, header, payload); + } else { + string? errMsg = result.message(); + test:assertFail(msg = errMsg is string ? errMsg : "Error in generated JWT."); + } +} + @test:Config {} isolated function testIssueJwtWithoutSigningKeyInformation() { IssuerConfig issuerConfig = { diff --git a/ballerina/tests/jwt_validator_test.bal b/ballerina/tests/jwt_validator_test.bal index 7c034e4b..cafe1731 100644 --- a/ballerina/tests/jwt_validator_test.bal +++ b/ballerina/tests/jwt_validator_test.bal @@ -619,3 +619,90 @@ isolated function testValidateJwtSignatureWithInvalidPublicCert() { test:assertFail(msg = "Error in validating JWT."); } } + +@test:Config {} +isolated function testValidateJwtSignatureWithHS256SharedSecret() { + ValidatorConfig validatorConfig = { + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + clockSkew: 60, + signatureConfig: { + secret: "s3cr3t" + } + }; + Payload|Error result = validate(JWT6, validatorConfig); + if (result is Error) { + string? errMsg = result.message(); + test:assertFail(msg = errMsg is string ? errMsg : "Error in validating JWT."); + } +} + +@test:Config {} +isolated function testValidateJwtSignatureWithHS384SharedSecret() { + ValidatorConfig validatorConfig = { + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + clockSkew: 60, + signatureConfig: { + secret: "s3cr3t" + } + }; + Payload|Error result = validate(JWT7, validatorConfig); + if (result is Error) { + string? errMsg = result.message(); + test:assertFail(msg = errMsg is string ? errMsg : "Error in validating JWT."); + } +} + +@test:Config {} +isolated function testValidateJwtSignatureWithHS512SharedSecret() { + ValidatorConfig validatorConfig = { + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + clockSkew: 60, + signatureConfig: { + secret: "s3cr3t" + } + }; + Payload|Error result = validate(JWT8, validatorConfig); + if (result is Error) { + string? errMsg = result.message(); + test:assertFail(msg = errMsg is string ? errMsg : "Error in validating JWT."); + } +} + +@test:Config {} +isolated function testValidateJwtSignatureWithInvalidSharedSecret() { + ValidatorConfig validatorConfig = { + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + clockSkew: 60, + signatureConfig: { + secret: "!nva1id" + } + }; + Payload|Error result = validate(JWT6, validatorConfig); + if (result is Error) { + assertContains(result, "JWT signature validation with shared secret has failed."); + } else { + test:assertFail(msg = "Error in validating JWT."); + } +} + +@test:Config {} +isolated function testValidateJwtSignatureWithInvalidAlgorithm() { + ValidatorConfig validatorConfig = { + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + clockSkew: 60, + signatureConfig: { + certFile: PUBLIC_CERT_PATH + } + }; + Payload|Error result = validate(JWT6, validatorConfig); + if (result is Error) { + assertContains(result, "Unsupported RSA algorithm 'HS256'."); + } else { + test:assertFail(msg = "Error in validating JWT."); + } +} diff --git a/ballerina/tests/test_utils.bal b/ballerina/tests/test_utils.bal index e7ec2da1..538e76b5 100644 --- a/ballerina/tests/test_utils.bal +++ b/ballerina/tests/test_utils.bal @@ -137,6 +137,64 @@ const string JWT5 = "eyJhbGciOiJSUzI1NiIsICJ0eXAiOiJKV1QiLCAia2lkIjoiNWEwYjc1NC0 "t4flzCBsTGWC7XZaFnwT4mUlX7WpTOgv1Nsq5GVLszvsnzs6BE__Mvr4zl5pdChVbkMXX3US6fYguK268XKjzgtpMVxUpL3" + "CrzwQpIRyI-Q"; +// { +// "alg": "HS256", +// "typ": "JWT" +// } +// { +// "iss": "wso2", +// "sub": "John", +// "aud": [ +// "ballerina", +// "ballerinaSamples" +// ], +// "exp": 1943255429, +// "nbf": 1627895429, +// "iat": 1627895429 +// } +const string JWT6 = "eyJhbGciOiJIUzI1NiIsICJ0eXAiOiJKV1QifQ.eyJpc3MiOiJ3c28yIiwgInN1YiI6IkpvaG4iLCAiYXVkIjpbImJhbGxl" + + "cmluYSIsICJiYWxsZXJpbmFTYW1wbGVzIl0sICJleHAiOjE5NDMyNTU0MjksICJuYmYiOjE2Mjc4OTU0MjksICJpYXQiOjE" + + "2Mjc4OTU0Mjl9.BqFvZtKFj4KiVGkSkbXAcG4mpmSnGM0f60GYMw7dj4k"; + +// { +// "alg": "HS384", +// "typ": "JWT" +// } +// { +// "iss": "wso2", +// "sub": "John", +// "aud": [ +// "ballerina", +// "ballerinaSamples" +// ], +// "exp": 1943257494, +// "nbf": 1627897494, +// "iat": 1627897494 +// } +const string JWT7 = "eyJhbGciOiJIUzM4NCIsICJ0eXAiOiJKV1QifQ.eyJpc3MiOiJ3c28yIiwgInN1YiI6IkpvaG4iLCAiYXVkIjpbImJhbGxl" + + "cmluYSIsICJiYWxsZXJpbmFTYW1wbGVzIl0sICJleHAiOjE5NDMyNTc0OTQsICJuYmYiOjE2Mjc4OTc0OTQsICJpYXQiOjE" + + "2Mjc4OTc0OTR9.gwyn7kbaX-AQi0SQQmPoKfazehSsr7XUTnxewKny2qcOOVJrIqlLVyCoyacFlPel"; + +// { +// "alg": "HS512", +// "typ": "JWT" +// } +// { +// "iss": "wso2", +// "sub": "John", +// "aud": [ +// "ballerina", +// "ballerinaSamples" +// ], +// "exp": 1627899452, +// "nbf": 1627898852, +// "iat": 1627898852 +// } +const string JWT8 = "eyJhbGciOiJIUzUxMiIsICJ0eXAiOiJKV1QifQ.eyJpc3MiOiJ3c28yIiwgInN1YiI6IkpvaG4iLCAiYXVkIjpbImJhbGxl" + + "cmluYSIsICJiYWxsZXJpbmFTYW1wbGVzIl0sICJleHAiOjE2Mjc4OTk0NTIsICJuYmYiOjE2Mjc4OTg4NTIsICJpYXQiOjE" + + "2Mjc4OTg4NTJ9.Y-71DT1OnESuDkXmzParDgJ_iJ65DqTGvT3aNj1GVPJaHV8UPpD44O3nAgrXGFXpszWmeMoPz99BmUAHA" + + "azszA"; + // Builds the complete error message by evaluating all the inner causes and asserts the inclusion. isolated function assertContains(error err, string text) { string message = err.message(); diff --git a/changelog.md b/changelog.md index c2c9a3b0..e0eeef53 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added +- [Add HMAC signature support for JWT](/~https://github.com/ballerina-platform/ballerina-standard-library/issues/1645) + +## [1.1.0-beta2] - 2021-07-06 + ### Added - [Improve JWT validation for all the fields of JWT issuer configuration](/~https://github.com/ballerina-platform/ballerina-standard-library/issues/1240)