diff --git a/docs/src/main/asciidoc/images/dev-ui-oidc-devconsole-card.png b/docs/src/main/asciidoc/images/dev-ui-oidc-devconsole-card.png new file mode 100644 index 0000000000000..564bb911f440b Binary files /dev/null and b/docs/src/main/asciidoc/images/dev-ui-oidc-devconsole-card.png differ diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index 1fdd448c0eaa6..be120a9bd8ead 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -3,18 +3,21 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// -= Dev Services for OpenId Connect (OIDC) += Dev Services and UI for OpenId Connect (OIDC) include::./attributes.adoc[] -This guide covers the Dev Services for OpenId Connect (OIDC) Keycloak provider and explains how to support other OpenId Connect providers. +This guide covers the Dev Services and UI for OpenId Connect (OIDC) Keycloak provider and explains how to support Dev Services and UI for other OpenId Connect providers. +It also describes Dev UI for all OpenId Connect providers which have already been started before Quarkus is launched in a dev mode. == Introduction Quarkus introduces an experimental `Dev Services For Keycloak` feature which is enabled by default when the `quarkus-oidc` extension is started in dev mode with `mvn quarkus:dev` and when the integration tests are running in test mode, but only when no `quarkus.oidc.auth-server-url` property is configured. It starts a Keycloak container for both the dev and/or test modes and initializes them by registering the existing Keycloak realm or creating a new realm with the client and users for you to start developing your Quarkus application secured by Keycloak immediately. It will restart the container when the `application.properties` or the realm file changes have been detected. -Additionally, link:dev-ui[Dev UI] available at http://localhost:8080/q/dev[/q/dev] supports this feature with a Keycloak specific page which helps to acquire the tokens from Keycloak and test your Quarkus application. +Additionally, link:dev-ui[Dev UI] available at http://localhost:8080/q/dev[/q/dev] complements this feature with a Dev UI page which helps to acquire the tokens from Keycloak and test your Quarkus application. + +If `quarkus.oidc.auth-server-url` is already set then a generic OpenId Connect Dev Console which can be used with all OpenId Connect providers will be activated, please see <> for more information. == Dev Services for Keycloak @@ -58,6 +61,7 @@ Click on the `Provider: Keycloak` link and you will see a Keycloak page which wi By default the Keycloak page can be used to support the development of a link:security-openid-connect[Quarkus OIDC service application]. +[[keycloak-authorization-code-grant]] ==== Authorization Code Grant If you set `quarkus.keycloak.devservices.grant.type=code` in `applicatin.properties` (this is a default value) then an `authorization_code` grant will be used to acquire both access and ID tokens. Using this grant is recommended to emulate a typical flow where a `Single Page Application` acquires the tokens and uses them to access Quarkus services. @@ -172,13 +176,44 @@ Note that even if you initialize Keycloak from a realm file, it is still needed If you prefer not to have a `Dev Services for Keycloak` container started or do not work with Keycloak then you can also disable this feature with `quarkus.keycloak.devservices.enabled=false` - it will only be necessary if you expect to start `quarkus:dev` without `quarkus.oidc.auth-server-url`. -The main Dev UI page will include an empty `OpenId Connect Card` when `Dev Services for Keycloak` is disabled: +The main Dev UI page will include an empty `OpenId Connect Card` when `Dev Services for Keycloak` is disabled and the `quarkus.oidc.auth-server-url` property +has not been initialized: image::dev-ui-oidc-card.png[alt=Dev UI OpenId Connect Card,role="center"] -== Dev Services Support for other OpenId Connect Providers +If `quarkus.oidc.auth-server-url` is already set then a generic OpenId Connect Dev Console which can be used with all OpenId Connect providers may be activated, please see <> for more information. + +[[dev-ui-all-oidc-providers]] +== Dev UI for all OpenId Connect Providers + +If `quarkus.oidc.auth-server-url` points to an already started OpenId Connect provider (which can be Keycloak or other provider), `quarkus.oidc.auth-server-url` is set to `service` (which is a default value) and at least `quarkus.oidc.client-id` is set then `Dev UI for all OpenId Connect Providers` will be activated. + +Setting `quarkus.oidc.credentials.secret` will mostly likely be required for Keycloak and other providers for the authorization code flow initiated from Dev UI to complete, unless the client identified with `quarkus.oidc.client-id` is configured as a public client in your OpenId Connect provider's administration console. + +Run `mvn`quarkus:dev` and you will see the following message: +[source,shell] +---- +$ mvn quarkus:dev +... +2021-09-07 15:53:42,697 INFO [io.qua.oid.dep.dev.OidcDevConsoleProcessor] (build-41) OIDC Dev Console: discovering the provider metadata at http://localhost:8180/auth/realms/quarkus/.well-known/openid-configuration +... +---- + +If the provider metadata discovery has been successful then, after you open the main link:http://localhost:8080/q/dev[Dev UI page], you will see the `OpenId Connect Card` page linking to `Dev Console`: + +image::dev-ui-oidc-devconsole-card.png[alt=Generic Dev UI OpenId Connect Card,role="center"] + +Follow the link and you'll be able log in to your provider, get the tokens and test the application. The experience will be the same as described in the <> section, where `Dev Services for Keycloak` container has been started, especially if you work with Keycloak (please also pay attention to a `redirect_uri` note in that section). + +If you work with other providers then a Dev UI experience described in the <> section might differ slightly. For example, an access token may not be in a JWT format so it won't be possibe to show its internal content, though all providers should return an ID Token as JWT. + +Some providers such as `Auth0` do not support a standard RP initiated logout so a logout option will also be hidden. + +At the moment `Dev UI for all OpenId Connect Providers` only supports an authorization code grant. More grants may be supported in the future, similarly to how it is done with `Dev Services for Keycloak`. + +== Dev Services and UI Support for other OpenId Connect Providers -Your custom extension would need to extend `quarkus-oidc` only and add the dependencies required to support your provider to the extension's `deployment` module only. +Your custom extension would need to extend `quarkus-oidc` and add the dependencies required to support your provider to the extension's `deployment` module only. The build step dealing with the `Dev Services` should additionally register two runtime properties into the "io.quarkus.quarkus-oidc" namespace: `oidcProviderName` (for example, `Google`) and `oidcProviderUrlBase` (for example: `mycompany.devservices-google`) for the `OpenId Connect Card` to link to the Dev UI page representing your provider, for example: diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakAuthorizationCodePostHandler.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcAuthorizationCodePostHandler.java similarity index 67% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakAuthorizationCodePostHandler.java rename to extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcAuthorizationCodePostHandler.java index df550d73225cd..2b9d9b7becf12 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakAuthorizationCodePostHandler.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcAuthorizationCodePostHandler.java @@ -1,10 +1,13 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.oidc.deployment.devservices; + +import java.time.Duration; 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; @@ -12,24 +15,32 @@ import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; -public class KeycloakAuthorizationCodePostHandler extends DevConsolePostHandler { - private static final Logger LOG = Logger.getLogger(KeycloakAuthorizationCodePostHandler.class); +public class OidcAuthorizationCodePostHandler extends DevConsolePostHandler { + private static final Logger LOG = Logger.getLogger(OidcAuthorizationCodePostHandler.class); + + Vertx vertxInstance; + Duration timeout; + + public OidcAuthorizationCodePostHandler(Vertx vertxInstance, Duration timeout) { + this.vertxInstance = vertxInstance; + this.timeout = timeout; + } @Override protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception { - WebClient client = KeycloakDevServicesUtils.createWebClient(); - String keycloakUrl = form.get("keycloakUrl") + "/realms/" + form.get("realm") + "/protocol/openid-connect/token"; + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); + String tokenUrl = form.get("tokenUrl"); try { - LOG.infof("Using authorization_code grant to get a token from '%s' in realm '%s' with client id '%s'", - keycloakUrl, form.get("realm"), form.get("client")); + LOG.infof("Using authorization_code grant to get a token from '%s' with client id '%s'", + tokenUrl, form.get("client")); - HttpRequest request = client.postAbs(keycloakUrl); + HttpRequest request = client.postAbs(tokenUrl); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); 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) { + if (form.get("clientSecret") != null && !form.get("clientSecret").isBlank()) { props.add("client_secret", form.get("clientSecret")); } props.add("grant_type", "authorization_code"); @@ -38,12 +49,12 @@ protected void handlePostAsync(RoutingContext event, MultiMap form) throws Excep String tokens = request.sendBuffer(OidcCommonUtils.encodeForm(props)).onItem() .transform(resp -> getBodyAsString(resp)) - .await().atMost(KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout); + .await().atMost(timeout); event.put("tokens", tokens); } catch (Throwable t) { - LOG.errorf("Token can not be acquired from Keycloak: %s", t.toString()); + LOG.errorf("Token can not be acquired from OpenId Connect provider: %s", t.toString()); } finally { client.close(); } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java new file mode 100644 index 0000000000000..c1975727214fe --- /dev/null +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java @@ -0,0 +1,146 @@ +package io.quarkus.oidc.deployment.devservices; + +import java.time.Duration; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +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.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.client.WebClient; + +public class OidcDevConsoleProcessor { + static volatile Vertx vertxInstance; + private static final Logger LOG = Logger.getLogger(OidcDevConsoleProcessor.class); + + private static final String CONFIG_PREFIX = "quarkus.oidc."; + private static final String TENANT_ENABLED_CONFIG_KEY = CONFIG_PREFIX + "tenant-enabled"; + private static final String AUTH_SERVER_URL_CONFIG_KEY = CONFIG_PREFIX + "auth-server-url"; + private static final String APP_TYPE_CONFIG_KEY = CONFIG_PREFIX + "application-type"; + private static final String SERVICE_APP_TYPE = "service"; + private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; + private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; + + @BuildStep(onlyIf = IsDevelopment.class) + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + void prepareOidcDevConsole(BuildProducer console, + BuildProducer devConsoleRoute) { + if (isOidcTenantEnabled() && isAuthServerUrlSet() && isClientIdSet() && isServiceAuthType()) { + + if (vertxInstance == null) { + vertxInstance = Vertx.vertx(); + + Runnable closeTask = new Runnable() { + @Override + public void run() { + if (vertxInstance != null) { + try { + vertxInstance.close(); + } catch (Throwable t) { + LOG.error("Failed to close Vertx instance", t); + } + } + vertxInstance = null; + } + }; + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); + ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); + Thread closeHookThread = new Thread(closeTask, "OIDC DevConsole Vertx close thread"); + Runtime.getRuntime().addShutdownHook(closeHookThread); + ((QuarkusClassLoader) cl.parent()).addCloseTask(new Runnable() { + @Override + public void run() { + Runtime.getRuntime().removeShutdownHook(closeHookThread); + } + }); + } + + String authServerUrl = getConfigProperty(AUTH_SERVER_URL_CONFIG_KEY); + JsonObject metadata = discoverMetadata(authServerUrl); + if (metadata == null) { + return; + } + if (authServerUrl.contains("/realms/")) { + console.produce(new DevConsoleTemplateInfoBuildItem("keycloakAdminUrl", + authServerUrl.substring(0, authServerUrl.indexOf("/realms/")))); + } + console.produce(new DevConsoleTemplateInfoBuildItem("oidcApplicationType", SERVICE_APP_TYPE)); + console.produce(new DevConsoleTemplateInfoBuildItem("clientId", getConfigProperty(CLIENT_ID_CONFIG_KEY))); + console.produce(new DevConsoleTemplateInfoBuildItem("clientSecret", getClientSecret())); + + console.produce(new DevConsoleTemplateInfoBuildItem("tokenUrl", metadata.getString("token_endpoint"))); + console.produce( + new DevConsoleTemplateInfoBuildItem("authorizationUrl", metadata.getString("authorization_endpoint"))); + if (metadata.containsKey("end_session_endpoint")) { + console.produce(new DevConsoleTemplateInfoBuildItem("logoutUrl", metadata.getString("end_session_endpoint"))); + } + console.produce(new DevConsoleTemplateInfoBuildItem("oidcGrantType", "code")); + + devConsoleRoute.produce(new DevConsoleRouteBuildItem("testServiceWithToken", "POST", + new OidcTestServiceHandler(vertxInstance, Duration.ofSeconds(3)))); + devConsoleRoute.produce(new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", + new OidcAuthorizationCodePostHandler(vertxInstance, Duration.ofSeconds(3)))); + } + } + + private JsonObject discoverMetadata(String authServerUrl) { + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); + try { + String metadataUrl = authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; + LOG.infof("OIDC Dev Console: discovering the provider metadata at %s", metadataUrl); + + HttpResponse resp = client.getAbs(metadataUrl) + .putHeader(HttpHeaders.ACCEPT.toString(), "application/json").send().await().indefinitely(); + if (resp.statusCode() == 200) { + return resp.bodyAsJsonObject(); + } else { + LOG.errorf("OIDC metadata discovery failed: %s", resp.bodyAsString()); + return null; + } + } catch (Throwable t) { + LOG.errorf("OIDC metadata discovery failed: %s", t.toString()); + return null; + } finally { + client.close(); + } + } + + private String getConfigProperty(String name) { + return ConfigProvider.getConfig().getValue(name, String.class); + } + + private static boolean isOidcTenantEnabled() { + return ConfigProvider.getConfig().getOptionalValue(TENANT_ENABLED_CONFIG_KEY, Boolean.class).orElse(true); + } + + private static boolean isClientIdSet() { + return ConfigUtils.isPropertyPresent(CLIENT_ID_CONFIG_KEY); + } + + private static String getClientSecret() { + return ConfigProvider.getConfig().getOptionalValue(CLIENT_SECRET_CONFIG_KEY, String.class).orElse(""); + } + + private static boolean isAuthServerUrlSet() { + return ConfigUtils.isPropertyPresent(AUTH_SERVER_URL_CONFIG_KEY); + } + + private boolean isServiceAuthType() { + return SERVICE_APP_TYPE.equals( + ConfigProvider.getConfig().getOptionalValue(APP_TYPE_CONFIG_KEY, String.class).orElse(SERVICE_APP_TYPE)); + } + +} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java index 78a7c59e677f3..6fafbb817a38c 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java @@ -2,6 +2,10 @@ import io.quarkus.builder.item.SimpleBuildItem; +/** + * Marker build item which indicates that Dev Services for OIDC are provided by another extension. + * Dev Services for Keycloak will be disabled if this item is detected. + */ public class OidcDevServicesBuildItem extends SimpleBuildItem { } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesUtils.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesUtils.java similarity index 83% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesUtils.java rename to extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesUtils.java index 5fb008965ea2b..5666973d58bf3 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesUtils.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesUtils.java @@ -1,9 +1,10 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.oidc.deployment.devservices; import java.time.Duration; 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.core.json.JsonObject; import io.vertx.mutiny.core.buffer.Buffer; @@ -11,23 +12,23 @@ import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; -public final class KeycloakDevServicesUtils { - private KeycloakDevServicesUtils() { +public final class OidcDevServicesUtils { + private OidcDevServicesUtils() { } - public static WebClient createWebClient() { - return WebClient.create(new io.vertx.mutiny.core.Vertx(KeycloakDevServicesProcessor.vertxInstance)); + public static WebClient createWebClient(Vertx vertx) { + return WebClient.create(new io.vertx.mutiny.core.Vertx(vertx)); } public static String getPasswordAccessToken(WebClient client, - String keycloakUrl, + String tokenUrl, String clientId, String clientSecret, String userName, String userPassword, Duration timeout) throws Exception { - HttpRequest request = client.postAbs(keycloakUrl); + HttpRequest request = client.postAbs(tokenUrl); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap()); @@ -45,11 +46,11 @@ public static String getPasswordAccessToken(WebClient client, } public static String getClientCredAccessToken(WebClient client, - String keycloakUrl, + String tokenUrl, String clientId, String clientSecret, Duration timeout) throws Exception { - HttpRequest request = client.postAbs(keycloakUrl); + HttpRequest request = client.postAbs(tokenUrl); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap()); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakImplicitGrantPostHandler.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcTestServiceHandler.java similarity index 67% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakImplicitGrantPostHandler.java rename to extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcTestServiceHandler.java index 6e57f35b85b4d..6f578d6b9c90e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakImplicitGrantPostHandler.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcTestServiceHandler.java @@ -1,28 +1,33 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.oidc.deployment.devservices; + +import java.time.Duration; import org.jboss.logging.Logger; import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; 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.ext.web.client.WebClient; -public class KeycloakImplicitGrantPostHandler extends DevConsolePostHandler { - private static final Logger LOG = Logger.getLogger(KeycloakImplicitGrantPostHandler.class); +public class OidcTestServiceHandler extends DevConsolePostHandler { + private static final Logger LOG = Logger.getLogger(OidcTestServiceHandler.class); + + Vertx vertxInstance; + Duration timeout; + + public OidcTestServiceHandler(Vertx vertxInstance, Duration timeout) { + this.vertxInstance = vertxInstance; + this.timeout = timeout; + } @Override protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception { - WebClient client = KeycloakDevServicesUtils.createWebClient(); + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); try { - String token = form.get("token"); - - LOG.infof("Test token: %s", token); - LOG.infof("Sending token to '%s'", form.get("serviceUrl")); - testServiceInternal(event, client, form.get("serviceUrl"), token); - } catch (Throwable t) { - LOG.errorf("Token can not be acquired from Keycloak: %s", t.toString()); + testServiceInternal(event, client, form.get("serviceUrl"), form.get("token")); } finally { client.close(); } @@ -30,9 +35,11 @@ protected void handlePostAsync(RoutingContext event, MultiMap form) throws Excep private void testServiceInternal(RoutingContext event, WebClient client, String serviceUrl, String token) { try { + LOG.infof("Test token: %s", token); + LOG.infof("Sending token to '%s'", serviceUrl); int statusCode = client.getAbs(serviceUrl) .putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer " + token).send().await() - .atMost(KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout) + .atMost(timeout) .statusCode(); LOG.infof("Result: %d", statusCode); event.put("result", String.valueOf(statusCode)); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java index 8faa62619bf32..3ec790d7fecb9 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java @@ -5,6 +5,7 @@ import org.jboss.logging.Logger; import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; +import io.quarkus.oidc.deployment.devservices.OidcDevServicesUtils; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; @@ -20,33 +21,31 @@ public KeycloakDevConsolePostHandler(Map users) { @Override protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception { - WebClient client = KeycloakDevServicesUtils.createWebClient(); - String keycloakUrl = form.get("keycloakUrl") + "/realms/" + form.get("realm") + "/protocol/openid-connect/token"; + WebClient client = OidcDevServicesUtils.createWebClient(KeycloakDevServicesProcessor.vertxInstance); + String tokenUrl = form.get("tokenUrl"); try { String token = null; if ("password".equals(form.get("grant"))) { - LOG.infof("Using a password grant to get a token from '%s' for user '%s' in realm '%s' with client id '%s'", - keycloakUrl, form.get("user"), form.get("realm"), form.get("client")); + LOG.infof("Using a password grant to get a token from '%s' for user '%s' with client id '%s'", + tokenUrl, form.get("user"), form.get("client")); String userName = form.get("user"); - token = KeycloakDevServicesUtils.getPasswordAccessToken(client, keycloakUrl, + token = OidcDevServicesUtils.getPasswordAccessToken(client, tokenUrl, form.get("client"), form.get("clientSecret"), userName, users.get(userName), KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout); } else { - LOG.infof("Using a client_credentials grant to get a token token from '%s' in realm '%s' with client id '%s'", - keycloakUrl, form.get("realm"), form.get("client")); + LOG.infof("Using a client_credentials grant to get a token token from '%s' with client id '%s'", + tokenUrl, form.get("client")); - token = KeycloakDevServicesUtils.getClientCredAccessToken(client, keycloakUrl, + token = OidcDevServicesUtils.getClientCredAccessToken(client, tokenUrl, form.get("client"), form.get("clientSecret"), KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout); } - LOG.infof("Test token: %s", token); - LOG.infof("Sending token to '%s'", form.get("serviceUrl")); testServiceInternal(event, client, form.get("serviceUrl"), token); } catch (Throwable t) { LOG.errorf("Token can not be acquired from Keycloak: %s", t.toString()); @@ -57,6 +56,8 @@ protected void handlePostAsync(RoutingContext event, MultiMap form) throws Excep private void testServiceInternal(RoutingContext event, WebClient client, String serviceUrl, String token) { try { + LOG.infof("Test token: %s", token); + LOG.infof("Sending token to '%s'", serviceUrl); int statusCode = client.getAbs(serviceUrl) .putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer " + token).send().await().indefinitely() .statusCode(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java index dab59e46358a2..6404c3ba67be7 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java @@ -10,6 +10,8 @@ import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.oidc.deployment.devservices.OidcAuthorizationCodePostHandler; +import io.quarkus.oidc.deployment.devservices.OidcTestServiceHandler; public class KeycloakDevConsoleProcessor { @@ -19,22 +21,25 @@ public class KeycloakDevConsoleProcessor { @Consume(RuntimeConfigSetupCompleteBuildItem.class) public void setConfigProperties(BuildProducer console, Optional configProps) { - if (configProps.isPresent()) { - console.produce( - new DevConsoleTemplateInfoBuildItem("devServicesEnabled", config.devservices.enabled)); - console.produce( - new DevConsoleTemplateInfoBuildItem("keycloakUrl", configProps.get().getProperties().get("keycloak.url"))); + if (configProps.isPresent() && configProps.get().getProperties().containsKey("keycloak.url")) { + String keycloakUrl = (String) configProps.get().getProperties().get("keycloak.url"); + String realmUrl = keycloakUrl + "/realms/" + configProps.get().getProperties().get("keycloak.realm"); + + console.produce(new DevConsoleTemplateInfoBuildItem("keycloakUrl", keycloakUrl)); + console.produce(new DevConsoleTemplateInfoBuildItem("keycloakAdminUrl", keycloakUrl)); console.produce(new DevConsoleTemplateInfoBuildItem("oidcApplicationType", configProps.get().getProperties().get("quarkus.oidc.application-type"))); - console.produce(new DevConsoleTemplateInfoBuildItem("keycloakClient", + console.produce(new DevConsoleTemplateInfoBuildItem("clientId", configProps.get().getProperties().get("quarkus.oidc.client-id"))); - console.produce(new DevConsoleTemplateInfoBuildItem("keycloakClientSecret", + console.produce(new DevConsoleTemplateInfoBuildItem("clientSecret", configProps.get().getProperties().get("quarkus.oidc.credentials.secret"))); console.produce( new DevConsoleTemplateInfoBuildItem("keycloakUsers", configProps.get().getProperties().get("oidc.users"))); - console.produce(new DevConsoleTemplateInfoBuildItem("keycloakRealm", - configProps.get().getProperties().get("keycloak.realm"))); + console.produce(new DevConsoleTemplateInfoBuildItem("tokenUrl", realmUrl + "/protocol/openid-connect/token")); + console.produce( + new DevConsoleTemplateInfoBuildItem("authorizationUrl", realmUrl + "/protocol/openid-connect/auth")); + console.produce(new DevConsoleTemplateInfoBuildItem("logoutUrl", realmUrl + "/protocol/openid-connect/logout")); console.produce(new DevConsoleTemplateInfoBuildItem("oidcGrantType", config.devservices.grant.type.getGrantType())); } } @@ -42,15 +47,19 @@ public void setConfigProperties(BuildProducer c @BuildStep(onlyIf = IsDevelopment.class) void invokeEndpoint(BuildProducer devConsoleRoute, Optional configProps) { - if (configProps.isPresent()) { + if (configProps.isPresent() && configProps.get().getProperties().containsKey("keycloak.url")) { @SuppressWarnings("unchecked") Map users = (Map) configProps.get().getProperties().get("oidc.users"); devConsoleRoute.produce( new DevConsoleRouteBuildItem("testService", "POST", new KeycloakDevConsolePostHandler(users))); devConsoleRoute.produce( - new DevConsoleRouteBuildItem("testServiceWithToken", "POST", new KeycloakImplicitGrantPostHandler())); + new DevConsoleRouteBuildItem("testServiceWithToken", "POST", + new OidcTestServiceHandler(KeycloakDevServicesProcessor.vertxInstance, + KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout))); devConsoleRoute.produce( - new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", new KeycloakAuthorizationCodePostHandler())); + new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", + new OidcAuthorizationCodePostHandler(KeycloakDevServicesProcessor.vertxInstance, + KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout))); } } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index 5bfc3126ebb1c..809724abe7a2c 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -50,6 +50,7 @@ import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled; import io.quarkus.oidc.deployment.devservices.OidcDevServicesBuildItem; +import io.quarkus.oidc.deployment.devservices.OidcDevServicesUtils; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; import io.vertx.core.Vertx; @@ -453,9 +454,9 @@ private void createRealm(String keycloakUrl, Map users, String o realm.getUsers().add(createUser(entry.getKey(), entry.getValue(), getUserRoles(entry.getKey()))); } - WebClient client = KeycloakDevServicesUtils.createWebClient(); + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); try { - String token = KeycloakDevServicesUtils.getPasswordAccessToken(client, + String token = OidcDevServicesUtils.getPasswordAccessToken(client, keycloakUrl + "/realms/master/protocol/openid-connect/token", "admin-cli", null, "admin", "admin", capturedDevServicesConfiguration.webClienTimeout); diff --git a/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html b/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html index 69c0914f3782e..bc2b692b4d347 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html +++ b/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html @@ -6,5 +6,8 @@ Provider: Keycloak -{#else} +{#else if info:authorizationUrl??} + + + Dev Console {/if} diff --git a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html index 3ab9b02730c08..ff78e4b835361 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html +++ b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html @@ -1,5 +1,11 @@ {#include main fluid=true} -{#title}Keycloak{/title} +{#title} +{#if info:keycloakUrl??} +Keycloak +{#else} +OpenId Connect Dev Console +{/if} +{/title} {#script} var port = {config:property('quarkus.http.port')}; @@ -28,8 +34,8 @@ loggedIn === true; $('.implicitLoggedOut').hide(); $('.implicitLoggedIn').show(); - var hash = window.location.hash; - var code = hash.match(/code=([^&]+)/)[1]; + var search = window.location.search; + var code = search.match(/code=([^&]+)/)[1]; exchangeCodeForTokens(code); }else{ loggedIn === false; @@ -71,18 +77,18 @@ return false; } - function signInToKeycloakAndGetTokens() { + function signInToOidcProviderAndGetTokens() { {#if info:oidcGrantType is 'implicit'} - window.location.href = '{info:keycloakUrl}' + "/realms/" + '{info:keycloakRealm}' + "/protocol/openid-connect/auth" - + "?client_id=" + '{info:keycloakClient}' + window.location.href = '{info:authorizationUrl}' + + "?client_id=" + '{info:clientId}' + "&redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + "%2Fq%2Fdev%2Fio.quarkus.quarkus-oidc%2Fprovider" - + "&scope=openid&response_type=token id_token&response_mode=fragment&prompt=login" + + "&scope=openid&response_type=token id_token&response_mode=query&prompt=login" + "&nonce=" + makeid(); {#else} - window.location.href = '{info:keycloakUrl}' + "/realms/" + '{info:keycloakRealm}' + "/protocol/openid-connect/auth" - + "?client_id=" + '{info:keycloakClient}' + window.location.href = '{info:authorizationUrl}' + + "?client_id=" + '{info:clientId}' + "&redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + "%2Fq%2Fdev%2Fio.quarkus.quarkus-oidc%2Fprovider" - + "&scope=openid&response_type=code&response_mode=fragment&prompt=login" + + "&scope=openid&response_type=code&response_mode=query&prompt=login" + "&nonce=" + makeid(); {/if} } @@ -140,17 +146,16 @@ } function logout() { - window.location.assign('{info:keycloakUrl}' + "/realms/" + '{info:keycloakRealm}' + "/protocol/openid-connect/logout" + window.location.assign('{info:logoutUrl??}' + "?post_logout_redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + "%2Fq%2Fdev%2Fio.quarkus.quarkus-oidc%2Fprovider"); } function exchangeCodeForTokens(code){ $.post("exchangeCodeForTokens", { - keycloakUrl: '{info:keycloakUrl}', - realm: '{info:keycloakRealm}', - client: '{info:keycloakClient}', - clientSecret: '{info:keycloakClientSecret}', + tokenUrl: '{info:tokenUrl}', + client: '{info:clientId}', + clientSecret: '{info:clientSecret}', authorizationCode: code, redirectUri: "http://localhost:" + port + "/q/dev/io.quarkus.quarkus-oidc/provider" }, @@ -179,6 +184,8 @@ } if (userName) { $('#loggedInUser').append("Logged in as " + userName + " "); + } else { + $('#loggedInUser').append("Logged in "); } } return "
" + 
@@ -212,11 +219,10 @@
     function testServiceWithPassword(userName, servicePath){
         $.post("testService",
             {
-              keycloakUrl: '{info:keycloakUrl}',
+              tokenUrl: '{info:tokenUrl}',
               serviceUrl: "http://localhost:" + port + servicePath,
-              realm: '{info:keycloakRealm}',
-              client: '{info:keycloakClient}',
-              clientSecret: '{info:keycloakClientSecret}',
+              client: '{info:clientId}',
+              clientSecret: '{info:clientSecret}',
               user: userName,
               grant: '{info:oidcGrantType}'
             },
@@ -230,11 +236,10 @@
     function testServiceWithClientCredentials(servicePath) {
         $.post("testService",
             {
-              keycloakUrl: '{info:keycloakUrl}',
+              tokenUrl: '{info:tokenUrl}',
               serviceUrl: "http://localhost:" + port + servicePath,
-              realm: '{info:keycloakRealm}',
-              client: '{info:keycloakClient}',
-              clientSecret: '{info:keycloakClientSecret}',
+              client: '{info:clientId}',
+              clientSecret: '{info:clientSecret}',
               grant: '{info:oidcGrantType}'
             },
             function(data, status){
@@ -264,9 +269,9 @@
 {#body}
 

-{#if info:keycloakUrl??} +{#if info:keycloakAdminUrl??}

@@ -278,7 +283,7 @@
+ {#if info:logoutUrl??} + {/if}
@@ -431,7 +438,7 @@
Decoded
{#else if info:oidcGrantType is 'client_credentials'}
- Get access token for {info:keycloakClient} and test your service + Get access token for {info:clientId} and test your service
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleOidcSmokeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleOidcSmokeTest.java index 8c36ccdebd06b..1d8c511494150 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleOidcSmokeTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleOidcSmokeTest.java @@ -20,9 +20,9 @@ public class DevConsoleOidcSmokeTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); @Test - public void testCaches() { + public void testOidcProviderTemplate() { RestAssured.get("q/dev/io.quarkus.quarkus-oidc/provider") .then() - .statusCode(200).body(Matchers.containsString("Keycloak")); + .statusCode(200).body(Matchers.containsString("OpenId Connect Dev Console")); } }