package io.apiman.plugins.keycloak_oauth_policy; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import io.apiman.gateway.engine.beans.ApiRequest; import io.apiman.gateway.engine.beans.PolicyFailure; import io.apiman.gateway.engine.components.IPolicyFailureFactoryComponent; import io.apiman.gateway.engine.components.ISharedStateComponent; import io.apiman.gateway.engine.impl.DefaultPolicyFailureFactoryComponent; import io.apiman.gateway.engine.impl.InMemorySharedStateComponent; import io.apiman.gateway.engine.policies.AuthorizationPolicy; import io.apiman.gateway.engine.policy.IPolicyChain; import io.apiman.gateway.engine.policy.IPolicyContext; import io.apiman.plugins.keycloak_oauth_policy.beans.ForwardAuthInfo; import io.apiman.plugins.keycloak_oauth_policy.beans.ForwardRoles; import io.apiman.plugins.keycloak_oauth_policy.beans.KeycloakOauthConfigBean; import java.io.IOException; import java.io.StringWriter; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Security; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Date; import java.util.HashSet; import java.util.Set; import javax.security.auth.x500.X500Principal; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; import org.bouncycastle.x509.X509V1CertificateGenerator; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.common.util.Time; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken.Access; import org.keycloak.representations.AddressClaimSet; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Test the {@link KeycloakOauthPolicy}. * * With thanks to the Keycloak project for their RSAVerifierTest whose setup procedures are adapted here for * our requirements. * * @author Marc Savy {@literal <msavy@redhat.com>} */ @SuppressWarnings({ "nls", "deprecation" }) public class KeycloakOauthPolicyTest { private static X509Certificate[] idpCertificates; private static KeyPair idpPair; private AccessToken token; private KeycloakOauthPolicy keycloakOauthPolicy; private KeycloakOauthConfigBean config; private ApiRequest apiRequest; @Mock private IPolicyChain<ApiRequest> mChain; @Mock private IPolicyContext mContext; private ForwardRoles forwardRoles; static { if (Security.getProvider("BC") == null) Security.addProvider(new BouncyCastleProvider()); } public static X509Certificate generateTestCertificate(String subject, String issuer, KeyPair pair) throws InvalidKeyException, NoSuchProviderException, SignatureException { X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); certGen.setIssuerDN(new X500Principal(issuer)); certGen.setNotBefore(new Date(System.currentTimeMillis() - 10000)); certGen.setNotAfter(new Date(System.currentTimeMillis() + 10000)); certGen.setSubjectDN(new X500Principal(subject)); certGen.setPublicKey(pair.getPublic()); certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); return certGen.generateX509Certificate(pair.getPrivate(), "BC"); } @BeforeClass public static void setupCerts() throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { idpPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); idpCertificates = new X509Certificate[] { generateTestCertificate("CN=IDP", "CN=IDP", idpPair) }; } @Before public void initTest() { MockitoAnnotations.initMocks(this); token = new AccessToken(); AccessToken realm = token.type("Bearer").subject("CN=Client").issuer("apiman-realm"); // KC seems to use issuer for realm? realm.addAccess("apiman-api").addRole("apiman-gateway-user-role").addRole("a-nother-role"); realm.setRealmAccess(new Access().addRole("lets-use-a-realm-role")); keycloakOauthPolicy = new KeycloakOauthPolicy(); config = new KeycloakOauthConfigBean(); config.setRequireOauth(true); config.setStripTokens(false); config.setBlacklistUnsafeTokens(false); config.setRequireTransportSecurity(false); forwardRoles = new ForwardRoles(); config.setForwardRoles(forwardRoles); apiRequest = new ApiRequest(); // Set up components. // Failure factory given(mContext.getComponent(IPolicyFailureFactoryComponent.class)). willReturn(new DefaultPolicyFailureFactoryComponent()); // Data store given(mContext.getComponent(ISharedStateComponent.class)). willReturn(new InMemorySharedStateComponent()); } private String generateAndSerializeToken() throws CertificateEncodingException, IOException { token.notBefore(Time.currentTime() - 100); config.setRealm("apiman-realm"); config.setRealmCertificateString(certificateAsPem(idpCertificates[0])); return new JWSBuilder().jsonContent(token).rsa256(idpPair.getPrivate()); } @Test public void shouldSucceedWithValidQueryAuthToken() throws CertificateEncodingException, IOException { String token = generateAndSerializeToken(); apiRequest.getQueryParams().put("access_token", token); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(1)).doApply(apiRequest); verify(mChain, never()).doFailure(any(PolicyFailure.class)); } @Test public void shouldSucceedWithValidHeaderAuthToken() throws CertificateEncodingException, IOException { String token = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + token); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(1)).doApply(apiRequest); verify(mChain, never()).doFailure(any(PolicyFailure.class)); } @Test public void shouldPassthroughOnNullTokenIfOAuthNotRequired() { config.setRequireOauth(false); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(any(ApiRequest.class)); } @Test public void shouldFailIfNoToken() throws CertificateEncodingException, IOException { config.setRealm("apiman-realm"); config.setRealmCertificateString(certificateAsPem(idpCertificates[0])); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(1)).doFailure(any(PolicyFailure.class)); verify(mChain, never()).doApply(any(ApiRequest.class)); } @Test public void shouldFailIfTokenNotYetValid() throws CertificateEncodingException, IOException { token.notBefore(Time.currentTime() + 9001); String encoded = new JWSBuilder().jsonContent(token).rsa256(idpPair.getPrivate()); apiRequest.getQueryParams().put("access_token", encoded); config.setRealm("apiman-realm"); config.setRealmCertificateString(certificateAsPem(idpCertificates[0])); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(1)).doFailure(any(PolicyFailure.class)); verify(mChain, never()).doApply(any(ApiRequest.class)); } @Test public void shouldFailOnInsecureConnection() throws CertificateEncodingException, IOException { // Require transport security config.setRequireTransportSecurity(true); // But set the connection as insecure apiRequest.setTransportSecure(false); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(1)).doFailure(any(PolicyFailure.class)); verify(mChain, never()).doApply(any(ApiRequest.class)); } @Test public void shouldBlacklistUnsafeToken() throws CertificateEncodingException, IOException { // Require transport security config.setRequireTransportSecurity(true); // Blacklist invalidly used tokens config.setBlacklistUnsafeTokens(true); // But set the connection as insecure apiRequest.setTransportSecure(false); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(1)).doFailure(any(PolicyFailure.class)); verify(mChain, never()).doApply(any(ApiRequest.class)); } @Test public void shouldTerminateOnBlacklistedToken() throws CertificateEncodingException, IOException { config.setRequireTransportSecurity(true); config.setBlacklistUnsafeTokens(true); apiRequest.setTransportSecure(false); // First, do a request that causes the token to be blacklisted. String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); // Second, do the request again with the blacklisted token *with secure*. // It *must* still be blocked. apiRequest.setTransportSecure(true); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain, times(2)).doFailure(any(PolicyFailure.class)); verify(mChain, never()).doApply(any(ApiRequest.class)); } @SuppressWarnings("serial") @Test public void shouldForwardAppRoles() throws CertificateEncodingException, IOException { forwardRoles.setActive(true); forwardRoles.setApplicationName("apiman-api"); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); Set<String> roles = new HashSet<String>() { { add("apiman-gateway-user-role"); add("a-nother-role"); } }; verify(mContext).setAttribute(eq(AuthorizationPolicy.AUTHENTICATED_USER_ROLES), eq(roles)); verify(mChain).doApply(any(ApiRequest.class)); } @Test public void shouldForwardRealmRoles() throws CertificateEncodingException, IOException { forwardRoles.setActive(true); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); @SuppressWarnings("serial") Set<String> roles = new HashSet<String>() { { add("lets-use-a-realm-role"); } }; verify(mContext).setAttribute(eq(AuthorizationPolicy.AUTHENTICATED_USER_ROLES), eq(roles)); verify(mChain).doApply(any(ApiRequest.class)); } @Test public void shouldForwardAuthInfoName() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("preferred_username"); config.getForwardAuthInfo().add(authInfo); token.setPreferredUsername("ABC"); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals("ABC", apiRequest.getHeaders().get("X-TEST")); } @Test public void shouldForwardToken() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("access_token"); config.getForwardAuthInfo().add(authInfo); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals(encoded, apiRequest.getHeaders().get("X-TEST")); } @Test public void shouldAccessSubClaim() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("address.street_address"); config.getForwardAuthInfo().add(authInfo); AddressClaimSet addressClaim = new AddressClaimSet(); addressClaim.setStreetAddress("123 newcastle street, miami"); token.setAddress(addressClaim); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals("123 newcastle street, miami", apiRequest.getHeaders().get("X-TEST")); } @Test public void shouldBeNullForInvalidNestedClaimLookup() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("address.street_address"); config.getForwardAuthInfo().add(authInfo); // Do *not* set street address String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals(null, apiRequest.getHeaders().get("X-TEST")); } @Test public void shouldBeNullForInvalidOtherClaimLookup() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("xxx"); config.getForwardAuthInfo().add(authInfo); // Do *not* set street address String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals(null, apiRequest.getHeaders().get("X-TEST")); } @Test public void shouldBeNullForUnsetStandardClaimLookup() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("email"); config.getForwardAuthInfo().add(authInfo); // Do *not* set street address String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals(null, apiRequest.getHeaders().get("X-TEST")); } @Test public void shouldForwardAuthInfoSubject() throws CertificateEncodingException, IOException { ForwardAuthInfo authInfo = new ForwardAuthInfo(); authInfo.setHeaders("X-TEST"); authInfo.setField("email"); config.getForwardAuthInfo().add(authInfo); token.setEmail("apiman@apiman.io"); String encoded = generateAndSerializeToken(); apiRequest.getHeaders().put("Authorization", "Bearer " + encoded); keycloakOauthPolicy.apply(apiRequest, mContext, config, mChain); verify(mChain).doApply(apiRequest); Assert.assertEquals("apiman@apiman.io", apiRequest.getHeaders().get("X-TEST")); } private String certificateAsPem(X509Certificate x509) throws CertificateEncodingException, IOException { StringWriter sw = new StringWriter(); PemWriter writer = new PemWriter(sw); PemObject pemObject = new PemObject("CERTIFICATE", x509.getEncoded()); try { writer.writeObject(pemObject); writer.flush(); } catch (IOException e) { throw new RuntimeException(e); } finally { writer.close(); } return sw.toString(); } }