/* * Copyright 2016 JBoss Inc * * 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.apiman.plugins.jwt; import io.apiman.gateway.engine.beans.PolicyFailure; import io.apiman.gateway.engine.beans.PolicyFailureType; import io.apiman.test.common.mock.EchoResponse; import io.apiman.test.policies.ApimanPolicyTest; import io.apiman.test.policies.Configuration; import io.apiman.test.policies.PolicyFailureError; import io.apiman.test.policies.PolicyTestRequest; import io.apiman.test.policies.PolicyTestRequestType; import io.apiman.test.policies.PolicyTestResponse; import io.apiman.test.policies.TestingPolicy; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Date; import org.junit.Assert; import org.junit.Test; import org.keycloak.common.util.PemUtils; /** * @author Marc Savy {@literal <msavy@redhat.com>} */ @TestingPolicy(JWTPolicy.class) @SuppressWarnings("nls") 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 AUTHORIZATION = "Authorization"; @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": true,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": false,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void signedValidToken() throws Throwable { String authVal = "Bearer " + signedToken(); 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\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void signedValidTokenStripAuth() throws Throwable { PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header(AUTHORIZATION, "Bearer " + signedToken()); PolicyTestResponse response = send(request); EchoResponse echo = response.entity(EchoResponse.class); Assert.assertNotNull(echo); Assert.assertNull(echo.getHeaders().get(AUTHORIZATION)); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void unsignedValidTokenHeader() throws Throwable { PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header(AUTHORIZATION, "Bearer " + unsignedToken()); PolicyTestResponse response = send(request); EchoResponse echo = response.entity(EchoResponse.class); Assert.assertNotNull(echo); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"aride\" }],\n" + " \"forwardAuthInfo\": [{ \"header\": \"X-Foo\", \"field\": \"sub\" }]\n" + "}" ) public void shouldForwardClaimsAsHeaders() throws Throwable { PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header(AUTHORIZATION, "Bearer " + Jwts.builder().setSubject("aride").compact()); PolicyTestResponse response = send(request); EchoResponse echo = response.entity(EchoResponse.class); Assert.assertNotNull(echo); Assert.assertEquals("aride", echo.getHeaders().get("X-Foo")); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }],\n" + " \"forwardAuthInfo\": [{ \"header\": \"X-Foo\", \"field\": \"access_token\" }]\n" + "}" ) public void shouldForwardAccessTokenAsHeader() throws Throwable { String token = unsignedToken(); PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header(AUTHORIZATION, "Bearer " + token); PolicyTestResponse response = send(request); EchoResponse echo = response.entity(EchoResponse.class); Assert.assertNotNull(echo); Assert.assertEquals(token, echo.getHeaders().get("X-Foo")); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void unsignedValidTokenQueryParam() throws Throwable { PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .query("access_token", unsignedToken()); PolicyTestResponse response = send(request); EchoResponse echo = response.entity(EchoResponse.class); Assert.assertNotNull(echo); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void shouldFailWhenNoTokenProvided() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante"); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12005, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void shouldFailWhenTokenInvalid() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header("Authorization", "Bearer <Obviously invalid token>"); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12007, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void shouldFailWhenTokenNotYetValid() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header("Authorization", "Bearer " + unsignedNotYetValidToken()); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12010, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"france frichot\" }]\n" + "}" ) public void shouldFailWhenTokenExpired() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header("Authorization", "Bearer " + unsignedExpiredToken()); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12006, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"will_not_match\" }]\n" + "}" ) public void shouldFailWithUnexpectedClaimValue() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header("Authorization", "Bearer " + unsignedToken()); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12009, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"will_not_match\" }]\n" + "}" ) public void shouldFailWithMissingClaim() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header("Authorization", "Bearer " + Jwts.builder().claim("x", "x").compact()); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12009, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } @Test @Configuration("{\n" + " \"requireJWT\": true,\n" + " \"requireSigned\": false,\n" + " \"requireTransportSecurity\": true,\n" + " \"stripTokens\": true,\n" + " \"signingKeyString\": \""+ PUBLIC_KEY_PEM +"\",\n" + " \"allowedClockSkew\": 0,\n" + " \"requiredClaims\": [{ \"claimName\": \"sub\", \"claimValue\": \"will_not_match\" }]\n" + "}" ) public void shouldFailWithNoTls() throws Throwable { PolicyFailure failure = null; PolicyTestRequest request = PolicyTestRequest.build(PolicyTestRequestType.GET, "/amirante") .header("Authorization", "Bearer " + Jwts.builder().claim("x", "x").compact()); try { send(request); } catch (PolicyFailureError pfe) { failure = pfe.getFailure(); } Assert.assertNotNull(failure); Assert.assertEquals(401, failure.getResponseCode()); Assert.assertEquals(12009, failure.getFailureCode()); Assert.assertEquals(PolicyFailureType.Authentication, failure.getType()); } private String signedToken() throws Exception { JwtBuilder jwts = Jwts.builder().setSubject("france frichot") .signWith(SignatureAlgorithm.RS256, PemUtils.decodePrivateKey(PRIVATE_KEY_PEM)); return jwts.compact(); } private String unsignedToken() throws Exception { JwtBuilder jwts = Jwts.builder().setSubject("france frichot"); return jwts.compact(); } private String unsignedNotYetValidToken() throws Exception { Instant instant = LocalDateTime.now().plusDays(5).toInstant(ZoneOffset.UTC); Date nbf = Date.from(instant); JwtBuilder jwts = Jwts.builder().setSubject("france frichot").setNotBefore(nbf); return jwts.compact(); } private String unsignedExpiredToken() throws Exception { Instant instant = LocalDateTime.now().minusDays(5).toInstant(ZoneOffset.UTC); Date exp = Date.from(instant); JwtBuilder jwts = Jwts.builder().setSubject("france frichot").setExpiration(exp); return jwts.compact(); } }