Skip to content

Commit

Permalink
Start Keycloak Dev Svc for standalone OIDC Client
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Sep 30, 2024
1 parent 4cab5df commit beb0279
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 121 deletions.
2 changes: 1 addition & 1 deletion extensions/oidc-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@
<http>
<url>http://localhost:8180</url>
</http>
<time>100000</time>
<time>10000000</time>
</wait>
</run>
</image>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
Expand All @@ -35,6 +39,9 @@
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.Page;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
Expand All @@ -51,6 +58,8 @@
import io.quarkus.oidc.client.runtime.TokenProviderProducer;
import io.quarkus.oidc.client.runtime.TokensHelper;
import io.quarkus.oidc.client.runtime.TokensProducer;
import io.quarkus.oidc.common.deployment.devservices.keycloak.KeycloakDevServicesConfigBuildItem;
import io.quarkus.oidc.common.deployment.devservices.keycloak.KeycloakDevServicesRequiredBuildItem;
import io.quarkus.oidc.token.propagation.AccessToken;
import io.quarkus.tls.TlsRegistryBuildItem;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
Expand Down Expand Up @@ -184,6 +193,27 @@ private AccessTokenInstanceBuildItem build() {
return index.getIndex().getAnnotations(ACCESS_TOKEN).stream().map(ItemBuilder::new).map(ItemBuilder::build).toList();
}

@BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class)
KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() {
// this needs to be done as OIDC Common doesn't know if the OIDC Client is enabled
return KeycloakDevServicesRequiredBuildItem.requireDevServiceForOidcClient();
}

@BuildStep(onlyIf = IsDevelopment.class)
void produceProviderComponent(Optional<KeycloakDevServicesConfigBuildItem> configProps,
BuildProducer<CardPageBuildItem> cardPageProducer, Capabilities capabilities) {
final String keycloakAdminUrl = configProps.map(item -> item.getConfig().get("keycloak.url")).orElse(null);
if (capabilities.isMissing(Capability.OIDC) && keycloakAdminUrl != null) {
// Add Admin page
final CardPageBuildItem cardPage = new CardPageBuildItem();
cardPage.addPage(Page.externalPageBuilder("Keycloak Admin")
.icon("font-awesome-solid:key")
.doNotEmbed(true)
.url(keycloakAdminUrl));
cardPageProducer.produce(cardPage);
}
}

/**
* Creates a Tokens producer class like follows:
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkus.oidc.client;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.oidc.runtime.OidcUtils;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

/**
* Test Keycloak Dev Service is started when OIDC extension is disabled (or not present, though indirectly).
* OIDC client auth server URL and client id and secret must be automatically configured for this test to pass.
*/
public class OidcClientKeycloakDevServiceTest {

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(NamedOidcClientResource.class)
.addAsResource("oidc-client-dev-service-test.properties", "application.properties"));

@Test
public void testInjectedNamedOidcClients() {
String token1 = doTestGetTokenByNamedClient("client1");
String token2 = doTestGetTokenByNamedClient("client2");
validateTokens(token1, token2);
}

@Test
public void testInjectedNamedTokens() {
String token1 = doTestGetTokenByNamedTokensProvider("client1");
String token2 = doTestGetTokenByNamedTokensProvider("client2");
validateTokens(token1, token2);
}

private void validateTokens(String token1, String token2) {
assertThat(token1, is(not(equalTo(token2))));
assertThat(upn(token1), is("alice"));
assertThat(upn(token2), is("bob"));
}

private String upn(String token) {
return OidcUtils.decodeJwtContent(token).getString("upn");
}

private String doTestGetTokenByNamedClient(String clientId) {
String token = RestAssured.given().get("/" + clientId + "/token").body().asString();
assertThat(token, is(notNullValue()));
return token;
}

private String doTestGetTokenByNamedTokensProvider(String clientId) {
String token = RestAssured.given().get("/" + clientId + "/token/singleton").body().asString();
assertThat(token, is(notNullValue()));
return token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
quarkus.oidc.enabled=false

quarkus.oidc-client.client1.auth-server-url=${quarkus.oidc-client.auth-server-url}
quarkus.oidc-client.client1.client-id=${quarkus.oidc-client.client-id}
quarkus.oidc-client.client1.credentials.secret=${quarkus.oidc-client.credentials.secret}
quarkus.oidc-client.client1.grant.type=password
quarkus.oidc-client.client1.grant-options.password.username=alice
quarkus.oidc-client.client1.grant-options.password.password=alice

quarkus.oidc-client.client2.auth-server-url=${quarkus.oidc-client.auth-server-url}
quarkus.oidc-client.client2.client-id=${quarkus.oidc-client.client-id}
quarkus.oidc-client.client2.credentials.secret=${quarkus.oidc-client.credentials.secret}
quarkus.oidc-client.client2.grant.type=password
quarkus.oidc-client.client2.grant-options.password.username=bob
quarkus.oidc-client.client2.grant-options.password.password=bob
25 changes: 25 additions & 0 deletions extensions/oidc-common/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-common</artifactId>
</dependency>
<!-- Dev Service dependencies -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-deployment</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.quarkus.oidc.deployment.devservices.keycloak;
package io.quarkus.oidc.common.deployment.devservices.keycloak;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;

import io.quarkus.oidc.deployment.DevUiConfig;
import io.quarkus.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
Expand Down Expand Up @@ -151,9 +151,12 @@ public class DevServicesConfig {
public boolean createRealm;

/**
* Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them as
* `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` properties, if the {@link #createRealm} property is set to
* true.
* Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them
* if the {@link #createRealm} property is set to true.
* For OIDC extension configuration properties `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` will
* be configured.
* For OIDC Client extension configuration properties `quarkus.oidc-client.client.id`
* and `quarkus.oidc-client.credentials.secret` will be configured.
*
* Set to `false` if clients have to be created using either the Keycloak Administration Console or
* the Keycloak Admin API provided by {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager}
Expand Down Expand Up @@ -191,54 +194,6 @@ public class DevServicesConfig {
@ConfigDocMapKey("role-name")
public Map<String, List<String>> roles;

/**
* Specifies the grant type.
*
* @deprecated This field is deprecated. Use {@link DevUiConfig#grant} instead.
*/
@Deprecated
public Grant grant = new Grant();

@ConfigGroup
public static class Grant {
public static enum Type {
/**
* `client_credentials` grant
*/
CLIENT("client_credentials"),
/**
* `password` grant
*/
PASSWORD("password"),

/**
* `authorization_code` grant
*/
CODE("code"),

/**
* `implicit` grant
*/
IMPLICIT("implicit");

private String grantType;

private Type(String grantType) {
this.grantType = grantType;
}

public String getGrantType() {
return grantType;
}
}

/**
* Defines the grant type for aquiring tokens for testing OIDC `service` applications.
*/
@ConfigItem(defaultValue = "code")
public Type type = Type.CODE;
}

/**
* The specific port for the dev service to listen on.
* <p>
Expand All @@ -254,6 +209,14 @@ public String getGrantType() {
@ConfigDocMapKey("environment-variable-name")
public Map<String, String> containerEnv;

/**
* The WebClient timeout.
* Use this property to configure how long an HTTP client used by OIDC dev service admin client will wait
* for a response from OpenId Connect Provider when acquiring admin token and creating realm.
*/
@ConfigItem(defaultValue = "4S")
public Duration webClientTimeout;

@Override
public boolean equals(Object o) {
if (this == o)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.oidc.deployment.devservices.keycloak;
package io.quarkus.oidc.common.deployment.devservices.keycloak;

import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigItem;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.oidc.deployment.devservices.keycloak;
package io.quarkus.oidc.common.deployment.devservices.keycloak;

import java.util.Map;

Expand All @@ -25,7 +25,7 @@ public Map<String, String> getConfig() {
return config;
}

boolean isContainerRestarted() {
public boolean isContainerRestarted() {
return containerRestarted;
}
}
Loading

0 comments on commit beb0279

Please sign in to comment.