diff --git a/.gitignore b/.gitignore index 38b9c990..ee7866ef 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,13 @@ hs_err_pid* # Maven files /target/ +./target/* # Idea files /.idea/ *.iml + +# Eclipse files (no comment please ;) +.classpath +.project +.settings/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..fbc84619 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# JWT Gravitee Policy + +Here you can document your JWT Gravitee Policy. + +Current policy use distributed cache (hazelcast) +To run the policy you need to start cache before starting the gateway diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..45e9fd40 --- /dev/null +++ b/pom.xml @@ -0,0 +1,152 @@ + + + + 4.0.0 + + io.gravitee.policy + gravitee-policy-jwt + 1.0.1-SNAPSHOT + + JWT Gravitee Policy + Description of the JWT Gravitee Policy + + + io.gravitee + gravitee-parent + 5 + + + + 0.12.0 + 0.9.0 + 0.10.0 + 0.1.0 + 0.15.0-SNAPSHOT + 4.12 + 0.6.0 + + 1.1.0 + ${project.build.directory}/schemas + + 2.5.5 + + + + + + io.gravitee.gateway + gravitee-gateway-api + ${gravitee-gateway-api.version} + provided + + + + io.gravitee.policy + gravitee-policy-api + ${gravitee-policy-api.version} + provided + + + + io.gravitee.common + gravitee-common + ${gravitee-common.version} + provided + + + + io.gravitee.resource + gravitee-resource-api + ${gravitee-resource-api.version} + provided + + + + io.gravitee.repository + gravitee-repository + ${gravitee-repository.version} + provided + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + org.springframework + spring-core + ${spring.version} + + + + ch.qos.logback + logback-classic + 1.1.7 + + + + + junit + junit + ${junit.version} + test + + + + org.mockito + mockito-all + ${mockito.version} + test + + + + + + + src/main/resources + true + + + + + maven-assembly-plugin + ${maven-assembly-plugin.version} + + false + + src/assembly/policy-assembly.xml + + + + + make-policy-assembly + package + + single + + + + + + + + diff --git a/src/assembly/policy-assembly.xml b/src/assembly/policy-assembly.xml new file mode 100644 index 00000000..d75e5bab --- /dev/null +++ b/src/assembly/policy-assembly.xml @@ -0,0 +1,59 @@ + + + policy + + zip + + false + + + + + ${project.build.directory}/${project.build.finalName}.jar + + + + + + + src/main/resources/schemas + schemas + + + + + + ${project.basedir}/src/assembly + lib + + * + + + + + + + + lib + false + + + \ No newline at end of file diff --git a/src/main/java/io/gravitee/policy/jwt/JWTPolicy.java b/src/main/java/io/gravitee/policy/jwt/JWTPolicy.java new file mode 100644 index 00000000..13467e29 --- /dev/null +++ b/src/main/java/io/gravitee/policy/jwt/JWTPolicy.java @@ -0,0 +1,328 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.policy.jwt; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +import io.gravitee.common.http.HttpHeaders; +import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.gateway.api.ExecutionContext; +import io.gravitee.gateway.api.Request; +import io.gravitee.gateway.api.Response; +import io.gravitee.policy.api.PolicyChain; +import io.gravitee.policy.api.PolicyResult; +import io.gravitee.policy.api.annotations.OnRequest; +import io.gravitee.policy.jwt.configuration.JWTPolicyConfiguration; +import io.gravitee.policy.jwt.exceptions.ValidationFromCacheException; +import io.gravitee.repository.cache.api.CacheManager; +import io.gravitee.repository.cache.model.Cache; +import io.gravitee.repository.cache.model.Element; +import io.gravitee.repository.exceptions.CacheException; +import io.gravitee.resource.api.ResourceManager; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import io.jsonwebtoken.impl.DefaultClaims; + +@SuppressWarnings("unused") +public class JWTPolicy { + + private static final Logger LOGGER = LoggerFactory.getLogger(JWTPolicy.class); + + + /** + * Private JWT constants + */ + private static final String BEARER = "Bearer"; + private static final String ACCESS_TOKEN = "access_token"; + private static final String ISS = "iss"; + private static final String DEFAULT_KID = "default"; + private static final String PUBLIC_KEY_PROPERTY = "policy.jwt.issuer.%s.%s"; + private static final String CACHE_NAME = "JWT_CACHE";//must be also set into your distributed cache settings (ex :hazelcast.xml) + private static final Pattern SSH_PUB_KEY = Pattern.compile("ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*)"); + + /** + * The associated configuration to this JWT Policy + */ + private JWTPolicyConfiguration configuration; + private Cache cache; + + /** + * Create a new JWT Policy instance based on its associated configuration + * + * @param configuration the associated configuration to the new JWT Policy instance + */ + public JWTPolicy(JWTPolicyConfiguration configuration) { + this.configuration = configuration; + } + + + + @OnRequest + public void onRequest(Request request, Response response, ExecutionContext executionContext, PolicyChain policyChain) { + try { + //1st extract the JWT to validate. + String jwt = extractJsonWebToken(request); + + //2nd check if cache is enabled and if yes, check if JWT has already been validated. + if(this.configuration.isUseValidationCache()) { + validateTokenFromCache(executionContext, jwt); + } + //3rd, if no cache is used, then just parse and validate it. + else { + validateJsonWebToken(executionContext, jwt); + } + + //Finally continue the process... + policyChain.doNext(request, response); + + } + catch (ValidationFromCacheException e ){ + policyChain.failWith(PolicyResult.failure(e.getMessage())); + } + catch (ExpiredJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e ) { + LOGGER.error(e.getMessage(),e.getCause()); + policyChain.failWith(PolicyResult.failure(HttpStatusCode.UNAUTHORIZED_401, "Unauthorized")); + } + } + + /** + * Extract JWT from the request. + * First attempt to extract if from standard Authorization header. + * If none, then try to extract it from access_token query param. + * @param request Request + * @return String Json Web Token. + */ + private String extractJsonWebToken(Request request) { + final String authorization = request.headers().getFirst(HttpHeaders.AUTHORIZATION); + String jwt; + + if (authorization != null) { + jwt = authorization.substring(BEARER.length()).trim(); + } else { + jwt = request.parameters().get(ACCESS_TOKEN); + } + + return jwt; + } + + private void validateTokenFromCache(ExecutionContext executionContext, String jwt) throws ValidationFromCacheException{ + try { + // Get Cache + CacheManager cacheManager = executionContext.getComponent(CacheManager.class); + if (cacheManager == null) { + throw new ValidationFromCacheException("No cache manager has been found"); + } + + cache = cacheManager.getCache(CACHE_NAME); + if (cache == null) { + throw new ValidationFromCacheException("No cache named [ " + CACHE_NAME + " ] has been found."); + } + + // Get token expiration date from cache. + Element cacheElement = cache.get(jwt); + Instant expiration; + if (cacheElement == null) { + // If no token found in cache then parse/validate/cache it + expiration = validateJsonWebToken(executionContext, jwt); + cache.put(Element.from(jwt, expiration)); + } else { + // If token (cache key) exists in cache, check expiration time (cache value). + expiration = (Instant) cacheElement.value(); + if (Instant.now().isAfter(expiration)) { + throw new JwtException("Token expired!"); + } + } + } + // If Cache is not correctly set or active, then do not break the policy + catch (ValidationFromCacheException | CacheException e) {// TODO wait Grégoire to provide gravitee CacheException + LOGGER.warn("Problem occurs on cache access, token is validated throught public key! Error is : "+e.getMessage()); + validateJsonWebToken(executionContext, jwt); + } + } + + /** + * This method is used to validate the JWT Token. + * It will check the signature (by using the public RSA key linked to the JWT issuer) + * Then it will check the expiration date provided in the token. + * @param executionContext ExecutionContext used to retrieve the public RSA key. + * @param jwt String Json Web Token + * @return Instant expiration provided in the token (may be cached) + */ + private Instant validateJsonWebToken(ExecutionContext executionContext, String jwt) { + + JwtParser jwtParser = Jwts.parser(); + + switch (configuration.getPublicKeyResolver()) { + case GIVEN_KEY: + String givenKey = configuration.getGivenKey(); + if(givenKey==null || givenKey.trim().equals("")) { + throw new IllegalArgumentException("No specified given key while expecting it due to policy settings."); + } + // Given endpoint can be defined as the template using EL + LOGGER.debug("Transform given key {} using template engine", givenKey); + givenKey = executionContext.getTemplateEngine().convert(givenKey); + jwtParser.setSigningKey(parsePublicKey(givenKey)); + break; + case GATEWAY_KEYS: + jwtParser.setSigningKeyResolver(getSigningKeyResolverByGatewaySettings(executionContext)); + break; + default: + throw new IllegalArgumentException("Unexpected public key resolver value."); + } + + final Jwt token = jwtParser.parse(jwt); + return ((DefaultClaims) token.getBody()).getExpiration().toInstant(); + } + + /** + * Return a SigingKeyResolver which will read iss claims value in order to get the associated public key. + * The associated public keys are set into the gateway settings and retrieved thanks to ExecutionContext. + * @param executionContext ExecutionContext + * @return SigningKeyResolver + */ + private SigningKeyResolver getSigningKeyResolverByGatewaySettings(ExecutionContext executionContext) { + return new SigningKeyResolverAdapter() { + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + + String keyId = header.getKeyId(); //or any other field that you need to inspect + final String iss = (String) claims.get(Claims.ISSUER); + + if (keyId == null || keyId.isEmpty()) { + keyId = DEFAULT_KID; + } + + Environment env = executionContext.getComponent(Environment.class); + String publicKey = env.getProperty(String.format(PUBLIC_KEY_PROPERTY, iss, keyId)); + if(publicKey==null || publicKey.trim().isEmpty()) { + return null; + } + return parsePublicKey(publicKey); + } + }; + } + + /** + * Generate RSA Public Key from the ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*) stored key. + * @param key String. + * @return RSAPublicKey + */ + static RSAPublicKey parsePublicKey(String key) { + Matcher m = SSH_PUB_KEY.matcher(key); + + if (m.matches()) { + String alg = m.group(1); + String encKey = m.group(2); + //String id = m.group(3); + + if (!"rsa".equalsIgnoreCase(alg)) { + throw new IllegalArgumentException("Only RSA is currently supported, but algorithm was " + alg); + } + + return parseSSHPublicKey(encKey); + } + + return null; + } + + /** + *
+     * Each rsa key should start with xxxxssh-rsa and then contains two big integer (modulus & exponent) which are prime number.
+     * The modulus & exponent are used to generate the RSA Public Key.
+     * See wiki explanations for deeper understanding
+     * 
+ * @param encKey String + * @return RSAPublicKey + */ + private static RSAPublicKey parseSSHPublicKey(String encKey) { + final byte[] PREFIX = new byte[] {0,0,0,7, 's','s','h','-','r','s','a'}; + ByteArrayInputStream in = new ByteArrayInputStream(Base64.getDecoder().decode(StandardCharsets.UTF_8.encode(encKey)).array()); + + byte[] prefix = new byte[11]; + + try { + if (in.read(prefix) != 11 || !Arrays.equals(PREFIX, prefix)) { + throw new IllegalArgumentException("SSH key prefix not found"); + } + + BigInteger e = new BigInteger(readBigInteger(in));//public exponent + BigInteger n = new BigInteger(readBigInteger(in));//modulus + + return createPublicKey(n, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static RSAPublicKey createPublicKey(BigInteger n, BigInteger e) { + try { + return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e)); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * bytes are not in the good order, they are in the big endian format, we reorder them before reading them... + * Each time you call this method, the buffer position will move, so result are differents... + * @param in byte array of a public encryption key without 11 "xxxxssh-rsa" first byte. + * @return BigInteger public exponent on first call, then modulus. + * @throws IOException + */ + private static byte[] readBigInteger(ByteArrayInputStream in) throws IOException { + byte[] b = new byte[4]; + + if (in.read(b) != 4) { + throw new IOException("Expected length data as 4 bytes"); + } + + int l = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; + + b = new byte[l]; + + if (in.read(b) != l) { + throw new IOException("Expected " + l + " key bytes"); + } + + return b; + } +} diff --git a/src/main/java/io/gravitee/policy/jwt/configuration/JWTPolicyConfiguration.java b/src/main/java/io/gravitee/policy/jwt/configuration/JWTPolicyConfiguration.java new file mode 100644 index 00000000..7d37085b --- /dev/null +++ b/src/main/java/io/gravitee/policy/jwt/configuration/JWTPolicyConfiguration.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.policy.jwt.configuration; + +import io.gravitee.policy.api.PolicyConfiguration; + +public class JWTPolicyConfiguration implements PolicyConfiguration { + + //settings attributes + private String givenKey; + private boolean useValidationCache = false; + private PublicKeyResolver publicKeyResolver = PublicKeyResolver.GIVEN_KEY; + + + //getter and setters + public PublicKeyResolver getPublicKeyResolver() { + return publicKeyResolver; + } + + public void setPublicKeyResolver(PublicKeyResolver publicKeyResolver) { + this.publicKeyResolver = publicKeyResolver; + } + + public String getGivenKey() { + return givenKey; + } + + public void setGivenKey(String givenKey) { + this.givenKey = givenKey; + } + + public boolean isUseValidationCache() { + return useValidationCache; + } + + public void setUseValidationCache(boolean useValidationCache) { + this.useValidationCache = useValidationCache; + } +} diff --git a/src/main/java/io/gravitee/policy/jwt/configuration/PublicKeyResolver.java b/src/main/java/io/gravitee/policy/jwt/configuration/PublicKeyResolver.java new file mode 100644 index 00000000..70d0ac81 --- /dev/null +++ b/src/main/java/io/gravitee/policy/jwt/configuration/PublicKeyResolver.java @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.policy.jwt.configuration; + + +public enum PublicKeyResolver { + GIVEN_KEY, + GATEWAY_KEYS +} diff --git a/src/main/java/io/gravitee/policy/jwt/exceptions/ValidationFromCacheException.java b/src/main/java/io/gravitee/policy/jwt/exceptions/ValidationFromCacheException.java new file mode 100644 index 00000000..6b959e44 --- /dev/null +++ b/src/main/java/io/gravitee/policy/jwt/exceptions/ValidationFromCacheException.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.policy.jwt.exceptions; + +/** + * Created by Alexandre on 20/07/2016. + */ +public class ValidationFromCacheException extends Exception{ + public ValidationFromCacheException(String message) { + super(message); + } +} diff --git a/src/main/resources/plugin.properties b/src/main/resources/plugin.properties new file mode 100644 index 00000000..7cd7ca45 --- /dev/null +++ b/src/main/resources/plugin.properties @@ -0,0 +1,6 @@ +id=jwt +name=JSON Web Tokens +version=${project.version} +description=${project.description} +class=io.gravitee.policy.jwt.JWTPolicy +type=policy diff --git a/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:jwt:configuration:JWTPolicyConfiguration.json b/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:jwt:configuration:JWTPolicyConfiguration.json new file mode 100644 index 00000000..8ecaa661 --- /dev/null +++ b/src/main/resources/schemas/urn:jsonschema:io:gravitee:policy:jwt:configuration:JWTPolicyConfiguration.json @@ -0,0 +1,26 @@ +{ + "type" : "object", + "id" : "urn:jsonschema:io:gravitee:policy:jwt:configuration:JWTPolicyConfiguration", + "properties" : { + "publicKeyResolver" : { + "title": "Public key resolver", + "description": "Select how public key is retrieved among : given key or gateway key settings...", + "type" : "string", + "default": "GIVEN_KEY", + "enum" : [ "GIVEN_KEY", "GATEWAY_KEYS" ] + }, + "givenKey" : { + "title": "Given public key (ssh-rsa ABCDE xxx@yyy.zz)", + "description": "The public key to use (needed if you select GIVEN_KEY resolver) (support EL).", + "type" : "string" + }, + "useValidationCache" : { + "title": "Use validation cache", + "description": "Cache validated JWT instead of checking signature on each API call", + "type" : "boolean" + } + }, + "required": [ + "useValidationCache" + ] +} \ No newline at end of file diff --git a/src/test/java/io/gravitee/policy/jwt/JWTPolicyTest.java b/src/test/java/io/gravitee/policy/jwt/JWTPolicyTest.java new file mode 100644 index 00000000..dd6fa46c --- /dev/null +++ b/src/test/java/io/gravitee/policy/jwt/JWTPolicyTest.java @@ -0,0 +1,468 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.policy.jwt; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.core.env.Environment; + +import io.gravitee.common.http.HttpHeaders; +import io.gravitee.gateway.api.ExecutionContext; +import io.gravitee.gateway.api.Request; +import io.gravitee.gateway.api.Response; +import io.gravitee.gateway.api.expression.TemplateEngine; +import io.gravitee.policy.api.PolicyChain; +import io.gravitee.policy.api.PolicyResult; +import io.gravitee.policy.jwt.configuration.JWTPolicyConfiguration; +import io.gravitee.policy.jwt.configuration.PublicKeyResolver; +import io.gravitee.repository.cache.api.CacheManager; +import io.gravitee.repository.cache.model.Cache; +import io.gravitee.repository.cache.model.Element; +import io.gravitee.repository.exceptions.CacheException; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +public class JWTPolicyTest { + + private static final String ISS = "gravitee.authorization.server"; + private static final String KID = "MAIN"; + + @Mock + private ExecutionContext executionContext; + @Mock + private Environment environment; + @Mock + private Request request; + @Mock + private Response response; + @Mock + private PolicyChain policyChain; + @Mock + private JWTPolicyConfiguration configuration; + @Mock + private CacheManager cacheManager; + @Mock + private Cache cache; + @Mock + private Element element; + @Mock + TemplateEngine templateEngine; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void test_with_cache_disabled_and_gateway_keys_and_valid_authorization_header() throws Exception { + + String jwt = getJsonWebToken(7200); + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test + public void test_with_cache_disabled_and_given_key_and_valid_authorization_header() throws Exception { + + String jwt = getJsonWebToken(7200); + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GIVEN_KEY); + when(configuration.getGivenKey()).thenReturn(getSshRsaKey()); + when(executionContext.getTemplateEngine()).thenReturn(templateEngine); + when(templateEngine.convert(getSshRsaKey())).thenReturn(getSshRsaKey()); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test + public void test_with_cache_disabled_and_given_key_using_EL_and_valid_authorization_header() throws Exception { + + String jwt = getJsonWebToken(7200); + final String property = "prop['key']"; + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GIVEN_KEY); + when(configuration.getGivenKey()).thenReturn(property); + when(executionContext.getTemplateEngine()).thenReturn(templateEngine); + when(templateEngine.convert(property)).thenReturn(getSshRsaKey()); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test + public void test_with_cache_disabled_and_given_key_but_not_provided() throws Exception { + + String jwt = getJsonWebToken(7200); + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GIVEN_KEY); + when(configuration.getGivenKey()).thenReturn(null); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,times(1)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(0)).doNext(request, response); + } + + @Test + public void test_with_cache_disabled_and_gateway_keys_and_valid_access_token() throws Exception { + + String jwt = getJsonWebToken(7200); + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + Map parameters = new HashMap(1); + parameters.put("access_token", jwt); + + when(request.headers()).thenReturn(new HttpHeaders()); + when(request.parameters()).thenReturn(parameters); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(request,times(1)).parameters(); + verify(policyChain,times(1)).doNext(request, response); + } + + @Test + public void test_with_cache_disabled_and_gateway_keys_and_expired_header_token() throws Exception { + + String jwt = getJsonWebToken(0); + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,times(1)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(0)).doNext(request, response); + } + + @Test + public void test_with_cache_disabled_and_gateway_keys_and_unknonw_issuer() throws Exception { + + String jwt = getJsonWebToken(7200,"unknown",null); + + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(false); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + verify(policyChain,times(1)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(0)).doNext(request, response); + } + + @Test + public void test_with_cache_enabled_and_valid_expiration_date_cache() throws Exception { + + String jwt = "jwtUsedAsKeyCache"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenReturn(cacheManager); + when(cacheManager.getCache(any(String.class))).thenReturn(cache); + when(cache.get(jwt)).thenReturn(element); + when(element.value()).thenReturn(Instant.now().plusSeconds(7200)); + + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test(expected = JwtException.class) + public void test_with_cache_enabled_and_non_valid_expiration_date_cache() throws Exception { + + String jwt = "jwtUsedAsKeyCache"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenReturn(cacheManager); + when(cacheManager.getCache(any(String.class))).thenReturn(cache); + when(cache.get(jwt)).thenReturn(element); + when(element.value()).thenReturn(Instant.now().minusSeconds(1)); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test + public void test_with_empty_cache_enabled_and_gateway_keys_and_valid_jwt() throws Exception { + + String jwt = getJsonWebToken(7200); + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenReturn(cacheManager); + when(cacheManager.getCache(any(String.class))).thenReturn(cache); + when(cache.get(jwt)).thenReturn(null); + when(element.value()).thenReturn(Instant.now().plusSeconds(7200)); + + JWTPolicy policy = new JWTPolicy(configuration); + + policy.onRequest(request, response, executionContext, policyChain);//1st time no cache found + when(cache.get(jwt)).thenReturn(element); + policy.onRequest(request, response, executionContext, policyChain);//2nd time cache found + + verify(cache,times(1)).put(any(Element.class)); + verify(policyChain,Mockito.times(2)).doNext(request, response); + } + + @Test + public void test_with_empty_cache_enabled_and_expired_jwt() throws Exception { + + String jwt = getJsonWebToken(0); + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenReturn(cacheManager); + when(cacheManager.getCache(any(String.class))).thenReturn(cache); + when(cache.get(jwt)).thenReturn(null); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,times(1)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(0)).doNext(request, response); + } + + @Test + public void test_with_cache_enabled_and_no_cache_manager() throws Exception { + + String jwt = getJsonWebToken(7200); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenReturn(null); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(0)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test + public void test_with_cache_enabled_and_no_cache_name() throws Exception { + + String jwt = getJsonWebToken(7200); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenReturn(cacheManager); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(0)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + @Test + public void test_with_cache_enabled_and_cache_exception() throws Exception { + + String jwt = getJsonWebToken(7200); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer "+jwt); + when(request.headers()).thenReturn(headers); + when(configuration.isUseValidationCache()).thenReturn(true); + when(configuration.getPublicKeyResolver()).thenReturn(PublicKeyResolver.GATEWAY_KEYS); + when(executionContext.getComponent(Environment.class)).thenReturn(environment); + when(environment.getProperty("policy.jwt.issuer.gravitee.authorization.server.MAIN")).thenReturn(getSshRsaKey()); + + //Mock cache actions. + when(executionContext.getComponent(CacheManager.class)).thenThrow(CacheException.class); + + new JWTPolicy(configuration).onRequest(request, response, executionContext, policyChain); + + verify(policyChain,Mockito.times(0)).failWith(any(PolicyResult.class)); + verify(policyChain,Mockito.times(1)).doNext(request, response); + } + + + //PRIVATE tools method for tests + /** + * Return Json Web Token string value. + * @return String + * @throws Exception + */ + private String getJsonWebToken(long secondsToAdd) throws Exception { + return getJsonWebToken(secondsToAdd,null,null); + } + + /** + * Return Json Web Token string value. + * @return String + * @throws Exception + */ + private String getJsonWebToken(long secondsToAdd, String iss, String kid) throws Exception{ + + Map header = new HashMap(2); + header.put("alg", "RS256"); + header.put("kid", kid!=null?kid:KID); + + JwtBuilder jwtBuilder = Jwts.builder(); + jwtBuilder.setHeader(header); + jwtBuilder.setSubject("alexluso"); + jwtBuilder.setIssuer(iss!=null?iss:ISS); + jwtBuilder.setExpiration(Date.from(Instant.now().plusSeconds(secondsToAdd))); + + jwtBuilder.signWith(SignatureAlgorithm.RS256, getPrivateKey()); + return jwtBuilder.compact(); + } + + /** + * How to generate keys? + * Run : ssh-keygen -t rsa -C "alex.luso@myCompany.com" + * ==> Will create id_rsa & id_rsa.pub + * Then run : openssl pkcs8 -topk8 -inform PEM -outform DER -in id_rsa -out private_key.der -nocrypt + * ==> Will create private_key.der unsecured that can be used. + * @return + * @throws Exception + */ + private PrivateKey getPrivateKey() throws Exception { + File file = new File(getClass().getClassLoader().getResource("private_key.der").getFile()); + FileInputStream fis = new FileInputStream(file); + DataInputStream dis = new DataInputStream(fis); + byte[] keyBytes = new byte[(int) file.length()]; + dis.readFully(keyBytes); + dis.close(); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("RSA"); + + return kf.generatePrivate(spec); + } + + /** + * Return string value of public key matching format ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*) + * @return String + * @throws IOException + */ + private String getSshRsaKey() throws IOException { + InputStream input = getClass().getClassLoader().getResourceAsStream("id_rsa.pub"); + BufferedReader buffer = new BufferedReader(new InputStreamReader(input)); + return buffer.lines().collect(Collectors.joining("\n")); + } +} diff --git a/src/test/resources/id_rsa b/src/test/resources/id_rsa new file mode 100644 index 00000000..359c0033 --- /dev/null +++ b/src/test/resources/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA43noN5Z2kxxzg4sy2vUwi87Qk+JpdXRGmHR4b4w1sAe8fcFX +Af066FFR8ph9Yu/+H1MoSBWnP+YsYjh8tByYuO9LanzUkKsNW97V3wQn5sFsHCdk +rF6xSsFRWWXCPjOYIVzxmuvrFLs7X9yUbjUxSzFd6mvN+PrUKUcJZfHkmaEblnHk +lKU0O2UtEIyJjBnp9XQYdYlY8FwhJ/z0FMMcaf+mv+NFrXll87Yz2hNFxFqAJyWB +3ogH0J5bWGPZcCnoRCTn+7CQn5fmmq4ocippjofjVPcQ/z1jB5MGUpWpQZ9pP6p9 +adPbN1AzWEKyVU6VCBvPJUyB4kgqqGl5d9JWDwIDAQABAoIBAQDXR30MdppbWVa9 +DFShweAiwCTHgEP8A4H4MGn2b4Qzxu6NORel60j/qk5awBQSOTyP2rxJlCyHncct +YXYrYtDqXJVL/z2QeEGZS4eumxlEGpO9BU8Sjj9Nlyzs5Q/ynBOCp5qD2nfNU/C6 +JWBX+IFhPyQ5gbMZyhBVzEPJtiZ5eKTF9qJuExrB9E1HzCrWxSftUBc9CkBiPntM +jA2Qq43Eh1Z2+/lcfT3Qc/7/vf3UBvf50J/3JejE6PAw4jNe04ozs9LHenKrLoRL +3dhuqHZPM1XEEH8xJEOnFngK4k4cA073gthL7fKz5kUAUPoAu8Im32Kpmg2WfVlT +bS1ZijFRAoGBAP+Vqa6MUddJoch3sipIIFXQFkI57oNQ57YRy8T9rj8ahLxuiLxF +1k/a5ihNGWvHChQhyARUaah0Z2O2tQ43ZJ7dZPabNwQov+COF5MGl/BxCkQ7QinY +aaGq8LUJLy3PdlyGUYHj9dC0gYfEWiV2t/l+ZPEUhFG30gXFZeobnINJAoGBAOPY +jLUXMNEK4Px1trhkExNVPcnlKzZztklvX6N1O8KgNrAPl64b2XoujINhohT86RX4 +weBR36r38UHtZWOpozW/1FOdQFY3z9+8crTJgZqpKaXKGTRL0OIoU2MEWqLlMSw2 +pZxAvB1RBYEmFX07SOMfaqSGLGUUBDN4XIAU07aXAoGAFHBqjmvoS5g22OpBlEIK +W/J1JTyux0+cCCJqMkm7Oo6rWMpaIvxOxDoUN9rakpTrSGrfLQF3JaKRdhbxab6i +TFYWMeZ9wtZjadjTJ83aLr9Le+NlSiVlZSlfcIrYfAhgRcv0DrglO1iEF1BriR1y +XwBtoB3s6wARSqbbnJoyrQkCgYA63aPc1ZUDLTBbiX4fvZtAD3HbS54Sf2rFJkUr +UgqSihoW+rBRh1h0vLoI55ycl4sQ5igQ8JY88bofMlpTmWxVYq5Uu/f3TowiXem0 +06rsbnAYKVLBtCTPiWOh3WodU+GUbrny2LbBTEGD0HcU19BI/cDrqM6nfrhnI92i +Kb9ZGQKBgGVeOyqSlXXE6mXLtSxSRpZ8S917xwgD1iHZIT42aYG5ER4MDUZ0+Wcw +7y9IjNxGIT1G2RIxMW880dVz/uXd6b7/XLn2J1JcvEIA+11OmBk2DfFtlDDlWWyz +fu00emXp/W9lLtTsN68ebEOY65hcOMSqZ8dC9j9bPfvqm29OXX9b +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/id_rsa.pub b/src/test/resources/id_rsa.pub new file mode 100644 index 00000000..da01480b --- /dev/null +++ b/src/test/resources/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjeeg3lnaTHHODizLa9TCLztCT4ml1dEaYdHhvjDWwB7x9wVcB/TroUVHymH1i7/4fUyhIFac/5ixiOHy0HJi470tqfNSQqw1b3tXfBCfmwWwcJ2SsXrFKwVFZZcI+M5ghXPGa6+sUuztf3JRuNTFLMV3qa834+tQpRwll8eSZoRuWceSUpTQ7ZS0QjImMGen1dBh1iVjwXCEn/PQUwxxp/6a/40WteWXztjPaE0XEWoAnJYHeiAfQnltYY9lwKehEJOf7sJCfl+aarihyKmmOh+NU9xD/PWMHkwZSlalBn2k/qn1p09s3UDNYQrJVTpUIG88lTIHiSCqoaXl30lYP alex.luso@myCompany.com diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 00000000..3e842fd9 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,22 @@ + + + + + + + %date{yyyy-MM-dd HH:mm:ss.SSS} [%-5p] %c: %m%n + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/private_key.der b/src/test/resources/private_key.der new file mode 100644 index 00000000..126f7dc7 Binary files /dev/null and b/src/test/resources/private_key.der differ