Skip to content

Commit

Permalink
Merge pull request quarkusio#33653 from michalvavrik/feature/new-oidc…
Browse files Browse the repository at this point in the history
…-dev-ui

Migrate OIDC to the new Dev UI
  • Loading branch information
sberyozkin authored May 31, 2023
2 parents 9aaa021 + 5b1acea commit ec1f4aa
Show file tree
Hide file tree
Showing 16 changed files with 2,202 additions and 263 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
package io.quarkus.oidc.deployment.devservices;

import java.time.Duration;
import java.util.Map;

import jakarta.inject.Singleton;

import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.ConfigurationBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler;
import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem;
import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem;
import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.Page;
import io.quarkus.oidc.runtime.OidcConfigPropertySupplier;
import io.quarkus.oidc.runtime.devui.OidcDevUiRecorder;
import io.quarkus.oidc.runtime.devui.OidcDevUiRpcSvcPropertiesBean;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;

public abstract class AbstractDevConsoleProcessor {
protected static final String CONFIG_PREFIX = "quarkus.oidc.";
Expand Down Expand Up @@ -81,4 +93,98 @@ protected void produceDevConsoleRouteItems(BuildProducer<DevConsoleRouteBuildIte
devConsoleRoute.produce(new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", exchangeCodeForTokens));
devConsoleRoute.produce(new DevConsoleRouteBuildItem("testService", "POST", passwordClientCredHandler));
}

protected static CardPageBuildItem createProviderWebComponent(OidcDevUiRecorder recorder,
Capabilities capabilities,
String oidcProviderName,
String oidcApplicationType,
String oidcGrantType,
String authorizationUrl,
String tokenUrl,
String logoutUrl,
boolean introspectionIsAvailable,
BuildProducer<SyntheticBeanBuildItem> beanProducer,
Duration webClientTimeout,
Map<String, Map<String, String>> grantOptions, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
ConfigurationBuildItem configurationBuildItem,
String keycloakAdminUrl,
Map<String, String> keycloakUsers,
Object keycloakRealms,
boolean alwaysLogoutUserInDevUiOnReload) {
final CardPageBuildItem cardPage = new CardPageBuildItem();

// prepare provider component
cardPage.addPage(Page
.webComponentPageBuilder()
.icon("font-awesome-solid:boxes-stacked")
.title(oidcProviderName == null ? "OpenId Connect Dev Console" : oidcProviderName + " provider")
.componentLink("qwc-oidc-provider.js"));

// prepare data for provider component
final boolean swaggerIsAvailable = capabilities.isPresent(Capability.SMALLRYE_OPENAPI);
final boolean graphqlIsAvailable = capabilities.isPresent(Capability.SMALLRYE_GRAPHQL);

final String swaggerUiPath;
if (swaggerIsAvailable) {
swaggerUiPath = nonApplicationRootPathBuildItem.resolvePath(
getProperty(configurationBuildItem, "quarkus.swagger-ui.path"));
} else {
swaggerUiPath = null;
}

final String graphqlUiPath;
if (graphqlIsAvailable) {
graphqlUiPath = nonApplicationRootPathBuildItem.resolvePath(
getProperty(configurationBuildItem, "quarkus.smallrye-graphql.ui.root-path"));
} else {
graphqlUiPath = null;
}

cardPage.addBuildTimeData("devRoot", nonApplicationRootPathBuildItem.getNonApplicationRootPath());

// pass down properties used by RPC service
beanProducer.produce(
SyntheticBeanBuildItem.configure(OidcDevUiRpcSvcPropertiesBean.class).unremovable()
.supplier(recorder.prepareRpcServiceProperties(authorizationUrl, tokenUrl, logoutUrl,
webClientTimeout, grantOptions, keycloakUsers, oidcProviderName, oidcApplicationType,
oidcGrantType, introspectionIsAvailable, keycloakAdminUrl, keycloakRealms,
swaggerIsAvailable, graphqlIsAvailable, swaggerUiPath, graphqlUiPath,
alwaysLogoutUserInDevUiOnReload))
.scope(Singleton.class)
.setRuntimeInit()
.done());

return cardPage;
}

private static String getProperty(ConfigurationBuildItem configurationBuildItem,
String propertyKey) {
// strictly speaking we know 'quarkus.swagger-ui.path' is build time property
// and 'quarkus.smallrye-graphql.ui.root-path' is build time with runtime fixed,
// but I wanted to make this bit more robust till we have DEV UI tests
// that will fail when this get changed in the future, then we can optimize this

String propertyValue = configurationBuildItem
.getReadResult()
.getAllBuildTimeValues()
.get(propertyKey);

if (propertyValue == null) {
propertyValue = configurationBuildItem
.getReadResult()
.getBuildTimeRunTimeValues()
.get(propertyKey);
} else {
return propertyValue;
}

if (propertyValue == null) {
propertyValue = configurationBuildItem
.getReadResult()
.getRunTimeDefaultValues()
.get(propertyKey);
}

return propertyValue;
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
package io.quarkus.oidc.deployment.devservices;

import static io.quarkus.oidc.runtime.devui.OidcDevServicesUtils.getTokens;

import java.time.Duration;
import java.util.Map;

import org.jboss.logging.Logger;

import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
import io.vertx.mutiny.ext.web.client.HttpResponse;
import io.vertx.mutiny.ext.web.client.WebClient;

public class OidcAuthorizationCodePostHandler extends DevConsolePostHandler {
private static final Logger LOG = Logger.getLogger(OidcAuthorizationCodePostHandler.class);
private static final String APPLICATION_JSON = "application/json";

private static final Logger LOG = Logger.getLogger(OidcAuthorizationCodePostHandler.class);
Vertx vertxInstance;
Duration timeout;
Map<String, String> grantOptions;
Expand All @@ -33,39 +28,20 @@ public OidcAuthorizationCodePostHandler(Vertx vertxInstance, Duration timeout,

@Override
protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception {
WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance);
String tokenUrl = form.get("tokenUrl");

try {
LOG.infof("Using authorization_code grant to get a token from '%s' with client id '%s'",
tokenUrl, form.get("client"));

HttpRequest<Buffer> request = client.postAbs(tokenUrl);
request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString());
request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON);

io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap());
props.add("client_id", form.get("client"));
if (form.get("clientSecret") != null && !form.get("clientSecret").isBlank()) {
props.add("client_secret", form.get("clientSecret"));
}
props.add("grant_type", "authorization_code");
props.add("code", form.get("authorizationCode"));
props.add("redirect_uri", form.get("redirectUri"));
if (grantOptions != null) {
props.addAll(grantOptions);
}

String tokens = request.sendBuffer(OidcCommonUtils.encodeForm(props)).onItem()
.transform(resp -> getBodyAsString(resp))
final String tokens = getTokens(
form.get("tokenUrl"),
form.get("client"),
form.get("clientSecret"),
form.get("authorizationCode"),
form.get("redirectUri"),
vertxInstance,
grantOptions)
.onFailure().recoverWithNull()
.await().atMost(timeout);

event.put("tokens", tokens);

} catch (Throwable t) {
LOG.errorf("Token can not be acquired from OpenId Connect provider: %s", t.toString());
} finally {
client.close();
}
}

Expand All @@ -78,12 +54,4 @@ protected void actionSuccess(RoutingContext event) {
}
}

private static String getBodyAsString(HttpResponse<Buffer> resp) {
if (resp.statusCode() == 200) {
return resp.bodyAsString();
} else {
String errorMessage = resp.bodyAsString();
throw new RuntimeException(errorMessage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,34 @@
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ConfigurationBuildItem;
import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem;
import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem;
import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem;
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Provider;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.deployment.OidcBuildTimeConfig;
import io.quarkus.oidc.runtime.devui.OidcDevJsonRpcService;
import io.quarkus.oidc.runtime.devui.OidcDevServicesUtils;
import io.quarkus.oidc.runtime.devui.OidcDevUiRecorder;
import io.quarkus.oidc.runtime.providers.KnownOidcProviders;
import io.quarkus.runtime.configuration.ConfigUtils;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.JsonObject;
Expand All @@ -50,13 +60,19 @@ public class OidcDevConsoleProcessor extends AbstractDevConsoleProcessor {

OidcBuildTimeConfig oidcConfig;

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep(onlyIf = IsDevelopment.class)
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
void prepareOidcDevConsole(BuildProducer<DevConsoleTemplateInfoBuildItem> devConsoleInfo,
BuildProducer<DevConsoleRuntimeTemplateInfoBuildItem> devConsoleRuntimeInfo,
CuratedApplicationShutdownBuildItem closeBuildItem,
BuildProducer<DevConsoleRouteBuildItem> devConsoleRoute,
Capabilities capabilities, CurateOutcomeBuildItem curateOutcomeBuildItem) {
Capabilities capabilities, CurateOutcomeBuildItem curateOutcomeBuildItem,
BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItemBuildProducer,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
BuildProducer<CardPageBuildItem> cardPageProducer,
ConfigurationBuildItem configurationBuildItem,
OidcDevUiRecorder recorder) {
if (!isOidcTenantEnabled() || !isClientIdSet()) {
return;
}
Expand Down Expand Up @@ -95,17 +111,20 @@ public void run() {
devConsoleInfo.produce(new DevConsoleTemplateInfoBuildItem("keycloakAdminUrl",
authServerUrl.substring(0, authServerUrl.indexOf("/realms/"))));
}
boolean metadataNotNull = metadata != null;

// old DEV UI
produceDevConsoleTemplateItems(capabilities,
devConsoleInfo,
devConsoleRuntimeInfo,
curateOutcomeBuildItem,
providerName,
getApplicationType(providerConfig),
oidcConfig.devui.grant.type.isPresent() ? oidcConfig.devui.grant.type.get().getGrantType() : "code",
metadata != null ? metadata.getString("authorization_endpoint") : null,
metadata != null ? metadata.getString("token_endpoint") : null,
metadata != null ? metadata.getString("end_session_endpoint") : null,
metadata != null
metadataNotNull ? metadata.getString("authorization_endpoint") : null,
metadataNotNull ? metadata.getString("token_endpoint") : null,
metadataNotNull ? metadata.getString("end_session_endpoint") : null,
metadataNotNull
? (metadata.containsKey("introspection_endpoint") || metadata.containsKey("userinfo_endpoint"))
: checkProviderUserInfoRequired(providerConfig));

Expand All @@ -115,9 +134,43 @@ public void run() {
oidcConfig.devui.grantOptions),
new OidcPasswordClientCredHandler(vertxInstance, oidcConfig.devui.webClientTimeout,
oidcConfig.devui.grantOptions));

// new DEV UI
final String keycloakAdminUrl;
if (KEYCLOAK.equals(providerName)) {
keycloakAdminUrl = authServerUrl.substring(0, authServerUrl.indexOf("/realms/"));
} else {
keycloakAdminUrl = null;
}
var cardPage = createProviderWebComponent(recorder,
capabilities,
providerName,
getApplicationType(providerConfig),
oidcConfig.devui.grant.type.isPresent() ? oidcConfig.devui.grant.type.get().getGrantType() : "code",
metadataNotNull ? metadata.getString("authorization_endpoint") : null,
metadataNotNull ? metadata.getString("token_endpoint") : null,
metadataNotNull ? metadata.getString("end_session_endpoint") : null,
metadataNotNull
? (metadata.containsKey("introspection_endpoint") || metadata.containsKey("userinfo_endpoint"))
: checkProviderUserInfoRequired(providerConfig),
syntheticBeanBuildItemBuildProducer,
oidcConfig.devui.webClientTimeout,
oidcConfig.devui.grantOptions,
nonApplicationRootPathBuildItem,
configurationBuildItem,
keycloakAdminUrl,
null,
null,
true);
cardPageProducer.produce(cardPage);
}
}

@BuildStep(onlyIf = IsDevelopment.class)
JsonRPCProvidersBuildItem produceOidcDevJsonRpcService() {
return new JsonRPCProvidersBuildItem(OidcDevJsonRpcService.class);
}

private boolean checkProviderUserInfoRequired(OidcTenantConfig providerConfig) {
if (providerConfig != null) {
return providerConfig.authentication.userInfoRequired.orElse(false);
Expand Down
Loading

0 comments on commit ec1f4aa

Please sign in to comment.