diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index c79e0fbc..8dae7384 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "jwt" -version = "2.11.0" +version = "2.12.0" authors = ["Ballerina"] keywords = ["security", "authentication", "jwt", "jwk", "jws"] repository = "https://github.com/ballerina-platform/module-ballerina-jwt" @@ -15,5 +15,5 @@ graalvmCompatible = true [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "jwt-native" -version = "2.11.0" -path = "../native/build/libs/jwt-native-2.11.0.jar" +version = "2.12.0" +path = "../native/build/libs/jwt-native-2.12.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index ce9fe75d..035cc78b 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -32,7 +32,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.0" +version = "2.7.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"} @@ -49,6 +49,9 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.value"} ] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] [[package]] org = "ballerina" @@ -61,10 +64,11 @@ modules = [ [[package]] org = "ballerina" name = "jwt" -version = "2.11.0" +version = "2.12.0" dependencies = [ {org = "ballerina", name = "cache"}, {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.int"}, {org = "ballerina", name = "lang.string"}, diff --git a/ballerina/jwt_issuer.bal b/ballerina/jwt_issuer.bal index 6f246e3b..4d05cf7e 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, private key configurations or shared key configurations +# + config - KeyStore configurations, private key configurations, `crypto:PrivateKey` 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?; - |} | string config?; + |} | crypto:PrivateKey | string config?; |}; # Issues a JWT based on the provided configurations. JWT will be signed (JWS) if `crypto:KeyStore` information is @@ -93,6 +93,8 @@ public isolated function issue(IssuerConfig issuerConfig) returns string|Error { } else { return prepareError("Failed to decode private key.", privateKey); } + } else if config is crypto:PrivateKey { + return signJwtAssertion(jwtAssertion, algorithm, config); } else { string keyFile = config?.keyFile; string? keyPassword = config?.keyPassword; diff --git a/ballerina/jwt_validator.bal b/ballerina/jwt_validator.bal index 38e2abab..9356ac62 100644 --- a/ballerina/jwt_validator.bal +++ b/ballerina/jwt_validator.bal @@ -48,7 +48,7 @@ public type ValidatorConfig record { # Represents JWT signature configurations. # # + jwksConfig - JWKS configurations -# + certFile - Public certificate file +# + certFile - Public certificate file path or a `crypto:PublicKey` # + trustStoreConfig - JWT TrustStore configurations # + secret - HMAC secret configuration public type ValidatorSignatureConfig record {| @@ -57,7 +57,7 @@ public type ValidatorSignatureConfig record {| cache:CacheConfig cacheConfig?; ClientConfiguration clientConfig = {}; |} jwksConfig?; - string certFile?; + string|crypto:PublicKey certFile?; record {| crypto:TrustStore trustStore; string certAlias; @@ -311,7 +311,7 @@ isolated function validateSignature(string jwt, Header header, Payload payload, if validatorSignatureConfig is ValidatorSignatureConfig { var jwksConfig = validatorSignatureConfig?.jwksConfig; - string? certFile = validatorSignatureConfig?.certFile; + var certFile = validatorSignatureConfig?.certFile; var trustStoreConfig = validatorSignatureConfig?.trustStoreConfig; string? secret = validatorSignatureConfig?.secret; if jwksConfig !is () { @@ -331,8 +331,13 @@ isolated function validateSignature(string jwt, Header header, Payload payload, } else { return prepareError("Key ID (kid) is not provided in JOSE header."); } - } else if certFile is string { - crypto:PublicKey|crypto:Error publicKey = crypto:decodeRsaPublicKeyFromCertFile(certFile); + } else if certFile !is () { + crypto:PublicKey|crypto:Error publicKey; + if certFile is crypto:PublicKey { + publicKey = certFile; + } else { + publicKey = crypto:decodeRsaPublicKeyFromCertFile(certFile); + } if publicKey is crypto:PublicKey { if !validateCertificate(publicKey) { return prepareError("Public key certificate validity period has passed."); diff --git a/ballerina/tests/jwt_issuer_test.bal b/ballerina/tests/jwt_issuer_test.bal index f3719c48..bfc17858 100644 --- a/ballerina/tests/jwt_issuer_test.bal +++ b/ballerina/tests/jwt_issuer_test.bal @@ -16,6 +16,8 @@ // NOTE: All the tokens/credentials used in this test are dummy tokens/credentials and used only for testing purposes. +import ballerina/crypto; +import ballerina/io; import ballerina/lang.'string; import ballerina/test; @@ -365,6 +367,44 @@ isolated function testIssueJwtWithEncryptedPrivateKey() returns Error? { assertDecodedJwt(result, expectedHeader, expectedPayload); } +@test:Config {} +isolated function testIssueJwtWithCryptoPrivateKey() returns io:Error|crypto:Error|Error? { + byte[] privateKeyContent = check io:fileReadBytes(PRIVATE_KEY_PATH); + crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromContent(privateKeyContent); + IssuerConfig issuerConfig = { + username: "John", + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + expTime: 600, + signatureConfig: { + config: privateKey + } + }; + string result = check issue(issuerConfig); + string expectedHeader = "{\"alg\":\"RS256\", \"typ\":\"JWT\"}"; + string expectedPayload = "{\"iss\":\"wso2\", \"sub\":\"John\", \"aud\":[\"ballerina\", \"ballerinaSamples\"]"; + assertDecodedJwt(result, expectedHeader, expectedPayload); +} + +@test:Config {} +isolated function testIssueJwtWithEncryptedCryptoPrivateKey() returns io:Error|crypto:Error|Error? { + byte[] privateKeyContent = check io:fileReadBytes(ENCRYPTED_PRIVATE_KEY_PATH); + crypto:PrivateKey encryptedPrivateKey = check crypto:decodeRsaPrivateKeyFromContent(privateKeyContent, "ballerina"); + IssuerConfig issuerConfig = { + username: "John", + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + expTime: 600, + signatureConfig: { + config: encryptedPrivateKey + } + }; + string result = check issue(issuerConfig); + string expectedHeader = "{\"alg\":\"RS256\", \"typ\":\"JWT\"}"; + string expectedPayload = "{\"iss\":\"wso2\", \"sub\":\"John\", \"aud\":[\"ballerina\", \"ballerinaSamples\"]"; + assertDecodedJwt(result, expectedHeader, expectedPayload); +} + isolated function assertDecodedJwt(string jwt, string header, string payload) { string[] parts = re `\.`.split(jwt); // check header diff --git a/ballerina/tests/jwt_validator_test.bal b/ballerina/tests/jwt_validator_test.bal index e90306c6..419837d6 100644 --- a/ballerina/tests/jwt_validator_test.bal +++ b/ballerina/tests/jwt_validator_test.bal @@ -16,6 +16,8 @@ // NOTE: All the tokens/credentials used in this test are dummy tokens/credentials and used only for testing purposes. +import ballerina/crypto; +import ballerina/io; import ballerina/test; @test:Config {} @@ -693,6 +695,22 @@ isolated function testValidateJwtSignatureWithPublicCert() returns Error? { test:assertEquals(result?.iss, "wso2"); } +@test:Config {} +isolated function testValidateJwtSignatureWithCryptoPublicKey() returns io:Error|crypto:Error|Error? { + byte[] pubicCertContent = check io:fileReadBytes(PUBLIC_CERT_PATH); + crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromContent(pubicCertContent); + ValidatorConfig validatorConfig = { + issuer: "wso2", + audience: ["ballerina", "ballerinaSamples"], + clockSkew: 60, + signatureConfig: { + certFile: publicKey + } + }; + Payload result = check validate(JWT1, validatorConfig); + test:assertEquals(result?.iss, "wso2"); +} + @test:Config {} isolated function testValidateJwtSignatureWithInvalidPublicCert() { ValidatorConfig validatorConfig = { diff --git a/changelog.md b/changelog.md index 60ffed78..d7235214 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,11 @@ This file contains all the notable changes done to the Ballerina JWT package thr The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added +- [Add support to directly provide `crypto:PrivateKey` and `crypto:PublicKey` in JWT signature configurations](https://github.com/ballerina-platform/ballerina-library/issues/6514) + ## [2.5.0] - 2022-11-29 ### Changed diff --git a/docs/proposals/enable-crypto-key-usage.md b/docs/proposals/enable-crypto-key-usage.md new file mode 100644 index 00000000..25e79fa8 --- /dev/null +++ b/docs/proposals/enable-crypto-key-usage.md @@ -0,0 +1,82 @@ +# Proposal: Enable direct use of `crypto:PrivateKey` and `crypto:PublicKey` in JWT signature configurations + +_Authors_: @ayeshLK \ +_Reviewers_: @shafreenAnfar @daneshk @NipunaRanasinghe @Bhashinee \ +_Created_: 2024/05/08 \ +_Updated_: 2024/05/08 \ +_Issue_: [#6515](https://github.com/ballerina-platform/ballerina-library/issues/6515) + +## Summary + +JWT signature configurations are designed to facilitate the generation and verification of JWT signatures. +Therefore, the JWT package should support direct usage of `crypto:PrivateKey` and `crypto:PublicKey` in +`jwt:IssuerSignatureConfig` and `jwt:ValidatorSignatureConfig` respectively. +## Goals + +- Enable direct use of `crypto:PrivateKey` and `crypto:PublicKey` in JWT signature configurations + +## Motivation + +JWT signature configurations are required configurations to generate the signature portion of a JWT. Typically, +these configurations involve a private key and a public certificate. In Ballerina, these elements are represented as +`crypto:PrivateKey` and `crypto:PublicKey`, respectively. Therefore, JWT signature configurations should allow the +direct usage of `crypto:PrivateKey` and `crypto:PublicKey` within its API. + +## Description + +As mentioned in the Goals section the purpose of this proposal is to enable direct use of `crypto:PrivateKey` +and `crypto:PublicKey` in JWT signature configurations. + +The key functionalities expected from this change are as follows, + +- Allow `crypto:PrivateKey` and `crypto:PublicKey` in `jwt:IssuerSignatureConfig` and `jwt:ValidatorSignatureConfig` respectively. + +### API changes + +Add support for `crypto:PrivateKey` in the `config` field of `jwt:IssuerSignatureConfig` record. + +```ballerina +# Represents JWT signature configurations. +# +# + algorithm - Cryptographic signing algorithm for JWS +# + config - KeyStore configurations, private key configurations or shared key configurations +public type IssuerSignatureConfig record {| + SigningAlgorithm algorithm = RS256; + record {| + crypto:KeyStore keyStore; + string keyAlias; + string keyPassword; + |} | record {| + string keyFile; + string keyPassword?; + |}|crypto:PrivateKey|string config?; +|}; +``` + +Add support for `crypto:PublicKey` in the `certFile` field of `jwt:ValidatorSignatureConfig` record. + +```ballerina +# Represents JWT signature configurations. +# +# + jwksConfig - JWKS configurations +# + certFile - Public certificate file +# + trustStoreConfig - JWT TrustStore configurations +# + secret - HMAC secret configuration +public type ValidatorSignatureConfig record {| + record {| + string url; + cache:CacheConfig cacheConfig?; + ClientConfiguration clientConfig = {}; + |} jwksConfig?; + string|crypto:PublicKey certFile?; + record {| + crypto:TrustStore trustStore; + string certAlias; + |} trustStoreConfig?; + string secret?; +|}; +``` + +## Dependencies + +- [#6513](https://github.com/ballerina-platform/ballerina-library/issues/6513) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 9e99e157..80b4bcc9 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -104,7 +104,7 @@ public type ValidatorSignatureConfig record {| cache:CacheConfig cacheConfig?; ClientConfiguration clientConfig = {}; |} jwksConfig?; - string certFile?; + string|crypto:PublicKey certFile?; record {| crypto:TrustStore trustStore; string certAlias; @@ -222,7 +222,7 @@ public type IssuerSignatureConfig record {| |}|record {| string keyFile; string keyPassword?; - |}|string config?; + |}|crypto:PrivateKey|string config?; |}; public class ClientSelfSignedJwtAuthProvider { diff --git a/gradle.properties b/gradle.properties index 1a54c43d..dc618366 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.11.1-SNAPSHOT +version=2.12.0-SNAPSHOT puppycrawlCheckstyleVersion=10.12.0 ballerinaGradlePluginVersion=2.0.1 ballerinaLangVersion=2201.9.0 stdlibCacheVersion=3.8.0 -stdlibCryptoVersion=2.7.0 +stdlibCryptoVersion=2.7.2 stdlibLogVersion=2.9.0 stdlibTimeVersion=2.4.0 # Transitive dependencies