diff --git a/integration-tests/oidc-wiremock/pom.xml b/integration-tests/oidc-wiremock/pom.xml index ab42bf7e4f224..d09aa282c5451 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 0000000000000..cd68880ea8552 --- /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 0000000000000..4b8c71899a984 --- /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 7756f4c596aa7..9353a7002b92f 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 0000000000000..5478b4810c03b --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -0,0 +1,43 @@ +package io.quarkus.it.keycloak; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@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("alice", page.getBody().asText()); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + +} 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 a1eda7a792ca7..0fa620f517f80 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,14 +1,18 @@ package io.quarkus.it.keycloak; 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.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -18,9 +22,11 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; import com.google.common.collect.ImmutableSet; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.smallrye.jwt.build.Jwt; public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager { @@ -31,7 +37,10 @@ public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager @Override public Map start() { - server = new WireMockServer(wireMockConfig().dynamicPort()); + server = new WireMockServer( + wireMockConfig() + .extensions(new ResponseTemplateTransformer(false)) + .dynamicPort()); server.start(); server.stubFor( @@ -41,10 +50,15 @@ public Map start() { .withBody("{\n" + " \"jwks_uri\": \"" + server.baseUrl() + "/auth/realms/quarkus/protocol/openid-connect/certs\",\n" + + " \"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() - + "/auth/realms/quarkus/protocol/openid-connect/token/introspect\",\n" - + "\"issuer\" : \"https://server.example.com\"" - + "}"))); + + "/auth/realms/quarkus/protocol/openid-connect/token/introspect\"" + + + "}"))); server.stubFor( get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/certs")) @@ -72,8 +86,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 +151,30 @@ 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\": \"" + + getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin"))) + "\",\n" + + " \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\",\n" + + " \"id_token\": \"" + getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin"))) + + "\"\n" + + "}"))); + } + + private String getAccessToken(String userName, Set groups) { + return Jwt.preferredUserName(userName) + .groups(groups) + .issuer("https://server.example.com") + .audience("https://service.example.com") + .jws() + .keyId("1") + .sign("privateKey.jwk"); + } + @Override public synchronized void stop() { if (server != null) {