Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HMAC signature support for JWT #307

Merged
merged 1 commit into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion ballerina/jwt_commons.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

Expand Down
45 changes: 40 additions & 5 deletions ballerina/jwt_issuer.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 {|
Expand All @@ -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
Expand Down Expand Up @@ -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 = <crypto:KeyStore> config?.keyStore;
string keyAlias = <string> config?.keyAlias;
Expand Down Expand Up @@ -111,23 +113,56 @@ 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 => {
byte[]|crypto:Error signature = crypto:signRsaSha384(jwtAssertion.toBytes(), privateKey);
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 => {
byte[]|crypto:Error signature = crypto:signRsaSha512(jwtAssertion.toBytes(), privateKey);
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);
}
}
_ => {
Expand Down
57 changes: 51 additions & 6 deletions ballerina/jwt_validator.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -205,6 +207,12 @@ isolated function parseHeader(map<json> 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() + "'.");
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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.");
}
Expand All @@ -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.");
}
Expand All @@ -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.");
}
}
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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? {
Expand Down
75 changes: 75 additions & 0 deletions ballerina/tests/jwt_issuer_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading