Skip to content

Commit

Permalink
Merge pull request #43565 from sberyozkin/oidc_test_with_dev_service
Browse files Browse the repository at this point in the history
Use Keycloak devservice for OIDC MTLS authentication
  • Loading branch information
sberyozkin authored Sep 27, 2024
2 parents 825848d + b28fbb7 commit 1ff7c69
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public class KeycloakDevServicesProcessor {

private static final String KEYCLOAK_CONTAINER_NAME = "keycloak";
private static final int KEYCLOAK_PORT = 8080;
private static final int KEYCLOAK_HTTPS_PORT = 8443;

private static final String KEYCLOAK_LEGACY_IMAGE_VERSION_PART = "-legacy";

Expand Down Expand Up @@ -253,8 +254,8 @@ public void run() {
return devService.toBuildItem();
}

private String startURL(String host, Integer port, boolean isKeycloakX) {
return "http://" + host + ":" + port + (isKeycloakX ? "" : "/auth");
private String startURL(String scheme, String host, Integer port, boolean isKeycloakX) {
return scheme + host + ":" + port + (isKeycloakX ? "" : "/auth");
}

private Map<String, String> prepareConfiguration(
Expand Down Expand Up @@ -383,10 +384,12 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild
oidcContainer.withEnv(capturedDevServicesConfiguration.containerEnv);
oidcContainer.start();

String internalUrl = startURL(oidcContainer.getHost(), oidcContainer.getPort(), oidcContainer.keycloakX);
String internalUrl = startURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(),
oidcContainer.getPort(), oidcContainer.keycloakX);
String hostUrl = oidcContainer.useSharedNetwork
// we need to use auto-detected host and port, so it works when docker host != localhost
? startURL(oidcContainer.getSharedNetworkExternalHost(), oidcContainer.getSharedNetworkExternalPort(),
? startURL("http://", oidcContainer.getSharedNetworkExternalHost(),
oidcContainer.getSharedNetworkExternalPort(),
oidcContainer.keycloakX)
: null;

Expand Down Expand Up @@ -518,6 +521,9 @@ protected void configure() {
withCommand(startCommand.orElse(KEYCLOAK_QUARKUS_START_CMD)
+ (useSharedNetwork ? " --hostname-port=" + fixedExposedPort.getAsInt() : ""));
addUpConfigResource();
if (isHttps()) {
addExposedPort(KEYCLOAK_HTTPS_PORT);
}
} else {
addEnv(KEYCLOAK_WILDFLY_USER_PROP, KEYCLOAK_ADMIN_USER);
addEnv(KEYCLOAK_WILDFLY_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD);
Expand Down Expand Up @@ -639,7 +645,11 @@ public int getPort() {
if (fixedExposedPort.isPresent()) {
return fixedExposedPort.getAsInt();
}
return getFirstMappedPort();
return super.getMappedPort(isHttps() ? KEYCLOAK_HTTPS_PORT : KEYCLOAK_PORT);
}

public boolean isHttps() {
return startCommand.isPresent() && startCommand.get().contains("--https");
}
}

Expand Down
17 changes: 4 additions & 13 deletions integration-tests/oidc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.angus</groupId>
Expand All @@ -46,10 +40,7 @@
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
Expand Down
21 changes: 12 additions & 9 deletions integration-tests/oidc/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
# Configuration file
quarkus.oidc.auth-server-url=replaced-by-tests
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials.secret=secret
quarkus.keycloak.devservices.create-realm=false
quarkus.keycloak.devservices.start-command=start --https-client-auth=required --hostname-strict=false --https-key-store-file=/etc/server-keystore.p12 --https-trust-store-file=/etc/server-truststore.p12 --https-trust-store-password=password --spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json
quarkus.keycloak.devservices.resource-aliases.keystore=server-keystore.p12
quarkus.keycloak.devservices.resource-aliases.truststore=server-truststore.p12
quarkus.keycloak.devservices.resource-mappings.keystore=/etc/server-keystore.p12
quarkus.keycloak.devservices.resource-mappings.truststore=/etc/server-truststore.p12

quarkus.oidc.token.principal-claim=email

quarkus.oidc.tls.verification=required
quarkus.oidc.tls.trust-store-file=client-truststore.jks
quarkus.oidc.tls.trust-store-file=client-truststore.p12
quarkus.oidc.tls.trust-store-password=password
quarkus.oidc.tls.key-store-file=client-keystore.jks
quarkus.oidc.tls.key-store-file=client-keystore.p12
quarkus.oidc.tls.key-store-password=password

%tls-registry.quarkus.oidc.tls.tls-configuration-name=oidc-tls
%tls-registry.quarkus.tls.oidc-tls.key-store.jks.path=client-keystore.jks
%tls-registry.quarkus.tls.oidc-tls.key-store.jks.path=client-keystore.p12
%tls-registry.quarkus.tls.oidc-tls.key-store.jks.password=password
%tls-registry.quarkus.tls.oidc-tls.trust-store.jks.path=client-truststore.jks
%tls-registry.quarkus.tls.oidc-tls.trust-store.jks.path=client-truststore.p12
%tls-registry.quarkus.tls.oidc-tls.trust-store.jks.password=password
%tls-registry.quarkus.oidc.tls.verification=
%tls-registry.quarkus.oidc.tls.trust-store-file=
%tls-registry.quarkus.oidc.tls.trust-store-password=
%tls-registry.quarkus.oidc.tls.key-store-file=
%tls-registry.quarkus.oidc.tls.key-store-password=

quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks
quarkus.native.additional-build-args=-H:IncludeResources=.*\\.p12

quarkus.http.cors=true
quarkus.http.cors.origins=*
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.quarkus.it.keycloak;

import static io.quarkus.it.keycloak.KeycloakXTestResourceLifecycleManager.getAccessToken;
import static io.quarkus.it.keycloak.KeycloakXTestResourceLifecycleManager.getRefreshToken;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;

Expand All @@ -12,10 +10,14 @@
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.quarkus.test.keycloak.client.KeycloakTestClient.Tls;
import io.restassured.RestAssured;

public abstract class AbstractBearerTokenAuthorizationTest {

KeycloakTestClient client = new KeycloakTestClient(new Tls());

@Test
public void testSecureAccessSuccessWithCors() {
String origin = "http://custom.origin.quarkus";
Expand Down Expand Up @@ -109,7 +111,7 @@ public void testAccessAdminResourceCustomHeaderBearerScheme() {

@Test
public void testAccessAdminResourceWithRefreshToken() {
RestAssured.given().auth().oauth2(getRefreshToken("admin"))
RestAssured.given().auth().oauth2(client.getRefreshToken("admin"))
.when().get("/api/admin")
.then()
.statusCode(401);
Expand Down Expand Up @@ -225,4 +227,8 @@ public void testAuthenticationEvent() {
.statusCode(200)
.body(Matchers.is("true"));
}

String getAccessToken(String username) {
return client.getAccessToken(username);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.quarkus.it.keycloak;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;

import org.junit.jupiter.api.Test;

Expand All @@ -14,6 +14,6 @@ public class BearerTokenAuthorizationInGraalITCase extends BearerTokenAuthorizat

@Test
public void testDevServicesProperties() {
assertThat(context.devServicesProperties()).isEmpty();
assertFalse(context.devServicesProperties().isEmpty());
}
}
Original file line number Diff line number Diff line change
@@ -1,85 +1,36 @@
package io.quarkus.it.keycloak;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.jboss.logging.Logger;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.util.JsonSerialization;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.quarkus.test.keycloak.client.KeycloakTestClient.Tls;

public class KeycloakXTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager {
private static final Logger LOGGER = Logger.getLogger(KeycloakXTestResourceLifecycleManager.class);
private GenericContainer<?> keycloak;
public class KeycloakXTestResourceLifecycleManager
implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {

private static String KEYCLOAK_SERVER_URL;
private static final String KEYCLOAK_REALM = "quarkus";
private static final String KEYCLOAK_SERVICE_CLIENT = "quarkus-service-app";
private static final String KEYCLOAK_VERSION = System.getProperty("keycloak.version", "23.0.1");
private static final String KEYCLOAK_SERVICE_CLIENT = "quarkus-app";

private static String CLIENT_KEYSTORE = "client-keystore.jks";
private static String CLIENT_TRUSTSTORE = "client-truststore.jks";
final KeycloakTestClient client = new KeycloakTestClient(new Tls());

private static String SERVER_KEYSTORE = "server-keystore.jks";
private static String SERVER_KEYSTORE_MOUNTED_PATH = "/etc/server-keystore.jks";
private static String SERVER_TRUSTSTORE = "server-truststore.jks";
private static String SERVER_TRUSTSTORE_MOUNTED_PATH = "/etc/server-truststore.jks";

@SuppressWarnings("resource")
@Override
public Map<String, String> start() {
keycloak = new GenericContainer<>("quay.io/keycloak/keycloak:" + KEYCLOAK_VERSION)
.withExposedPorts(8080, 8443)
.withEnv("KEYCLOAK_ADMIN", "admin")
.withEnv("KEYCLOAK_ADMIN_PASSWORD", "admin")
.waitingFor(Wait.forLogMessage(".*Keycloak.*started.*", 1));

keycloak = keycloak
.withClasspathResourceMapping(SERVER_KEYSTORE, SERVER_KEYSTORE_MOUNTED_PATH, BindMode.READ_ONLY)
.withClasspathResourceMapping(SERVER_TRUSTSTORE, SERVER_TRUSTSTORE_MOUNTED_PATH, BindMode.READ_ONLY)
.withClasspathResourceMapping("/upconfig.json", "/opt/keycloak/upconfig.json", BindMode.READ_ONLY)
.withCommand("build --https-client-auth=required")
.withCommand(String.format(
"start --https-client-auth=required --hostname-strict=false"
+ " --https-key-store-file=%s --https-trust-store-file=%s --https-trust-store-password=password"
+ " --spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json",
SERVER_KEYSTORE_MOUNTED_PATH, SERVER_TRUSTSTORE_MOUNTED_PATH));
keycloak.start();
LOGGER.info(keycloak.getLogs());

KEYCLOAK_SERVER_URL = "https://localhost:" + keycloak.getMappedPort(8443);

RealmRepresentation realm = createRealm(KEYCLOAK_REALM);
postRealm(realm);

return Map.of("quarkus.oidc.auth-server-url", KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM);
}
client.createRealm(realm);

private static void postRealm(RealmRepresentation realm) {
try {
createRequestSpec().auth().oauth2(getAdminAccessToken())
.contentType("application/json")
.body(JsonSerialization.writeValueAsBytes(realm))
.when()
.post(KEYCLOAK_SERVER_URL + "/admin/realms").then()
.statusCode(201);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Map.of();
}

private static RealmRepresentation createRealm(String name) {
Expand Down Expand Up @@ -111,17 +62,6 @@ private static RealmRepresentation createRealm(String name) {
return realm;
}

private static String getAdminAccessToken() {
return createRequestSpec()
.param("grant_type", "password")
.param("username", "admin")
.param("password", "admin")
.param("client_id", "admin-cli")
.when()
.post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token")
.as(AccessTokenResponse.class).getToken();
}

private static ClientRepresentation createServiceClient(String clientId) {
ClientRepresentation client = new ClientRepresentation();

Expand Down Expand Up @@ -155,39 +95,13 @@ private static UserRepresentation createUser(String username, List<String> realm
return user;
}

public static String getAccessToken(String userName) {
return createRequestSpec().param("grant_type", "password")
.param("username", userName)
.param("password", userName)
.param("client_id", KEYCLOAK_SERVICE_CLIENT)
.param("client_secret", "secret")
.when()
.post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token")
.as(AccessTokenResponse.class).getToken();
}

public static String getRefreshToken(String userName) {
return createRequestSpec().param("grant_type", "password")
.param("username", userName)
.param("password", userName)
.param("client_id", KEYCLOAK_SERVICE_CLIENT)
.param("client_secret", "secret")
.when()
.post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token")
.as(AccessTokenResponse.class).getRefreshToken();
@Override
public void setIntegrationTestContext(DevServicesContext context) {
client.setIntegrationTestContext(context);
}

@Override
public void stop() {
createRequestSpec().auth().oauth2(getAdminAccessToken())
.when()
.delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204);

keycloak.stop();
}

private static RequestSpecification createRequestSpec() {
return RestAssured.given().trustStore(CLIENT_TRUSTSTORE, "password")
.keyStore(CLIENT_KEYSTORE, "password");
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.quarkus.it.keycloak;

import static io.quarkus.it.keycloak.KeycloakXTestResourceLifecycleManager.getAccessToken;

import java.net.URI;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
Expand All @@ -18,6 +16,8 @@
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.quarkus.test.keycloak.client.KeycloakTestClient.Tls;
import io.quarkus.websockets.BearerTokenClientEndpointConfigurator;

@QuarkusTest
Expand All @@ -27,6 +27,8 @@ public class WebsocketOidcTestCase {
@TestHTTPResource("secured-hello")
URI wsUri;

KeycloakTestClient client = new KeycloakTestClient(new Tls());

@Test
public void websocketTest() throws Exception {

Expand All @@ -42,7 +44,7 @@ public void onMessage(String s) {
});
session.getAsyncRemote().sendText("hello");
}
}, new BearerTokenClientEndpointConfigurator(getAccessToken("alice")), wsUri);
}, new BearerTokenClientEndpointConfigurator(client.getAccessToken("alice")), wsUri);

try {
Assertions.assertEquals("hello [email protected]", message.poll(20, TimeUnit.SECONDS));
Expand Down
Loading

0 comments on commit 1ff7c69

Please sign in to comment.