/* * Copyright 2018 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.openshift; import io.undertow.Undertow; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import org.jboss.arquillian.graphene.page.Page; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.ComponentResource; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.StreamUtil; import org.keycloak.events.Details; import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.openshift.OpenshiftClientStorageProviderFactory; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.ConsentPage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.OAuthClient; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.keycloak.common.Profile.Feature.OPENSHIFT_INTEGRATION; import static org.keycloak.testsuite.ProfileAssume.assumeFeatureEnabled; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; /** * Test that clients can override auth flows * * @author <a href="mailto:[email protected]">Pedro Igor</a> */ @AuthServerContainerExclude({AuthServer.REMOTE, AuthServer.QUARKUS}) @EnableFeature(value = OPENSHIFT_INTEGRATION, skipRestart = true) public final class OpenshiftClientStorageTest extends AbstractTestRealmKeycloakTest { private static Undertow OPENSHIFT_API_SERVER; @Rule public AssertEvents events = new AssertEvents(this); @Page private LoginPage loginPage; @Page private AppPage appPage; @Page private ConsentPage consentPage; @Page private ErrorPage errorPage; private String userId; private String clientStorageId; @Override public void configureTestRealm(RealmRepresentation testRealm) { } @BeforeClass public static void onBeforeClass() { OPENSHIFT_API_SERVER = Undertow.builder().addHttpListener(8880, "localhost", new HttpHandler() { @Override public void handleRequest(HttpServerExchange exchange) throws Exception { String uri = exchange.getRequestURI(); if (uri.endsWith("/version/openshift") || uri.endsWith("/version")) { writeResponse("openshift-version.json", exchange); } else if (uri.endsWith("/oapi")) { writeResponse("oapi-response.json", exchange); } else if (uri.endsWith("/apis")) { writeResponse("apis-response.json", exchange); } else if (uri.endsWith("/api")) { writeResponse("api.json", exchange); } else if (uri.endsWith("/api/v1")) { writeResponse("api-v1.json", exchange); } else if (uri.endsWith("/oapi/v1")) { writeResponse("oapi-v1.json", exchange); } else if (uri.contains("/apis/route.openshift.io/v1")) { writeResponse("apis-route-v1.json", exchange); } else if (uri.endsWith("/api/v1/namespaces/default")) { writeResponse("namespace-default.json", exchange); } else if (uri.endsWith("/oapi/v1/namespaces/default/routes/proxy")) { writeResponse("route-response.json", exchange); } else if (uri.contains("/serviceaccounts/system")) { writeResponse("sa-system.json", exchange); } else if (uri.contains("/serviceaccounts/")) { writeResponse(uri.substring(uri.lastIndexOf('/') + 1) + ".json", exchange); } } private void writeResponse(String file, HttpServerExchange exchange) throws IOException { exchange.getResponseSender().send(StreamUtil.readString(getClass().getResourceAsStream("/openshift/client-storage/" + file))); } }).build(); OPENSHIFT_API_SERVER.start(); } @AfterClass public static void onAfterClass() { if (OPENSHIFT_API_SERVER != null) { OPENSHIFT_API_SERVER.stop(); } } @Before public void onBefore() { assumeFeatureEnabled(OPENSHIFT_INTEGRATION); ComponentRepresentation provider = new ComponentRepresentation(); provider.setName("openshift-client-storage"); provider.setProviderId(OpenshiftClientStorageProviderFactory.PROVIDER_ID); provider.setProviderType(ClientStorageProvider.class.getName()); provider.setConfig(new MultivaluedHashMap<>()); provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_OPENSHIFT_URI, "http://localhost:8880"); provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_ACCESS_TOKEN, "token"); provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_DEFAULT_NAMESPACE, "default"); provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, "true"); Response resp = adminClient.realm("test").components().add(provider); resp.close(); clientStorageId = ApiUtil.getCreatedId(resp); getCleanup().addComponentId(clientStorageId); } @Before public void clientConfiguration() { userId = findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(); } @Test public void testCodeGrantFlowWithServiceAccountUsingOAuthRedirectReference() { String clientId = "system:serviceaccount:default:sa-oauth-redirect-reference"; testCodeGrantFlow(clientId, "https://myapp.org/callback", () -> assertSuccessfulResponseWithoutConsent(clientId)); } @Test public void failCodeGrantFlowWithServiceAccountUsingOAuthRedirectReference() throws Exception { testCodeGrantFlow("system:serviceaccount:default:sa-oauth-redirect-reference", "http://myapp.org/callback", () -> assertEquals(OAuthErrorException.INVALID_REDIRECT_URI, events.poll().getError())); } @Test public void testCodeGrantFlowWithServiceAccountUsingOAuthRedirectUri() { String clientId = "system:serviceaccount:default:sa-oauth-redirect-uri"; testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithoutConsent(clientId)); testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth/second", () -> assertSuccessfulResponseWithoutConsent(clientId)); testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth/third", () -> assertSuccessfulResponseWithoutConsent(clientId)); } @Test public void testCodeGrantFlowWithUserConsent() { String clientId = "system:serviceaccount:default:sa-oauth-redirect-uri"; testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithConsent(clientId), "user:info user:check-access"); ComponentResource component = testRealm().components().component(clientStorageId); ComponentRepresentation representation = component.toRepresentation(); representation.getConfig().put(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, Arrays.asList("false")); component.update(representation); testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithoutConsent(clientId), "user:info user:check-access"); representation.getConfig().put(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, Arrays.asList("true")); component.update(representation); testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithoutConsent(clientId, Details.CONSENT_VALUE_PERSISTED_CONSENT), "user:info user:check-access"); testRealm().users().get(userId).revokeConsent(clientId); testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithConsent(clientId), "user:info user:check-access"); } @Test public void failCodeGrantFlowWithServiceAccountUsingOAuthRedirectUri() throws Exception { testCodeGrantFlow("system:serviceaccount:default:sa-oauth-redirect-uri", "http://myapp.org/callback", () -> assertEquals(OAuthErrorException.INVALID_REDIRECT_URI, events.poll().getError())); } private void testCodeGrantFlow(String clientId, String expectedRedirectUri, Runnable assertThat) { testCodeGrantFlow(clientId, expectedRedirectUri, assertThat, null); } private void testCodeGrantFlow(String clientId, String expectedRedirectUri, Runnable assertThat, String scope) { if (scope != null) { oauth.scope(scope); } oauth.clientId(clientId); oauth.redirectUri(expectedRedirectUri); driver.navigate().to(oauth.getLoginFormUrl()); loginPage.assertCurrent(); try { // Fill username+password. I am successfully authenticated oauth.fillLoginForm("test-user@localhost", "password"); } catch (Exception ignore) { } assertThat.run(); } private void assertSuccessfulResponseWithoutConsent(String clientId) { assertSuccessfulResponseWithoutConsent(clientId, null); } private void assertSuccessfulResponseWithoutConsent(String clientId, String consentDetail) { AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().client(clientId).detail(Details.REDIRECT_URI, oauth.getRedirectUri()).detail(Details.USERNAME, "test-user@localhost"); if (consentDetail != null) { expectedEvent.detail(Details.CONSENT, Details.CONSENT_VALUE_PERSISTED_CONSENT); } expectedEvent.assertEvent(); assertSuccessfulRedirect(); } private void assertSuccessfulResponseWithConsent(String clientId) { consentPage.assertCurrent(); driver.getPageSource().contains("user:info"); driver.getPageSource().contains("user:check-access"); consentPage.confirm(); events.expectLogin().client(clientId).detail(Details.REDIRECT_URI, oauth.getRedirectUri()).detail(Details.USERNAME, "test-user@localhost").detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED).assertEvent(); assertSuccessfulRedirect("user:info", "user:check-access"); } private void assertSuccessfulRedirect(String... expectedScopes) { String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, null); String accessToken = tokenResponse.getAccessToken(); Assert.assertNotNull(accessToken); try { AccessToken token = new JWSInput(accessToken).readJsonContent(AccessToken.class); for (String expectedScope : expectedScopes) { token.getScope().contains(expectedScope); } } catch (Exception e) { fail("Failed to parse access token"); e.printStackTrace(); } Assert.assertNotNull(tokenResponse.getRefreshToken()); oauth.doLogout(tokenResponse.getRefreshToken(), null); events.clear(); } }