Skip to content

Commit

Permalink
Merge pull request #1229 from ayeshLK/master
Browse files Browse the repository at this point in the history
Add support to directly provide `crypto:PrivateKey` and `crypto:PublicKey` in JWT signature configurations
  • Loading branch information
ayeshLK authored May 31, 2024
2 parents 83e2cbd + 2d2b543 commit 90cd6b6
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 16 deletions.
6 changes: 3 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
8 changes: 6 additions & 2 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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"
Expand All @@ -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"},
Expand Down
6 changes: 4 additions & 2 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, 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 {|
Expand All @@ -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
Expand Down Expand Up @@ -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 = <string> config?.keyFile;
string? keyPassword = config?.keyPassword;
Expand Down
15 changes: 10 additions & 5 deletions ballerina/jwt_validator.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 {|
Expand All @@ -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;
Expand Down Expand Up @@ -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 () {
Expand All @@ -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.");
Expand Down
40 changes: 40 additions & 0 deletions ballerina/tests/jwt_issuer_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions ballerina/tests/jwt_validator_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -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 = {
Expand Down
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions docs/proposals/enable-crypto-key-usage.md
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions docs/spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -222,7 +222,7 @@ public type IssuerSignatureConfig record {|
|}|record {|
string keyFile;
string keyPassword?;
|}|string config?;
|}|crypto:PrivateKey|string config?;
|};
public class ClientSelfSignedJwtAuthProvider {
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 90cd6b6

Please sign in to comment.