From dd9cfc95010952092fb4875f6c536b405e2bdd3a Mon Sep 17 00:00:00 2001 From: ThibaudAv Date: Thu, 3 Oct 2024 15:30:43 +0200 Subject: [PATCH] feat: add secretBase64Encoded for HMAC algorithms --- .../policy/generatejwt/GenerateJwtPolicy.java | 8 +- .../GenerateJwtPolicyConfiguration.java | 126 +----------------- src/main/resources/schemas/schema-form.json | 41 +++++- .../GenerateJwtPolicyIntegrationTest.java | 37 +++++ .../apis/generate-jwt-secret-base64.json | 72 ++++++++++ 5 files changed, 157 insertions(+), 127 deletions(-) create mode 100644 src/test/resources/apis/generate-jwt-secret-base64.json diff --git a/src/main/java/io/gravitee/policy/generatejwt/GenerateJwtPolicy.java b/src/main/java/io/gravitee/policy/generatejwt/GenerateJwtPolicy.java index 78c65ccd..c32ee1e8 100644 --- a/src/main/java/io/gravitee/policy/generatejwt/GenerateJwtPolicy.java +++ b/src/main/java/io/gravitee/policy/generatejwt/GenerateJwtPolicy.java @@ -17,6 +17,7 @@ import static java.security.KeyStore.*; +import com.google.common.primitives.Bytes; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; @@ -43,6 +44,7 @@ import java.io.FileNotFoundException; import java.io.InputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; @@ -117,7 +119,11 @@ public void onRequest(Request request, Response response, ExecutionContext execu ) { jwsHeader = new JWSHeader.Builder(configuration.getSignature().getAlg()).keyID(configuration.getKid()).build(); - signer = new MACSigner(configuration.getContent()); + if (configuration.getSecretBase64Encoded()) { + signer = new MACSigner(java.util.Base64.getDecoder().decode(configuration.getContent())); + } else { + signer = new MACSigner(configuration.getContent().getBytes(StandardCharsets.UTF_8)); + } } JWTClaimsSet claimsSet = buildClaims(executionContext); diff --git a/src/main/java/io/gravitee/policy/generatejwt/configuration/GenerateJwtPolicyConfiguration.java b/src/main/java/io/gravitee/policy/generatejwt/configuration/GenerateJwtPolicyConfiguration.java index fdbc4991..802261a5 100644 --- a/src/main/java/io/gravitee/policy/generatejwt/configuration/GenerateJwtPolicyConfiguration.java +++ b/src/main/java/io/gravitee/policy/generatejwt/configuration/GenerateJwtPolicyConfiguration.java @@ -20,11 +20,15 @@ import io.gravitee.policy.generatejwt.model.Claim; import java.util.List; import java.util.concurrent.TimeUnit; +import lombok.Getter; +import lombok.Setter; /** * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team */ +@Setter +@Getter public class GenerateJwtPolicyConfiguration implements PolicyConfiguration { private KeyResolver keyResolver = KeyResolver.INLINE; @@ -55,125 +59,7 @@ public class GenerateJwtPolicyConfiguration implements PolicyConfiguration { private String subject; - private List customClaims; - - public KeyResolver getKeyResolver() { - return keyResolver; - } - - public void setKeyResolver(KeyResolver keyResolver) { - this.keyResolver = keyResolver; - } - - public Signature getSignature() { - return signature; - } - - public void setSignature(Signature signature) { - this.signature = signature; - } - - public String getKid() { - return kid; - } - - public void setKid(String kid) { - this.kid = kid; - } - - public X509CertificateChain getX509CertificateChain() { - return x509CertificateChain; - } - - public void setX509CertificateChain(X509CertificateChain x509CertificateChain) { - this.x509CertificateChain = x509CertificateChain; - } - - public List getAudiences() { - return audiences; - } - - public void setAudiences(List audiences) { - this.audiences = audiences; - } - - public long getExpiresIn() { - return expiresIn; - } - - public void setExpiresIn(long expiresIn) { - this.expiresIn = expiresIn; - } - - public TimeUnit getExpiresInUnit() { - return expiresInUnit; - } - - public void setExpiresInUnit(TimeUnit expiresInUnit) { - this.expiresInUnit = expiresInUnit; - } - - public String getId() { - return id; - } + private Boolean secretBase64Encoded = false; - public void setId(String id) { - this.id = id; - } - - public String getIssuer() { - return issuer; - } - - public void setIssuer(String issuer) { - this.issuer = issuer; - } - - public String getSubject() { - return subject; - } - - public void setSubject(String subject) { - this.subject = subject; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public List getCustomClaims() { - return customClaims; - } - - public void setCustomClaims(List customClaims) { - this.customClaims = customClaims; - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getStorepass() { - return storepass; - } - - public void setStorepass(String storepass) { - this.storepass = storepass; - } - - public String getKeypass() { - return keypass; - } - - public void setKeypass(String keypass) { - this.keypass = keypass; - } + private List customClaims; } diff --git a/src/main/resources/schemas/schema-form.json b/src/main/resources/schemas/schema-form.json index 6a61e1c3..d1d9a2cd 100644 --- a/src/main/resources/schemas/schema-form.json +++ b/src/main/resources/schemas/schema-form.json @@ -64,17 +64,38 @@ "alias" : { "type" : "string", "title": "Key alias", - "description": "Alias used to access a keystore entry. (only for JKS and PKCS#12)" + "description": "Alias used to access a keystore entry. (only for JKS and PKCS#12)", + "gioConfig": { + "displayIf": { + "$eq": { + "value.keyResolver": ["JKS", "PKCS12"] + } + } + } }, "storepass" : { "type" : "string", "title": "Store password", - "description": "Pass used to access the key store. (only for JKS and PKCS#12)" + "description": "Pass used to access the key store. (only for JKS and PKCS#12)", + "gioConfig": { + "displayIf": { + "$eq": { + "value.keyResolver": ["JKS", "PKCS12"] + } + } + } }, "keypass" : { "type" : "string", "title": "Key password", - "description": "Pass used to access the particular key pair's private key. (only for JKS)" + "description": "Pass used to access the particular key pair's private key. (only for JKS)", + "gioConfig": { + "displayIf": { + "$eq": { + "value.keyResolver": ["JKS"] + } + } + } }, "kid" : { "type" : "string", @@ -163,10 +184,18 @@ "allowDropFileTypes": true } }, - "format": "gio-code-editor", + "format": "gio-code-editor" + }, + "secretBase64Encoded": { + "type" : "boolean", + "title": "Secret base64 encoded", + "description": "Whether the secret key is base64 encoded. (only for HMAC algorithms)", + "default": false, "gioConfig": { - "monacoEditorConfig": { - "language": "json" + "displayIf": { + "$eq": { + "value.signature": ["HMAC_HS256", "HMAC_HS384", "HMAC_HS512"] + } } } }, diff --git a/src/test/java/io/gravitee/policy/generatejwt/GenerateJwtPolicyIntegrationTest.java b/src/test/java/io/gravitee/policy/generatejwt/GenerateJwtPolicyIntegrationTest.java index 1c4eb1ef..e970a2a4 100644 --- a/src/test/java/io/gravitee/policy/generatejwt/GenerateJwtPolicyIntegrationTest.java +++ b/src/test/java/io/gravitee/policy/generatejwt/GenerateJwtPolicyIntegrationTest.java @@ -20,8 +20,12 @@ import static org.assertj.core.api.Assertions.assertThat; import com.github.tomakehurst.wiremock.verification.LoggedRequest; +import com.google.common.primitives.Bytes; +import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import io.gravitee.apim.gateway.tests.sdk.AbstractPolicyTest; @@ -33,7 +37,9 @@ import io.vertx.core.http.HttpMethod; import io.vertx.rxjava3.core.http.HttpClient; import io.vertx.rxjava3.core.http.HttpClientRequest; +import java.nio.charset.StandardCharsets; import java.text.ParseException; +import java.util.Base64; import java.util.List; import java.util.Map; import org.junit.jupiter.api.DisplayName; @@ -79,4 +85,35 @@ void shouldGenerateJWTWithCustomClaims(HttpClient httpClient) throws Interrupted assertThat(claimsSet.getStringClaim("claim1")).isEqualTo("claim1-value"); assertThat(claimsSet.getClaim("claim2")).isEqualTo("/test"); } + + @Test + @DisplayName("Should generate a JWT token with secret base64 encoded") + @DeployApi("/apis/generate-jwt-secret-base64.json") + void shouldGenerateJWTWithSecretBase64Encoded(HttpClient httpClient) throws InterruptedException, ParseException, JOSEException { + wiremock.stubFor(get("/endpoint").willReturn(ok())); + + httpClient + .rxRequest(HttpMethod.GET, "/test-jwt-secret-base64") + .flatMap(HttpClientRequest::rxSend) + .test() + .await() + .assertComplete() + .assertValue(response -> { + assertThat(response.statusCode()).isEqualTo(200); + return true; + }) + .assertNoErrors(); + + List requests = wiremock.findRequestsMatching(getRequestedFor(urlPathEqualTo("/endpoint")).build()).getRequests(); + assertThat(requests).hasSize(1); + String generatedJwt = requests.get(0).getHeader(GENERATED_JWT_HEADER_NAME); + SignedJWT signedJWT = SignedJWT.parse(generatedJwt); + JWSHeader jwsHeader = signedJWT.getHeader(); + JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet(); + + JWSVerifier verifier = new MACVerifier("I'm a valid Base64 key with at least 256 bits\n"); + assertThat(signedJWT.verify(verifier)).isTrue(); + + assertThat(jwsHeader.getAlgorithm()).isEqualTo(JWSAlgorithm.HS256); + } } diff --git a/src/test/resources/apis/generate-jwt-secret-base64.json b/src/test/resources/apis/generate-jwt-secret-base64.json new file mode 100644 index 00000000..0ed28e77 --- /dev/null +++ b/src/test/resources/apis/generate-jwt-secret-base64.json @@ -0,0 +1,72 @@ +{ + "id": "my-api-jwt-secret-base64", + "name": "my-api", + "gravitee": "2.0.0", + "proxy": { + "context_path": "/test-jwt-secret-base64", + "endpoints": [ + { + "name": "default", + "target": "http://localhost:8080/endpoint", + "http": { + "connectTimeout": 3000, + "readTimeout": 60000 + } + } + ] + }, + "flows": [ + { + "name": "flow-1", + "methods": [ + "GET" + ], + "enabled": true, + "path-operator": { + "path": "/", + "operator": "STARTS_WITH" + }, + "pre": [ + { + "name": "Generate JWT", + "description": "", + "enabled": true, + "policy": "policy-generate-jwt", + "configuration": { + "signature": "HMAC_HS256", + "expiresIn": 30, + "expiresInUnit": "SECONDS", + "issuer": "urn://gravitee-api-gw", + "audiences": [ + "graviteeam" + ], + "customClaims": [ + { + "name": "claim1", + "value": "claim1-value" + }, + { + "name": "claim2", + "value": "{#request.path}" + } + ], + "id": "817c6cfa-6ae6-446e-a631-5ded215b404b", + "kid": "my-kid", + "//📝 Content Decoded": "I'm a valid Base64 key with at least 256 bits", + "content": "SSdtIGEgdmFsaWQgQmFzZTY0IGtleSB3aXRoIGF0IGxlYXN0IDI1NiBiaXRzCg==", + "secretBase64Encoded": true + } + }, + { + "name": "Generate JWT", + "description": "", + "enabled": true, + "policy": "jwt-attributes-to-headers", + "configuration": { + } + } + ], + "post": [] + } + ] +}