diff --git a/.github/native-tests.json b/.github/native-tests.json
index cd9b3beb494a91..ee6584b2b45f45 100644
--- a/.github/native-tests.json
+++ b/.github/native-tests.json
@@ -75,7 +75,7 @@
{
"category": "Security2",
"timeout": 75,
- "test-modules": "oidc, oidc-code-flow, oidc-tenancy, oidc-client, oidc-client-reactive, oidc-token-propagation, oidc-wiremock, oidc-client-wiremock, oidc-wiremock-providers",
+ "test-modules": "oidc, oidc-code-flow, oidc-tenancy, oidc-client, oidc-client-reactive, oidc-token-propagation, oidc-wiremock, oidc-client-wiremock, oidc-wiremock-providers, oidc-dev-services",
"os-name": "ubuntu-latest"
},
{
diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 5f4dcdca4494ac..74b972f8d4a15d 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -1099,6 +1099,11 @@
quarkus-devservices-keycloak${project.version}
+
+ io.quarkus
+ quarkus-devservices-oidc
+ ${project.version}
+ io.quarkusquarkus-flyway
diff --git a/docs/src/main/asciidoc/images/dev-ui-oidc-dev-svc-login-for-custom-users.png b/docs/src/main/asciidoc/images/dev-ui-oidc-dev-svc-login-for-custom-users.png
new file mode 100644
index 00000000000000..48ed2a6604f72b
Binary files /dev/null and b/docs/src/main/asciidoc/images/dev-ui-oidc-dev-svc-login-for-custom-users.png differ
diff --git a/docs/src/main/asciidoc/images/dev-ui-oidc-dev-svc-login-page.png b/docs/src/main/asciidoc/images/dev-ui-oidc-dev-svc-login-page.png
new file mode 100644
index 00000000000000..7d923c72623e0f
Binary files /dev/null and b/docs/src/main/asciidoc/images/dev-ui-oidc-dev-svc-login-page.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 dcdbdbeac04150..2cc1425252e8b7 100644
--- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc
+++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc
@@ -21,11 +21,12 @@ The Dev Services for Keycloak feature starts a Keycloak container for both the d
It initializes them by registering the existing Keycloak realm or creating a new realm with the client and users required for you to start developing your Quarkus application secured by Keycloak immediately.
The container restarts when the `application.properties` or the realm file changes have been detected.
-Additionally, xref:dev-ui.adoc[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.
+Additionally, xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-ui/extensions[/q/dev-ui/extensions] 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, is activated.
For more information, see <>.
+[[dev-services-for-keycloak]]
== Dev Services for Keycloak
Start your application without configuring `quarkus.oidc` properties in the `application.properties` file:
@@ -406,6 +407,46 @@ This document refers to the `http://localhost:8080/q/dev-ui` Dev UI URL in sever
If you customize `quarkus.http.root-path` or `quarkus.http.non-application-root-path` properties, then replace `q` accordingly.
For more information, see the https://quarkus.io/blog/path-resolution-in-quarkus/[Path resolution in Quarkus] blog post.
+== Dev Services for OIDC
+
+When you work with Keycloak in production, <> provides the best dev mode experience.
+For other OpenID Connect providers, it is recommended to enable the Dev Services for OIDC like in the example below:
+
+[source,properties]
+----
+quarkus.oidc.devservices.enabled=true
+----
+
+NOTE: the Dev Services for OIDC are enabled by default if Docker and Podman are not available.
+
+Once enabled, Quarkus starts a new OIDC server that supports most common OpenID Connect operations.
+You can confirm in the Dev UI console that the OIDC server started, you will see output similar to the following:
+
+[source,shell]
+----
+2025-01-08 20:50:20,900 INFO [io.qua.dev.oid.OidcDevServicesProcessor] (build-16) Dev Services for OIDC started on http://localhost:38139
+----
+
+If you navigate to the <>, you can log into the OIDC server as builtin users `alice` or `bob`:
+
+image::dev-ui-oidc-dev-svc-login-page.png[alt=Dev Services for OIDC builtin user login,role="center"]
+
+As always, default `alice` roles are `admin` and `user`, while default `bob` role is `user`.
+Nevertheless, you can configure roles according to your preference:
+
+[source,properties]
+----
+quarkus.oidc.devservices.roles.alice=root <1>
+quarkus.oidc.devservices.roles.bob=guest
+----
+<1> Assign a `root` role to the user `alice`.
+
+Another option is log in as a custom user with username and roles of your choice:
+
+image::dev-ui-oidc-dev-svc-login-for-custom-users.png[alt=Dev Services for OIDC custom user login,role="center"]
+
+Whichever user you choose, password is not required.
+
== References
* xref:dev-ui.adoc[Dev UI]
diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java
index 20857559b3c47d..9a0087eea9e263 100644
--- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java
+++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java
@@ -28,12 +28,6 @@ public interface KeycloakDevServicesConfig {
@WithDefault("true")
boolean enabled();
- /**
- * Use lightweight dev services instead of Keycloak
- */
- @ConfigItem(defaultValue = "false")
- public boolean lightweight;
-
/**
* The container image name for Dev Services providers.
*
diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java
index 6227b263b3125d..8709a2a76b2c26 100644
--- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java
+++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java
@@ -145,7 +145,8 @@ DevServicesResultBuildItem startKeycloakContainer(
DevServicesConfig devServicesConfig, DockerStatusBuildItem dockerStatusBuildItem) {
if (devSvcRequiredMarkerItems.isEmpty()
- || linuxContainersNotAvailable(dockerStatusBuildItem, devSvcRequiredMarkerItems)) {
+ || linuxContainersNotAvailable(dockerStatusBuildItem, devSvcRequiredMarkerItems)
+ || oidcDevServicesEnabled()) {
if (devService != null) {
closeDevService();
}
@@ -248,6 +249,10 @@ public void run() {
return devService.toBuildItem();
}
+ private static boolean oidcDevServicesEnabled() {
+ return ConfigProvider.getConfig().getOptionalValue("quarkus.oidc.devservices.enabled", boolean.class).orElse(false);
+ }
+
private static boolean linuxContainersNotAvailable(DockerStatusBuildItem dockerStatusBuildItem,
List devSvcRequiredMarkerItems) {
if (dockerStatusBuildItem.isContainerRuntimeAvailable()) {
diff --git a/extensions/devservices/oidc/pom.xml b/extensions/devservices/oidc/pom.xml
new file mode 100644
index 00000000000000..ec198748674a61
--- /dev/null
+++ b/extensions/devservices/oidc/pom.xml
@@ -0,0 +1,53 @@
+
+
+
+ quarkus-devservices-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+ 4.0.0
+
+ quarkus-devservices-oidc
+ Quarkus - DevServices - OIDC
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-devservices-common
+
+
+ io.smallrye.reactive
+ smallrye-mutiny-vertx-web
+
+
+ io.smallrye
+ smallrye-jwt-build
+
+
+
+
+
+ maven-compiler-plugin
+
+
+ default-compile
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
+
diff --git a/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesConfig.java b/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesConfig.java
new file mode 100644
index 00000000000000..e97eef86dad8d0
--- /dev/null
+++ b/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesConfig.java
@@ -0,0 +1,35 @@
+package io.quarkus.devservices.oidc;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigDocDefault;
+import io.quarkus.runtime.annotations.ConfigDocMapKey;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+
+/**
+ * OpenID Connect Dev Services configuration.
+ */
+@ConfigRoot
+@ConfigMapping(prefix = "quarkus.oidc.devservices")
+public interface OidcDevServicesConfig {
+
+ /**
+ * Use OpenID Connect Dev Services instead of Keycloak.
+ */
+ @ConfigDocDefault("Enabled when Docker and Podman are not available")
+ Optional enabled();
+
+ /**
+ * A map of roles for OIDC identity provider users.
+ *
+ * If empty, default roles are assigned: `alice` receives `admin` and `user` roles, while other users receive
+ * `user` role.
+ * This map is used for role creation when no realm file is found at the `realm-path`.
+ */
+ @ConfigDocMapKey("role-name")
+ Map> roles();
+
+}
diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/LightweightDevServicesConfigBuildItem.java b/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesConfigBuildItem.java
similarity index 50%
rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/LightweightDevServicesConfigBuildItem.java
rename to extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesConfigBuildItem.java
index 30d9fac042b9bc..14fc63be89258b 100644
--- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/LightweightDevServicesConfigBuildItem.java
+++ b/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesConfigBuildItem.java
@@ -1,18 +1,22 @@
-package io.quarkus.oidc.deployment.devservices.keycloak;
+package io.quarkus.devservices.oidc;
import java.util.Map;
import io.quarkus.builder.item.SimpleBuildItem;
-public final class LightweightDevServicesConfigBuildItem extends SimpleBuildItem {
+/**
+ * OIDC Dev Services configuration properties.
+ */
+public final class OidcDevServicesConfigBuildItem extends SimpleBuildItem {
private final Map config;
- public LightweightDevServicesConfigBuildItem(Map config) {
+ OidcDevServicesConfigBuildItem(Map config) {
this.config = config;
}
public Map getConfig() {
return config;
}
+
}
diff --git a/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesProcessor.java b/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesProcessor.java
new file mode 100644
index 00000000000000..1729e3ad64e706
--- /dev/null
+++ b/extensions/devservices/oidc/src/main/java/io/quarkus/devservices/oidc/OidcDevServicesProcessor.java
@@ -0,0 +1,932 @@
+package io.quarkus.devservices.oidc;
+
+import static io.quarkus.deployment.bean.JavaBeanUtil.capitalize;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.eclipse.microprofile.jwt.Claims;
+import org.jboss.logging.Logger;
+import org.jose4j.base64url.Base64Url;
+
+import io.quarkus.deployment.IsNormal;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem;
+import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
+import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService;
+import io.quarkus.deployment.builditem.DockerStatusBuildItem;
+import io.quarkus.deployment.dev.devservices.DevServicesConfig;
+import io.quarkus.runtime.configuration.ConfigUtils;
+import io.smallrye.jwt.build.Jwt;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.json.JsonObject;
+import io.vertx.mutiny.core.Vertx;
+import io.vertx.mutiny.core.http.HttpServer;
+import io.vertx.mutiny.ext.web.Router;
+import io.vertx.mutiny.ext.web.RoutingContext;
+import io.vertx.mutiny.ext.web.handler.BodyHandler;
+
+@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = DevServicesConfig.Enabled.class)
+public class OidcDevServicesProcessor {
+
+ private static final Logger LOG = Logger.getLogger(OidcDevServicesProcessor.class);
+ private static final String CONFIG_PREFIX = "quarkus.oidc.";
+ private static final String OIDC_ENABLED = CONFIG_PREFIX + "enabled";
+ 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 PROVIDER_CONFIG_KEY = CONFIG_PREFIX + "provider";
+ private static final String APPLICATION_TYPE_CONFIG_KEY = CONFIG_PREFIX + "application-type";
+ private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id";
+ private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret";
+
+ private static volatile KeyPair kp;
+ private static volatile String kid;
+ private static volatile String baseURI;
+ private static volatile String clientId;
+ private static volatile String clientSecret;
+ private static volatile String applicationType;
+ private static volatile Map configProperties;
+ private static volatile Map> userToDefaultRoles;
+ private static volatile Runnable closeDevServiceTask;
+
+ @BuildStep
+ DevServicesResultBuildItem startServer(CuratedApplicationShutdownBuildItem closeBuildItem,
+ OidcDevServicesConfig devServicesConfig, DockerStatusBuildItem dockerStatusBuildItem,
+ BuildProducer devServiceConfigProducer) {
+ if (shouldNotStartServer(devServicesConfig, dockerStatusBuildItem)) {
+ closeDevSvcIfNecessary();
+ return null;
+ }
+
+ userToDefaultRoles = devServicesConfig.roles();
+ if (closeDevServiceTask == null) {
+ LOG.info("Starting Dev Services for OIDC");
+ Vertx vertx = Vertx.vertx();
+ HttpServerOptions options = new HttpServerOptions();
+ options.setPort(0);
+ HttpServer httpServer = vertx.createHttpServer(options);
+
+ Router router = Router.router(vertx);
+ httpServer.requestHandler(router);
+ registerRoutes(router);
+
+ httpServer.listenAndAwait();
+ baseURI = "http://localhost:" + httpServer.actualPort();
+ closeDevServiceTask = new Runnable() {
+
+ private volatile boolean closed = false;
+
+ @Override
+ public void run() {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ // this is done on delegates because closing Mutiny wrapper can result in unrelated exception
+ // when other tests (not necessarily using this dev services) run after a test using this service
+ httpServer.getDelegate().close(httpServerResult -> {
+ if (httpServerResult != null && httpServerResult.failed()) {
+ LOG.error("Failed to close HTTP Server", httpServerResult.cause());
+ }
+ vertx.getDelegate().close(vertxResult -> {
+ if (vertxResult != null && vertxResult.failed()) {
+ LOG.error("Failed to close Vertx instance", vertxResult.cause());
+ }
+ });
+ });
+ }
+ };
+ closeBuildItem.addCloseTask(OidcDevServicesProcessor::closeDevSvcIfNecessary, true);
+ updateDevSvcConfigProperties();
+ LOG.infof("Dev Services for OIDC started on %s", baseURI);
+ } else if (!getOidcClientId().equals(clientId) || !getOidcApplicationType().equals(applicationType)) {
+ updateDevSvcConfigProperties();
+ }
+
+ devServiceConfigProducer.produce(new OidcDevServicesConfigBuildItem(configProperties));
+ return new RunningDevService("oidc-dev-services", null, () -> {
+ }, configProperties).toBuildItem();
+ }
+
+ private static void closeDevSvcIfNecessary() {
+ if (closeDevServiceTask != null) {
+ closeDevServiceTask.run();
+ closeDevServiceTask = null;
+ }
+ }
+
+ private static boolean shouldNotStartServer(OidcDevServicesConfig devServicesConfig,
+ DockerStatusBuildItem dockerStatusBuildItem) {
+ boolean explicitlyDisabled = devServicesConfig.enabled().isPresent() && !devServicesConfig.enabled().get();
+ if (explicitlyDisabled) {
+ LOG.debug("Not starting Dev Services for OIDC as it has been disabled in the config");
+ return true;
+ }
+ if (devServicesConfig.enabled().isEmpty() && dockerStatusBuildItem.isContainerRuntimeAvailable()) {
+ LOG.debug("Not starting Dev Services for OIDC as detected support the container functionality");
+ return true;
+ }
+ if (!isOidcEnabled()) {
+ LOG.debug("Not starting Dev Services for OIDC as OIDC extension has been disabled in the config");
+ return true;
+ }
+ if (!isOidcTenantEnabled()) {
+ LOG.debug("Not starting Dev Services for OIDC as 'quarkus.oidc.tenant.enabled' is false");
+ return true;
+ }
+ if (ConfigUtils.isPropertyPresent(AUTH_SERVER_URL_CONFIG_KEY)) {
+ LOG.debug("Not starting Dev Services for OIDC as 'quarkus.oidc.auth-server-url' has been provided");
+ return true;
+ }
+ if (ConfigUtils.isPropertyPresent(PROVIDER_CONFIG_KEY)) {
+ LOG.debug("Not starting Dev Services for OIDC as 'quarkus.oidc.provider' has been provided");
+ return true;
+ }
+ return false;
+ }
+
+ private static void updateDevSvcConfigProperties() {
+ // relevant configuration has changed
+ clientId = getOidcClientId();
+ clientSecret = getOidcClientSecret();
+ applicationType = getOidcApplicationType();
+ final Map aConfigProperties = new HashMap<>();
+ aConfigProperties.put(AUTH_SERVER_URL_CONFIG_KEY, baseURI);
+ aConfigProperties.put(APPLICATION_TYPE_CONFIG_KEY, applicationType);
+ aConfigProperties.put(CLIENT_ID_CONFIG_KEY, clientId);
+ aConfigProperties.put(CLIENT_SECRET_CONFIG_KEY, clientSecret);
+ configProperties = Map.copyOf(aConfigProperties);
+ }
+
+ private static void registerRoutes(Router router) {
+ BodyHandler bodyHandler = BodyHandler.create();
+ router.get("/").handler(OidcDevServicesProcessor::mainRoute);
+ router.get("/.well-known/openid-configuration").handler(OidcDevServicesProcessor::configuration);
+ router.get("/authorize").handler(OidcDevServicesProcessor::authorize);
+ router.post("/login").handler(bodyHandler).handler(OidcDevServicesProcessor::login);
+ router.post("/token").handler(bodyHandler).handler(OidcDevServicesProcessor::token);
+ router.get("/keys").handler(OidcDevServicesProcessor::getKeys);
+ router.get("/logout").handler(OidcDevServicesProcessor::logout);
+ router.get("/userinfo").handler(OidcDevServicesProcessor::userInfo);
+
+ // can be used for testing of bearer token authentication
+ router.get("/testing/generate/access-token").handler(OidcDevServicesProcessor::generateAccessToken);
+
+ KeyPairGenerator kpg;
+ try {
+ kpg = KeyPairGenerator.getInstance("RSA");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ kpg.initialize(2048);
+ kp = kpg.generateKeyPair();
+ kid = createKeyId();
+ }
+
+ private static void generateAccessToken(RoutingContext rc) {
+ String user = rc.request().getParam("user");
+ if (user == null || user.isEmpty()) {
+ rc.response().setStatusCode(400).endAndForget("Missing required parameter: user");
+ return;
+ }
+ String rolesParam = rc.request().getParam("roles");
+ Set roles = new HashSet<>();
+ if (rolesParam == null || rolesParam.isEmpty()) {
+ roles.addAll(getUserRoles(user));
+ } else {
+ roles.addAll(Arrays.asList(rolesParam.split(",")));
+ }
+ rc.response().endAndForget(createAccessToken(user, roles, Set.of("openid", "email")));
+ }
+
+ private static List getUsers() {
+ if (userToDefaultRoles.isEmpty()) {
+ return Arrays.asList("alice", "bob");
+ } else {
+ List ret = new ArrayList<>(userToDefaultRoles.keySet());
+ Collections.sort(ret);
+ return ret;
+ }
+ }
+
+ private static List getUserRoles(String user) {
+ List roles = userToDefaultRoles.get(user);
+ return roles == null ? ("alice".equals(user) ? List.of("admin", "user") : List.of("user"))
+ : roles;
+ }
+
+ private static boolean isOidcEnabled() {
+ return ConfigProvider.getConfig().getValue(OIDC_ENABLED, Boolean.class);
+ }
+
+ private static boolean isOidcTenantEnabled() {
+ return ConfigProvider.getConfig().getOptionalValue(TENANT_ENABLED_CONFIG_KEY, Boolean.class).orElse(true);
+ }
+
+ private static String getOidcApplicationType() {
+ return ConfigProvider.getConfig().getOptionalValue(APPLICATION_TYPE_CONFIG_KEY, String.class).orElse("service");
+ }
+
+ private static String getOidcClientId() {
+ return ConfigProvider.getConfig().getOptionalValue(CLIENT_ID_CONFIG_KEY, String.class)
+ .orElse("quarkus-app");
+ }
+
+ private static String getOidcClientSecret() {
+ return ConfigProvider.getConfig().getOptionalValue(CLIENT_SECRET_CONFIG_KEY, String.class)
+ .orElseGet(() -> UUID.randomUUID().toString());
+ }
+
+ private static void mainRoute(RoutingContext rc) {
+ rc.response().endAndForget("OIDC server up and running");
+ }
+
+ private static void configuration(RoutingContext rc) {
+ String data = """
+ {
+ "token_endpoint":"%1$s/token",
+ "token_endpoint_auth_methods_supported":[
+ "client_secret_post",
+ "private_key_jwt",
+ "client_secret_basic"
+ ],
+ "jwks_uri":"%1$s/keys",
+ "response_modes_supported":[
+ "query"
+ ],
+ "subject_types_supported":[
+ "pairwise"
+ ],
+ "id_token_signing_alg_values_supported":[
+ "RS256"
+ ],
+ "response_types_supported":[
+ "code",
+ "id_token",
+ "code id_token",
+ "id_token token",
+ "code id_token token"
+ ],
+ "scopes_supported":[
+ "openid",
+ "profile",
+ "email",
+ "offline_access"
+ ],
+ "issuer":"%1$s",
+ "request_uri_parameter_supported":false,
+ "userinfo_endpoint":"%1$s/userinfo",
+ "authorization_endpoint":"%1$s/authorize",
+ "device_authorization_endpoint":"%1$s/devicecode",
+ "http_logout_supported":true,
+ "frontchannel_logout_supported":true,
+ "end_session_endpoint":"%1$s/logout",
+ "claims_supported":[
+ "sub",
+ "iss",
+ "aud",
+ "exp",
+ "iat",
+ "auth_time",
+ "acr",
+ "nonce",
+ "preferred_username",
+ "name",
+ "tid",
+ "ver",
+ "at_hash",
+ "c_hash",
+ "email"
+ ]
+ }
+ """.formatted(baseURI);
+ rc.response().putHeader("Content-Type", "application/json");
+ rc.endAndForget(data);
+ }
+
+ /*
+ * First request:
+ * GET
+ * https://localhost:X/authorize?response_type=code&client_id=SECRET&scope=openid+openid+
+ * email+profile&redirect_uri=http://localhost:8080/Login/oidcLoginSuccess&state=STATE
+ *
+ * returns a 302 to
+ * GET http://localhost:8080/Login/oidcLoginSuccess?code=CODE&state=STATE
+ */
+ private static void authorize(RoutingContext rc) {
+ String response_type = rc.request().params().get("response_type");
+ String clientId = rc.request().params().get("client_id");
+ String scope = rc.request().params().get("scope");
+ String state = rc.request().params().get("state");
+ String redirect_uri = rc.request().params().get("redirect_uri");
+ URI redirect;
+ try {
+ redirect = new URI(redirect_uri + "?state=" + state);
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ StringBuilder predefinedUsers = new StringBuilder();
+ for (String predefinedUser : getUsers()) {
+ predefinedUsers.append(" \n");
+ }
+ rc.response()
+ .endAndForget(
+ """
+
+
+ Login
+
+
+
+