From a972c785a42b0c71e875fb987c6c9522c3b7d2ba Mon Sep 17 00:00:00 2001 From: Cem Nura Date: Fri, 8 Jan 2021 00:44:03 +0300 Subject: [PATCH] Added code flow authorization it test with wiremock stubbing Signed-off-by: Cem Nura Added code & grant_type to login success redirect Signed-off-by: Cem Nura Added token mock for code flow authentication Signed-off-by: Cem Nura Corrected token mock for code flow authentication Signed-off-by: Cem Nura --- integration-tests/oidc-wiremock/pom.xml | 11 +++ .../quarkus/it/keycloak/CodeFlowResource.java | 21 ++++ .../it/keycloak/CustomTenantResolver.java | 19 ++++ .../src/main/resources/application.properties | 9 +- .../keycloak/CodeFlowAuthorizationTest.java | 60 +++++++++++ .../it/keycloak/KeycloakTestResource.java | 99 +++++++++++++++---- 6 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java create mode 100644 integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java diff --git a/integration-tests/oidc-wiremock/pom.xml b/integration-tests/oidc-wiremock/pom.xml index ab42bf7e4f224b..d09aa282c54519 100644 --- a/integration-tests/oidc-wiremock/pom.xml +++ b/integration-tests/oidc-wiremock/pom.xml @@ -48,6 +48,17 @@ jakarta.servlet-api test + + net.sourceforge.htmlunit + htmlunit + test + + + org.eclipse.jetty + * + + + io.quarkus diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java new file mode 100644 index 00000000000000..cd68880ea85522 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/code-flow") +@Authenticated +public class CodeFlowResource { + + @Inject + SecurityIdentity identity; + + @GET + public String access() { + return identity.getPrincipal().getName(); + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java new file mode 100644 index 00000000000000..4b8c71899a9842 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -0,0 +1,19 @@ +package io.quarkus.it.keycloak; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.oidc.TenantResolver; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomTenantResolver implements TenantResolver { + + @Override + public String resolve(RoutingContext context) { + String path = context.normalisedPath(); + if (path.endsWith("code-flow")) { + return "code-flow"; + } + return null; + } +} \ No newline at end of file diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 7756f4c596aa77..9353a7002b92f1 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -2,7 +2,14 @@ quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret -quarkus.oidc.token.principal-claim=email +quarkus.oidc.authentication.scopes=profile,email,phone + +quarkus.oidc.code-flow.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow.client-id=quarkus-web-app +quarkus.oidc.code-flow.credentials.secret=secret +quarkus.oidc.code-flow.application-type=web-app +quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".min-level=TRACE +quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".level=TRACE quarkus.oidc.token.audience=https://service.example.com diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java new file mode 100644 index 00000000000000..ec87544f59bcf6 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -0,0 +1,60 @@ +package io.quarkus.it.keycloak; + +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.util.Cookie; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@QuarkusTest +@QuarkusTestResource(KeycloakTestResource.class) +public class CodeFlowAuthorizationTest { + + @Test + public void testCodeFlow() throws IOException { + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/code-flow"); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + page = form.getInputByValue("login").click(); + + assertEquals("Welcome to Test App", page.getTitleText()); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + + private void verifyLocationHeader(WebClient webClient, String loc, String tenant, String path, boolean forceHttps) { + assertTrue(loc.contains("/auth")); + String scheme = forceHttps ? "https" : "http"; + assertTrue(loc.contains("redirect_uri=" + scheme + "%3A%2F%2Flocalhost%3A8081%2F" + path)); + assertTrue(loc.contains("state=" + getStateCookieStateParam(webClient, tenant))); + assertTrue(loc.contains("scope=openid")); + assertTrue(loc.contains("response_type=code")); + assertTrue(loc.contains("client_id=quarkus-web-app")); + } + + private Cookie getStateCookie(WebClient webClient, String tenantId) { + return webClient.getCookieManager().getCookie("q_auth" + (tenantId == null ? "" : "_" + tenantId)); + } + + private String getStateCookieStateParam(WebClient webClient, String tenantId) { + return getStateCookie(webClient, tenantId).getValue().split("\\|")[0]; + } +} diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java index a1eda7a792ca78..1874138d6ba2c2 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java @@ -1,27 +1,28 @@ package io.quarkus.it.keycloak; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; +import com.google.common.collect.ImmutableSet; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.jboss.logging.Logger; + +import javax.ws.rs.core.MediaType; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.util.Collections.emptySet; import static java.util.stream.Collectors.joining; -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -import javax.ws.rs.core.MediaType; - -import org.jboss.logging.Logger; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.google.common.collect.ImmutableSet; - -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; - public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager { private static final Logger LOG = Logger.getLogger(KeycloakTestResource.class); @@ -31,7 +32,11 @@ public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager @Override public Map start() { - server = new WireMockServer(wireMockConfig().dynamicPort()); + server = new WireMockServer( + wireMockConfig() + .extensions(new ResponseTemplateTransformer(false)) + .notifier(new ConsoleNotifier(true)) + .dynamicPort()); server.start(); server.stubFor( @@ -41,10 +46,13 @@ public Map start() { .withBody("{\n" + " \"jwks_uri\": \"" + server.baseUrl() + "/auth/realms/quarkus/protocol/openid-connect/certs\",\n" + - " \"introspection_endpoint\": \"" + server.baseUrl() - + "/auth/realms/quarkus/protocol/openid-connect/token/introspect\",\n" - + "\"issuer\" : \"https://server.example.com\"" - + "}"))); + " \"token_introspection_endpoint\": \"" + server.baseUrl() + + "/auth/realms/quarkus/protocol/openid-connect/token/introspect\",\n" + + " \"authorization_endpoint\": \"" + server.baseUrl() + "/auth/realms/quarkus\"," + + " \"token_endpoint\": \"" + server.baseUrl() + "/auth/realms/quarkus/token\"," + + " \"issuer\" : \"https://server.example.com\"" + + " \"introspection_endpoint\": \"" + server.baseUrl() + + "}"))); server.stubFor( get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/certs")) @@ -72,8 +80,45 @@ public Map start() { // Invalid defineInvalidIntrospectionMockTokenStubForUserWithRoles("expired", emptySet()); + // Code Flow Authorization Mock + defineCodeFlowAuthorizationMockTokenStub(); + + // Login Page + server.stubFor( + get(urlPathMatching("/auth/realms/quarkus")) + .willReturn(aResponse() + .withHeader("Content-Type", MediaType.TEXT_HTML) + .withBody("\n" + + "\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + + " \n" + + "
\n" + + "\n" + + " ") + .withTransformers("response-template"))); + + // Login Request + server.stubFor( + get(urlPathMatching("/login")) + .willReturn(aResponse() + .withHeader("Location", + "{{request.query.redirect_uri}}?state={{request.query.state}}&code=58af24f2-9093-4674-a431-4a9d66be719c.50437113-cd78-48a2-838e-b936fe458c5d.0ac5df91-e044-4051-bd03-106a3a5fb9cc") + .withStatus(302) + .withTransformers("response-template"))); + LOG.infof("Keycloak started in mock mode: %s", server.baseUrl()); - return Collections.singletonMap("quarkus.oidc.auth-server-url", server.baseUrl() + "/auth/realms/quarkus"); + Map conf = new HashMap<>(); + conf.put("quarkus.oidc.auth-server-url", server.baseUrl() + "/auth/realms/quarkus"); + conf.put("quarkus.oidc.code-flow.auth-server-url", server.baseUrl() + "/auth/realms/quarkus"); + conf.put("keycloak-url", server.baseUrl()); + + return conf; } private void defineValidIntrospectionMockTokenStubForUserWithRoles(String user, Set roles) { @@ -100,6 +145,20 @@ private void defineInvalidIntrospectionMockTokenStubForUserWithRoles(String user + "\",\"iat\":1562315654,\"exp\":1,\"expires_in\":1,\"client_id\":\"my_client_id\"}"))); } + private void defineCodeFlowAuthorizationMockTokenStub() { + server.stubFor(WireMock.post("/auth/realms/quarkus/token") + .withRequestBody(containing("authorization_code")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody("{\n" + + " \"access_token\": \"eyJraWQiOiIxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdyb3VwcyI6WyJhZG1pbiIsInVzZXIiXSwiaWF0IjoxNjE1NDIzMDgxLCJleHAiOjE2MTU0MjMzODEsImp0aSI6IjIwMTgyNTMxLTQ5ZWUtNDI2OC04YjM5LWQwN2Q5NDQ1OGYyMSJ9.OvSV8ZVY5cUj7nmqJRmlMz6S--wqedm3Fnz8lMPGUkeyAUbPm8_N6aQi4VgduSPbCrH-2-Kgppoh4w1Oy203kVaG_-KV5PgS7Yesrq6SDp3g9I3WL6UYXdM1nRSoDDGYxhWCdJ1TMwhjV8a7HV_M1PtbLfaR_7Do8_BDLhmc_lSWt_ZLiUqweXf-RJw7hm8aNuZ_OaYhiDd_tII6Ldn5CJfXpWf1Sm8x09cieNzvGDxxP9Dn7FSSMX0I9JPsCCPV6pvmrPDNej2bonZPrHG1QU6ChmO6WSwTAyNE0Cj9z_n7UxltI6AAhk8DlGo18voqogasg4uEgp1FhUdmWenaNQ\",\n" + + + " \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\",\n" + + " \"id_token\": \"eyJraWQiOiIxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdyb3VwcyI6WyJhZG1pbiIsInVzZXIiXSwiaWF0IjoxNjE1NDIzMDgxLCJleHAiOjE2MTU0MjMzODEsImp0aSI6IjIwMTgyNTMxLTQ5ZWUtNDI2OC04YjM5LWQwN2Q5NDQ1OGYyMSJ9.OvSV8ZVY5cUj7nmqJRmlMz6S--wqedm3Fnz8lMPGUkeyAUbPm8_N6aQi4VgduSPbCrH-2-Kgppoh4w1Oy203kVaG_-KV5PgS7Yesrq6SDp3g9I3WL6UYXdM1nRSoDDGYxhWCdJ1TMwhjV8a7HV_M1PtbLfaR_7Do8_BDLhmc_lSWt_ZLiUqweXf-RJw7hm8aNuZ_OaYhiDd_tII6Ldn5CJfXpWf1Sm8x09cieNzvGDxxP9Dn7FSSMX0I9JPsCCPV6pvmrPDNej2bonZPrHG1QU6ChmO6WSwTAyNE0Cj9z_n7UxltI6AAhk8DlGo18voqogasg4uEgp1FhUdmWenaNQ\"\n" + + + "}"))); + } + @Override public synchronized void stop() { if (server != null) {