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" +
+ " ")
+ .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) {