JWT-Policy: Add support for JWK(S)
MODIFY
jwt-policy/pom.xml
jwt-policy/pom.xml
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
+ <!-- https://mvnrepository.com/artifact/commons-validator/commons-validator -->
+ <dependency>
+ <groupId>commons-validator</groupId>
+ <artifactId>commons-validator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.auth0</groupId>
+ <artifactId>jwks-rsa</artifactId>
+ </dependency>
<!-- apiman -->
<dependency>
<groupId>io.apiman</groupId>
<artifactId>apiman-test-policies</artifactId>
<scope>test</scope>
</dependency>
+ <!-- mockserver -->
+ <dependency>
+ <groupId>org.mock-server</groupId>
+ <artifactId>mockserver-netty</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<resources>

MODIFY
jwt-policy/src/main/apiman/policyDefs/schemas/jwt-policyDef.schema
jwt-policy/src/main/apiman/policyDefs/schemas/jwt-policyDef.schema
"default": false
},
"signingKeyString": {
- "title": "Signing Key",
- "description": "To validate JWT. Must be Base-64 encoded.",
+ "title": "Signing Key or URL to a JWK(S)",
+ "description": "To validate JWT. Must be Base-64 encoded or you specify a URL to a JWK(S)",
"type": "string",
"format": "textarea"
},
+ "kid": {
+ "title": "Key ID (kid) of JWK(S)",
+ "description": "If you provided a JWK(S) URL above you can specify here the kid of the JWK(S)",
+ "type": "string"
+ },
"allowedClockSkew": {
"title": "Maximum Clock Skew",
"description": "Maximum allowed clock skew in seconds when validating exp (expiry) and nbf (not before) claims. Zero implies default behaviour.",

MODIFY
jwt-policy/src/main/java/io/apiman/plugins/jwt/JWTPolicy.java
jwt-policy/src/main/java/io/apiman/plugins/jwt/JWTPolicy.java
package io.apiman.plugins.jwt;
+import com.auth0.jwk.*;
import io.apiman.gateway.engine.beans.ApiRequest;
import io.apiman.gateway.engine.policies.AbstractMappedPolicy;
import io.apiman.gateway.engine.policy.IPolicyChain;
@@ -31,7 +32,10 @@ import io.jsonwebtoken.PrematureJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.lang.Objects;
+import org.apache.commons.validator.routines.UrlValidator;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.Map;
import java.util.Optional;
@@ -46,6 +50,9 @@ public class JWTPolicy extends AbstractMappedPolicy<JWTPolicyBean> {
private static final String ACCESS_TOKEN_QUERY_KEY = "access_token"; //$NON-NLS-1$
private static final String BEARER = "bearer "; //$NON-NLS-1$
private static final PolicyFailureFactory FAILURE_FACTORY = PolicyFailureFactory.getInstance();
+ private static final String[] schemes = {"http","https"};
+ private static final UrlValidator urlValidator = new UrlValidator(schemes);
+ private JwkProvider provider = null;
@Override
protected Class<JWTPolicyBean> getConfigurationClass() {
@@ -54,11 +61,12 @@ public class JWTPolicy extends AbstractMappedPolicy<JWTPolicyBean> {
@Override
protected void doApply(ApiRequest request, IPolicyContext context, JWTPolicyBean config, IPolicyChain<ApiRequest> chain) {
+
String jwt = Optional.ofNullable(request.getHeaders().get(AUTHORIZATION_KEY))
// If seems to be bearer token
.filter(e -> e.toLowerCase().startsWith(BEARER))
// Get out token value
- .map(e -> e.substring(BEARER.length(), e.length()))
+ .map(e -> e.substring(BEARER.length()))
// Otherwise attempt to get from the access_token query param
.orElse(request.getQueryParams().get(ACCESS_TOKEN_QUERY_KEY));
@@ -76,7 +84,7 @@ public class JWTPolicy extends AbstractMappedPolicy<JWTPolicyBean> {
if (jwt != null) {
try {
- Map<String, Object> claims = validateJwt(jwt, request, config);
+ Map<String, Object> claims = validateJwt(jwt, config);
forwardHeaders(request, config, jwt, claims);
stripAuthTokens(request, config);
chain.doApply(request);
@@ -87,6 +95,10 @@ public class JWTPolicy extends AbstractMappedPolicy<JWTPolicyBean> {
} catch (MalformedJwtException e) {
chain.doFailure(FAILURE_FACTORY.jwtMalformed(context, e));
} catch (SignatureException e) {
+ if (urlValidator.isValid(config.getSigningKeyString())){
+ // This will clear the cache of the jwk(s) provider
+ provider = getNewJwksProvider(config.getSigningKeyString());
+ }
chain.doFailure(FAILURE_FACTORY.signatureException(context, e));
} catch (InvalidClaimException e) {
chain.doFailure(FAILURE_FACTORY.invalidClaim(context, e));
@@ -100,8 +112,26 @@ public class JWTPolicy extends AbstractMappedPolicy<JWTPolicyBean> {
}
}
- private Map<String, Object> validateJwt(String token, ApiRequest request, JWTPolicyBean config)
+ private Map<String, Object> validateJwt(String token, JWTPolicyBean config)
throws ExpiredJwtException, PrematureJwtException, MalformedJwtException, SignatureException, InvalidClaimException {
+
+ // check if we have to use jwk(s)
+ if (urlValidator.isValid(config.getSigningKeyString())){
+ if (provider == null){
+ provider = getNewJwksProvider(config.getSigningKeyString());
+ }
+
+ Jwk jwk;
+ try {
+ jwk = provider.get(config.getKid());
+ if (config.getSigningKey() == null || !(config.getSigningKey().equals(jwk.getPublicKey()))) {
+ config.setSigningKey(jwk.getPublicKey());
+ }
+ } catch (JwkException e) {
+ throw new SignatureException("JWK was not found with kid: " + config.getKid(), e);
+ }
+ }
+
JwtParser parser = Jwts.parser()
.setSigningKey(config.getSigningKey())
.setAllowedClockSkewSeconds(config.getAllowedClockSkew());
@@ -129,4 +159,14 @@ public class JWTPolicy extends AbstractMappedPolicy<JWTPolicyBean> {
}
}
}
+
+ private GuavaCachedJwkProvider getNewJwksProvider(String url){
+ JwkProvider http = null;
+ try {
+ http = new UrlJwkProvider(new URL(url));
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ return new GuavaCachedJwkProvider(http);
+ }
}

MODIFY
jwt-policy/src/main/java/io/apiman/plugins/jwt/beans/JWTPolicyBean.java
jwt-policy/src/main/java/io/apiman/plugins/jwt/beans/JWTPolicyBean.java
@@ -23,15 +23,10 @@ import java.util.Map;
import javax.annotation.Generated;
+import com.fasterxml.jackson.annotation.*;
+import org.apache.commons.validator.routines.UrlValidator;
import org.keycloak.common.util.PemUtils;
-import com.fasterxml.jackson.annotation.JsonAnyGetter;
-import com.fasterxml.jackson.annotation.JsonAnySetter;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonPropertyOrder;
-
/**
* JWT Authentication Policy Configuration
*
@@ -39,7 +34,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Generated("org.jsonschema2pojo")
-@JsonPropertyOrder({ "requireJWT", "requireSigned", "requireTransportSecurity", "stripTokens", "signingKeyString", "allowedClockSkew",
+@JsonPropertyOrder({ "requireJWT", "requireSigned", "requireTransportSecurity", "stripTokens", "signingKeyString", "kid", "allowedClockSkew",
"requiredClaims", "forwardAuthInfo" })
public class JWTPolicyBean {
@@ -83,6 +78,15 @@ public class JWTPolicyBean {
@JsonProperty("signingKeyString")
private String signingKeyString;
private Key signingKey;
+ /**
+ * Key ID (kid) of JWK(S)
+ * <p>
+ * If you provided a JWK(S) URL above you can specify here the kid of the JWK(S)
+ *
+ */
+ @JsonProperty("kid")
+ @JsonPropertyDescription("If you provided a JWK(S) URL above you can specify here the kid of the JWK(S)")
+ private String kid;
/**
* Maximum Clock Skew
* <p>
@@ -248,11 +252,10 @@ public class JWTPolicyBean {
}
/**
- * Signing Key
+ * Signing Key or URL to a JWK(S)
* <p>
- * To validate JWT. Must be Base-64 encoded.
+ * To validate JWT. Must be Base-64 encoded or you specify a URL to a JWK(S)
*
- * @return The signingKeyString
*/
@JsonProperty("signingKeyString")
public String getSigningKeyString() {
@@ -271,8 +274,13 @@ public class JWTPolicyBean {
@JsonProperty("signingKeyString")
public void setSigningKeyString(String signingKeyString) throws Exception {
if (signingKey == null) {
+ String[] schemes = {"http","https"};
+ UrlValidator urlValidator = new UrlValidator(schemes);
+ // If it is a jwk(s) url we don't set the signingKey
+ if (!urlValidator.isValid(signingKeyString)){
signingKey = PemUtils.decodePublicKey(signingKeyString);
}
+ }
this.signingKeyString = signingKeyString;
}
@@ -281,6 +289,32 @@ public class JWTPolicyBean {
return this;
}
+ /**
+ * Key ID (kid) of JWK(S)
+ * <p>
+ * If you provided a JWK(S) URL above you can specify here the kid of the JWK(S)
+ *
+ */
+ @JsonProperty("kid")
+ public String getKid() { return kid; }
+
+ /**
+ * Key ID (kid) of JWK(S)
+ * <p>
+ * If you provided a JWK(S) URL above you can specify here the kid of the JWK(S)
+ *
+ */
+ @JsonProperty("kid")
+ public void setKid(String kid){
+ this.kid = kid.equalsIgnoreCase("null") || kid.isEmpty() ? null : kid;
+ }
+
+ @JsonProperty("kid")
+ public JWTPolicyBean withKid (String kid){
+ this.kid = kid;
+ return this;
+ }
+
/**
* Maximum Clock Skew
* <p>
@@ -410,7 +444,9 @@ public class JWTPolicyBean {
return this;
}
- public Key getSigningKey() {
- return signingKey;
+ public Key getSigningKey() { return signingKey; }
+
+ public void setSigningKey(Key signingKey){
+ this.signingKey = signingKey;
}
}

MODIFY
jwt-policy/src/test/java/io/apiman/plugins/jwt/JWTPolicyTest.java
jwt-policy/src/test/java/io/apiman/plugins/jwt/JWTPolicyTest.java
@@ -30,14 +30,23 @@ import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
+import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
+import java.util.Base64;
import java.util.Date;
import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.PemUtils;
+import org.mockserver.client.MockServerClient;
+import org.mockserver.junit.MockServerRule;
+
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
/**
* @author Marc Savy {@literal <msavy@redhat.com>}
@@ -47,8 +56,33 @@ import org.keycloak.common.util.PemUtils;
public class JWTPolicyTest extends ApimanPolicyTest {
private static final String PUBLIC_KEY_PEM = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmoV2gM0BGxgLQUpMkNdLKkXq46tcCBjoatHWqukrYj6VZ1t6OciWYKZRsmBVDsc34gFM6/fBqBn7zRwIK+OGXu1OLGoXEjR9I+awdxpQItjDq9lyFMDFPfXu6nCPSpZ+txNWl6V2cno6PpcEPpUYT6n6lUjcwpbTuGwq80P29Net212ksAwLJGvpIIUJ5yWuYJtirhoUeJEwKJAGbo5xrRrY9w1pkw+1kdPhUpP26pd80Mga2hcwJtykeIx5gLajRbhsXaijOv2FBtBSKgEH8tXISt16SBjaUbp642tLvqsT/VUPvvcgmcWWqhvm72ALaBwu3G/OHswRMCxxMohMyQIDAQAB";
private static final String PRIVATE_KEY_PEM = "MIIEpQIBAAKCAQEAmoV2gM0BGxgLQUpMkNdLKkXq46tcCBjoatHWqukrYj6VZ1t6OciWYKZRsmBVDsc34gFM6/fBqBn7zRwIK+OGXu1OLGoXEjR9I+awdxpQItjDq9lyFMDFPfXu6nCPSpZ+txNWl6V2cno6PpcEPpUYT6n6lUjcwpbTuGwq80P29Net212ksAwLJGvpIIUJ5yWuYJtirhoUeJEwKJAGbo5xrRrY9w1pkw+1kdPhUpP26pd80Mga2hcwJtykeIx5gLajRbhsXaijOv2FBtBSKgEH8tXISt16SBjaUbp642tLvqsT/VUPvvcgmcWWqhvm72ALaBwu3G/OHswRMCxxMohMyQIDAQABAoIBAQCHcwZ10T5u6Zy0FtUXAiI5ZCCKgeOilXLmcBqkptAIxqNgfqedj1+CSUjD+/2Tfr5Vtp4fGob/PAelvDTNhBx9ibdE55phsvEfT1DQlpg4c5rSQUHnPzOnJLXRe+mfkFxzTthRBhHWN55mzypBUaCF9JJb2grp6ByfRPJBXApWhHrEALUwTd/9OiETsC4d7GbJ6ofk45tSl0HzNIeld9iEZk0WrgH95ucN75yCYv839096nB9nCH80yXV9JZIGj8bC6aPwbBnUnUdQqZxsDBlKNkT7U5AIdhqQdYjdXteTopuv12bflXtZGyTJoes1qLL8lpWgzkbjQg91+qmpCywhAoGBANv5opBc10C3y6ZJh9zepbM+wr0tbzUvTFAhj1Y1DoaHwxb9qV1mtWQZ5qEf1O+7RJYljv3hwzUc/gsZ13nBfySpVrdiVMTVIuLC8UPuH/sv0Z/uXcbwr3jxezrhJX5dJkhz1I8gPUKHLWIhMp/jZr26ieSPz9KwupTn+MPmPyYfAoGBALPTtHqZbB4dpxPmImv2l7PgR92CwVSd/yjrfOold4Oi1bODjhNSR6/h/YghWfRHAHIoRBWTlSfu/JsffJG/2bZa2xlcpqMb/fHzg05zBtmu8ozi3CAE7Twg5bE4GtuqV1hFXK4mPxzboSmj8H4puU85GNuTA/sRDd9saZSu6CAXAoGAJ30//rR7++VCzN5EYpUhn/TzVqyyWxTbmUL9DVfG/MWgcx8kaV0H0SmJKoGhY0v1+xJRAiimN4G15V5FPVlMLtOreo5Pc2pjsduXHj/ARAKImjJbaVxJ0+dd3OsQJQgp2DXbAbqi5K+JqSUWhnd3OTYkjQB4KXWKeTLPiLNrwLcCgYEAm7l8dCLCRv4kvo2vR1E/I+zYLxHZO96qpRPwk4+ohJ0RdKg6865wF/abKDTBglGuKC2IcCriordJl0fYBxtdfJYHYFokj+FgsxLOpbPkvcPLlYerWisKCeTvI93THGDRzMYcMU87nlDvqnCmhYq6R8nJJfSVIOku20k10ST6LTcCgYEAtrTamlY8tQ9Li+yi+yeGB2nxQCVkjQE0yl2GPxGrZaXlpH2mrhshtz0UUXcDmfpUOINCc3OzgWNCymNUesmVNuaobvgERXiDv51cSDgfYNT6NZz4+JPox2sGgeZIgkvQlFPr6+OxaMl8iQLHKwIqFjJAGCajKA4CodlIRaQClqM=";
+ private static final String PUBLIC_KEY_PEM_2 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+TCyq0U+N4IHVBbwuRoOGYF/exUcEnQGsbSXiSXIHFATQkGEhTRFbGFiq6PBRKnhSQKoM9IzKO7Fo4rpwSf6HdYsN5y9UkL/DQpiQxbXLjbAoAcjpvToSkCS2sfbzDubU1ZVJJN8g9Ag47tG1VjdpDRqH8GgeR1t/qBn62NbU1RCU8WfIqfNNL2JeVGvuixSnoFleIBYejCN7+i0/glUvf+6D/WdeJYuZ2ZbBMk8p+Xh2WE4dNv0ILV4yC27hFho+sbImn2YI7cIowXjRY3zDN69n+JzjAz0sdgMcaNi34E6U9wZAidgQF52XiqVeimjpPzcm31sm7n3BahodKC4wIDAQAB";
+ private static final String PRIVATE_KEY_PEM_2 = "MIIEowIBAAKCAQEAu+TCyq0U+N4IHVBbwuRoOGYF/exUcEnQGsbSXiSXIHFATQkGEhTRFbGFiq6PBRKnhSQKoM9IzKO7Fo4rpwSf6HdYsN5y9UkL/DQpiQxbXLjbAoAcjpvToSkCS2sfbzDubU1ZVJJN8g9Ag47tG1VjdpDRqH8GgeR1t/qBn62NbU1RCU8WfIqfNNL2JeVGvuixSnoFleIBYejCN7+i0/glUvf+6D/WdeJYuZ2ZbBMk8p+Xh2WE4dNv0ILV4yC27hFho+sbImn2YI7cIowXjRY3zDN69n+JzjAz0sdgMcaNi34E6U9wZAidgQF52XiqVeimjpPzcm31sm7n3BahodKC4wIDAQABAoIBACTjtgba0opF9Wvj8hAijf+8sCJ5et6M72nCbV0EbBN3iLtXMTTtl1td/i0LNpM1ZWRzfg6yg8WBw+KaySFCfC7E98nJ8uILlGnQx/LbVTiwJneoNXMeTv+OMKAkCQjon3cgP5CmJN2Idw8dSZobOqr1peQiBGIOO7qCWV7DUUgLFr3DYWgrYdTOhHD7tFU9TVNd67OahJmPvq7KLVmbjjJJT+XcV52DV/bE74EfCCrm/XrVvfPxeuY0xps9g3C5uYmTJ947s1yYvDnedmgbKf8xpxZOOrU2SGMkrGY8M2pXEfIzKCEpf5lJd4jmfanx07hYnbqVCNK0Wi1kWxgZ+pECgYEA6QUN+xIPeMcv1ezxdIlbC9EV05y81qv67Qu8d9vOgBPq/dtij1s83vbaLjo2CL298Fff9+d+mc+EmYkdh3cnalFGkFaYEgRJK2ZX9M6HzVOvcxHACeMFStnEoHUX4oYS+I5LnbPTqNpSN353wTaQaTlET8NwjAx31Eq7Qh/EWLsCgYEAzmxtNnu3DnhLYVTnOIPKhDcDj0Y8vUgoH2mh9DNLiDOUpoAyJN2++P4YROTlV2DO8YAJKn/nT/jDP431SNjMWxg294TwhRLBTo6Wsb6VshvO2oMNlzU4XhxslYostxtpzxxXR5Hz4Y4r7iEGyeCVWK/FV3+VMKv4XzyhN787z/kCgYAkbEnXKOeKNXhgs/y5/o5gtnn27dUGqTM7wk3fXlhU5MgijhxF1DkbkPJhr2+qxh3eZ532nhTH7gwIA8q91f4vPC0Permid1EIm6K7/Vx019Pg5LFj2jyFiqyVeDgXjGWCvJtNN0KqbhXT3szRQron+G+ZGC+LdJd2c4f5ugVy6QKBgH1kyjcVkg7dLlVS4R6omGYWkTgAn7cP1Se34GTaoCB9zOgT6eIRPgg9OnrnXRXmPe/gKjFB/z7KS7kYwA7fe8w+em2DuSPtT7Yr0gjpUEAgulhs0d2vu60XsTJp/F3C7lScz7wvQiobVj5Sm7AYmECGjedoHjWx0a/wwbJ+nZPpAoGBALwMo52OY05k+LpSsYWQCLFNfuitizQ8STYOTBPA3nGA2C1ntYK29ixFt897bZvWiZvgP261W1yaRNkUIG6qS43mUOVL9uiJvVHMM4+mHuz6N64Q8hbFi7+HQiWvKyN/FRp3H0mbPR4VMdMqwqI0N02HuERPJfcYn4L1wg5Mizb8";
private static final String AUTHORIZATION = "Authorization";
+ @Rule
+ // http://www.mock-server.com/mock_server/running_mock_server.html#junit_rule
+ public MockServerRule mockServerRule = new MockServerRule(this, 1080);
+ private MockServerClient mockServerClient = mockServerRule.getClient();
+
+ @Before
+ public void initialize(){
+ RSAPublicKey rsa = getPublicRsaKey(PUBLIC_KEY_PEM);
+ mockServerClient.when(request().withMethod("GET").withPath("/jwks.json"))
+ .respond(response().withStatusCode(200)
+ .withBody("{\"keys\": [\n" +
+ " {\n" +
+ " \"kid\": null,\n" +
+ " \"e\": \""+ Base64.getUrlEncoder().encodeToString(rsa.getPublicExponent().toByteArray()) +"\",\n" +
+ " \"n\": \""+ Base64.getUrlEncoder().encodeToString(rsa.getModulus().toByteArray()) +"\",\n" +
+ " \"kty\":\"RSA\",\n" +
+ " \"alg\": \"RS256\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}")
+ );
+ }
+
@Test
@Configuration("{\n" +
" \"requireJWT\": true,\n" +
@@ -61,7 +95,31 @@ public class JWTPolicyTest extends ApimanPolicyTest {
"}"
)
public void signedValidToken() throws Throwable {
- String authVal = "Bearer " + signedToken();
+ String authVal = "Bearer " + signedToken(PRIVATE_KEY_PEM);
+ PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante")
+ .header(AUTHORIZATION, authVal);
+ PolicyTestResponse response = send(request);
+ EchoResponse echo = response.entity(EchoResponse.class);
+ Assert.assertNotNull(echo);
+ // Ensure we didn't remove the header and it has remained unchanged
+ Assert.assertEquals(authVal, echo.getHeaders().get(AUTHORIZATION));
+ }
+
+ @Test
+ @Configuration("{\n" +
+ " \"requireJWT\": true,\n" +
+ " \"requireSigned\": true,\n" +
+ " \"requireTransportSecurity\": true,\n" +
+ " \"stripTokens\": false,\n" +
+ " \"signingKeyString\": \"http://127.0.0.1:1080/jwks.json\", \n" +
+ " \"kid\": \"null\", \n" +
+ " \"allowedClockSkew\": 0,\n" +
+ " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" +
+ "}"
+ )
+ public void signedValidTokenWithJwksAndExpireCache() throws Throwable {
+ // First request with first jwk(s)
+ String authVal = "Bearer " + signedToken(PRIVATE_KEY_PEM);
PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante")
.header(AUTHORIZATION, authVal);
PolicyTestResponse response = send(request);
@@ -69,6 +127,45 @@ public class JWTPolicyTest extends ApimanPolicyTest {
Assert.assertNotNull(echo);
// Ensure we didn't remove the header and it has remained unchanged
Assert.assertEquals(authVal, echo.getHeaders().get(AUTHORIZATION));
+
+ // Second request will fail because jwk(s) has changed and we have to invalidate the cache
+ authVal = "Bearer " + signedToken(PRIVATE_KEY_PEM_2);
+ request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante")
+ .header(AUTHORIZATION, authVal);
+ try {
+ send(request);
+ // we should never get a valid response
+ Assert.fail("Valid response instead of exception");
+ } catch (Exception e) {
+ assert true;
+ }
+
+ // Update jwk(s) on mockserver
+ mockServerClient.reset();
+ RSAPublicKey rsa = getPublicRsaKey(PUBLIC_KEY_PEM_2);
+ mockServerClient.when(request().withMethod("GET").withPath("/jwks.json"))
+ .respond(response().withStatusCode(200)
+ .withBody("{\"keys\": [\n" +
+ " {\n" +
+ " \"kid\": null,\n" +
+ " \"e\": \""+ Base64.getUrlEncoder().encodeToString(rsa.getPublicExponent().toByteArray()) +"\",\n" +
+ " \"n\": \""+ Base64.getUrlEncoder().encodeToString(rsa.getModulus().toByteArray()) +"\",\n" +
+ " \"kty\":\"RSA\",\n" +
+ " \"alg\": \"RS256\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}")
+ );
+
+ // Send request again and it will pass
+ authVal = "Bearer " + signedToken(PRIVATE_KEY_PEM_2);
+ request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante")
+ .header(AUTHORIZATION, authVal);
+ response = send(request);
+ echo = response.entity(EchoResponse.class);
+ Assert.assertNotNull(echo);
+ // Ensure we didn't remove the header and it has remained unchanged
+ Assert.assertEquals(authVal, echo.getHeaders().get(AUTHORIZATION));
}
@Test
@@ -84,7 +181,7 @@ public class JWTPolicyTest extends ApimanPolicyTest {
)
public void signedValidTokenStripAuth() throws Throwable {
PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante")
- .header(AUTHORIZATION, "Bearer " + signedToken());
+ .header(AUTHORIZATION, "Bearer " + signedToken(PRIVATE_KEY_PEM));
PolicyTestResponse response = send(request);
EchoResponse echo = response.entity(EchoResponse.class);
@@ -358,9 +455,9 @@ public class JWTPolicyTest extends ApimanPolicyTest {
Assert.assertEquals(PolicyFailureType.Authentication, failure.getType());
}
- private String signedToken() throws Exception {
+ private String signedToken(String privateKey) throws Exception {
JwtBuilder jwts = Jwts.builder().setSubject("france frichot")
- .signWith(SignatureAlgorithm.RS256, PemUtils.decodePrivateKey(PRIVATE_KEY_PEM));
+ .signWith(SignatureAlgorithm.RS256, PemUtils.decodePrivateKey(privateKey));
return jwts.compact();
}
@@ -382,4 +479,14 @@ public class JWTPolicyTest extends ApimanPolicyTest {
JwtBuilder jwts = Jwts.builder().setSubject("france frichot").setExpiration(exp);
return jwts.compact();
}
+
+ private RSAPublicKey getPublicRsaKey(String publicKey){
+ RSAPublicKey rsa = null;
+ try {
+ rsa = (RSAPublicKey) PemUtils.decodePublicKey(publicKey);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return rsa;
+ }
}

MODIFY
pom.xml
pom.xml
<version.junit>4.11</version.junit>
<version.commons-lang>2.6</version.commons-lang>
+ <version.commons-validator>1.6</version.commons-validator>
<version.com.google.guava>21.0</version.com.google.guava>
<version.com.fasterxml.jackson>2.9.5</version.com.fasterxml.jackson>
<version.org.keycloak>2.0.0.Final</version.org.keycloak>
<version.org.bouncycastle>1.52</version.org.bouncycastle>
<version.org.json>20140107</version.org.json>
- <version.io.jsonwebtoken.jjwt>0.9.0</version.io.jsonwebtoken.jjwt>
+ <version.org.mock-server>5.6.0</version.org.mock-server>
+ <version.io.jsonwebtoken.jjwt>0.9.1</version.io.jsonwebtoken.jjwt>
+ <version.com.auth0.jwks-rsa>0.8.2</version.com.auth0.jwks-rsa>
</properties>
<repositories>
<artifactId>mockito-all</artifactId>
<version>${version.org.mockito}</version>
</dependency>
+ <!-- mockserver -->
+ <dependency>
+ <groupId>org.mock-server</groupId>
+ <artifactId>mockserver-netty</artifactId>
+ <version>${version.org.mock-server}</version>
+ </dependency>
<!-- others -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${version.commons-lang}</version>
</dependency>
+ <dependency>
+ <groupId>commons-validator</groupId>
+ <artifactId>commons-validator</artifactId>
+ <version>${version.commons-validator}</version>
+ </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<artifactId>jjwt</artifactId>
<version>${version.io.jsonwebtoken.jjwt}</version>
</dependency>
+ <dependency>
+ <groupId>com.auth0</groupId>
+ <artifactId>jwks-rsa</artifactId>
+ <version>${version.com.auth0.jwks-rsa}</version>
+ </dependency>
</dependencies>
</dependencyManagement>

JWT-Policy: Add support for JWK(S)