/* * Copyright 2017 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * 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 org.keycloak.testsuite.jaas; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandles; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.AfterClass; import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.KeycloakPrincipal; import org.keycloak.adapters.jaas.AbstractKeycloakLoginModule; import org.keycloak.adapters.jaas.BearerTokenLoginModule; import org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule; import org.keycloak.adapters.jaas.RolePrincipal; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.admin.ApiUtil; import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.AUTH_SERVER_SSL_REQUIRED; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.utils.io.IOUtil; /** * @author <a href="mailto:[email protected]">Marek Posolda</a> */ @AuthServerContainerExclude(AuthServer.REMOTE) public class LoginModulesTest extends AbstractKeycloakTest { public static final URI DIRECT_GRANT_CONFIG; public static final URI BEARER_CONFIG; private static final File DIRECT_GRANT_CONFIG_FILE; private static final File BEARER_CONFIG_FILE; static { try { DIRECT_GRANT_CONFIG = MethodHandles.lookup().lookupClass().getResource("/adapter-test/customer-portal/WEB-INF/keycloak.json").toURI(); BEARER_CONFIG = MethodHandles.lookup().lookupClass().getResource("/adapter-test/customer-db-audience-required/WEB-INF/keycloak.json").toURI(); DIRECT_GRANT_CONFIG_FILE = File.createTempFile("LoginModulesTest", "testDirectAccessGrantLoginModuleLoginFailed"); BEARER_CONFIG_FILE = File.createTempFile("LoginModulesTest", "testBearerLoginFailedLogin"); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void addTestRealms(List<RealmRepresentation> testRealms) { testRealms.add(IOUtil.loadRealm("/adapter-test/demorealm.json")); } private static void enabled() { Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED); } @BeforeClass public static void createTemporaryFiles() throws Exception { enabled(); copyContentAndReplaceAuthServerAddress(new File(DIRECT_GRANT_CONFIG), DIRECT_GRANT_CONFIG_FILE); copyContentAndReplaceAuthServerAddress(new File(BEARER_CONFIG), BEARER_CONFIG_FILE); } @AfterClass public static void removeTemporaryFiles() { DIRECT_GRANT_CONFIG_FILE.deleteOnExit(); BEARER_CONFIG_FILE.deleteOnExit(); } private static void copyContentAndReplaceAuthServerAddress(File input, File output) throws IOException { try (InputStream inputStream = httpsAwareConfigurationStream(new FileInputStream(input))) { try (FileOutputStream outputStream = new FileOutputStream(output)) { byte[] buffer = new byte[inputStream.available()]; inputStream.read(buffer); outputStream.write(buffer); } } } @Before public void generateAudienceClientScope() { if (ApiUtil.findClientScopeByName(adminClient.realm("demo"), "customer-db-audience-required") != null) { return; } // Generate audience client scope String clientScopeId = testingClient.testing().generateAudienceClientScope("demo", "customer-db-audience-required"); ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("demo"), "customer-portal"); client.addOptionalClientScope(clientScopeId); } @Test public void testDirectAccessGrantLoginModuleLoginFailed() throws Exception { LoginContext loginContext = new LoginContext("does-not-matter", null, createJaasCallbackHandler("[email protected]", "bad-password"), createJaasConfigurationForDirectGrant(null)); try { loginContext.login(); Assert.fail("Not expected to successfully login"); } catch (LoginException le) { // Ignore } } @Test public void testDirectAccessGrantLoginModuleLoginSuccess() throws Exception { oauth.realm("demo"); LoginContext loginContext = directGrantLogin(null); Subject subject = loginContext.getSubject(); // Assert principals in subject KeycloakPrincipal principal = subject.getPrincipals(KeycloakPrincipal.class).iterator().next(); Assert.assertEquals("[email protected]", principal.getKeycloakSecurityContext().getToken().getPreferredUsername()); assertToken(principal.getKeycloakSecurityContext().getTokenString(), true); Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class); Assert.assertEquals(1, roles.size()); Assert.assertEquals("user", roles.iterator().next().getName()); // Logout and assert token not valid anymore loginContext.logout(); assertToken(principal.getKeycloakSecurityContext().getTokenString(), false); } @Test public void testBearerLoginFailedLogin() throws Exception { oauth.realm("demo"); LoginContext directGrantCtx = directGrantLogin(null); String accessToken = directGrantCtx.getSubject().getPrincipals(KeycloakPrincipal.class).iterator().next() .getKeycloakSecurityContext().getTokenString(); LoginContext bearerCtx = new LoginContext("does-not-matter", null, createJaasCallbackHandler("doesn-not-matter", accessToken), createJaasConfigurationForBearer()); // Login should fail due insufficient audience in the token try { bearerCtx.login(); Assert.fail("Not expected to successfully login"); } catch (LoginException le) { // Ignore } directGrantCtx.logout(); } @Test public void testBearerLoginSuccess() throws Exception { oauth.realm("demo"); LoginContext directGrantCtx = directGrantLogin("customer-db-audience-required"); String accessToken = directGrantCtx.getSubject().getPrincipals(KeycloakPrincipal.class).iterator().next() .getKeycloakSecurityContext().getTokenString(); LoginContext bearerCtx = new LoginContext("does-not-matter", null, createJaasCallbackHandler("doesn-not-matter", accessToken), createJaasConfigurationForBearer()); // Login should be successful bearerCtx.login(); // Assert subject Subject subject = bearerCtx.getSubject(); KeycloakPrincipal principal = subject.getPrincipals(KeycloakPrincipal.class).iterator().next(); Assert.assertEquals("[email protected]", principal.getKeycloakSecurityContext().getToken().getPreferredUsername()); assertToken(principal.getKeycloakSecurityContext().getTokenString(), true); Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class); Assert.assertEquals(1, roles.size()); Assert.assertEquals("user", roles.iterator().next().getName()); // Logout bearerCtx.logout(); directGrantCtx.logout(); } private LoginContext directGrantLogin(String scope) throws LoginException { LoginContext loginContext = new LoginContext("does-not-matter", null, createJaasCallbackHandler("[email protected]", "password"), createJaasConfigurationForDirectGrant(scope)); loginContext.login(); return loginContext; } private void assertToken(String accessToken, boolean expectActive) throws IOException { String introspectionResponse = oauth.introspectAccessTokenWithClientCredential("customer-portal", "password", accessToken); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(introspectionResponse); Assert.assertEquals(expectActive, jsonNode.get("active").asBoolean()); } private CallbackHandler createJaasCallbackHandler(final String principal, final String password) { return new CallbackHandler() { @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (Callback callback : callbacks) { if (callback instanceof NameCallback) { NameCallback nameCallback = (NameCallback) callback; nameCallback.setName(principal); } else if (callback instanceof PasswordCallback) { PasswordCallback passwordCallback = (PasswordCallback) callback; passwordCallback.setPassword(password.toCharArray()); } else { throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback.getClass().getCanonicalName()); } } } }; } private Configuration createJaasConfigurationForDirectGrant(String scope) { return new Configuration() { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { Map<String, Object> options = new HashMap<>(); options.put(AbstractKeycloakLoginModule.KEYCLOAK_CONFIG_FILE_OPTION, DIRECT_GRANT_CONFIG_FILE.getAbsolutePath()); if (scope != null) { options.put(DirectAccessGrantsLoginModule.SCOPE_OPTION, scope); } AppConfigurationEntry LMConfiguration = new AppConfigurationEntry(DirectAccessGrantsLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); return new AppConfigurationEntry[] { LMConfiguration }; } }; } private Configuration createJaasConfigurationForBearer() { return new Configuration() { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { Map<String, Object> options = new HashMap<>(); options.put(AbstractKeycloakLoginModule.KEYCLOAK_CONFIG_FILE_OPTION, BEARER_CONFIG_FILE.getAbsolutePath()); AppConfigurationEntry LMConfiguration = new AppConfigurationEntry(BearerTokenLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); return new AppConfigurationEntry[] { LMConfiguration }; } }; } }