From 3536044b8e0947765c84c45d0b0f5e5e177ca200 Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sat, 15 Aug 2026 19:15:56 -0400 Subject: [PATCH 1/7] Add OryAuth provider config Simplified factory for Ory Network / self hosted Ory Hydra with the public OAuth2/OIDC endpoints documented in the Ory Hydra API reference, plus an OpenID Connect discovery shortcut. Fixes eclipse-vertx/vertx-auth#377 --- .../ext/auth/oauth2/providers/OryAuth.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OryAuth.java diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OryAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OryAuth.java new file mode 100644 index 000000000..bf067cbc5 --- /dev/null +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OryAuth.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.auth.oauth2.providers; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2Options; + +/** + * Simplified factory to create an {@link OAuth2Auth} for Ory (Ory Network or a self hosted Ory Hydra). + *

+ * Ory Hydra is a certified OpenID Connect provider. The endpoints below are taken from the Ory Hydra public API + * reference (https://www.ory.com/docs/hydra/reference/api) + * and the Ory OAuth2 authorization code flow guide + * (https://www.ory.com/docs/oauth2-oidc/authorization-code-flow). + *

+ * The {@code site} is the base URL of the public API of your deployment: + *

+ *

+ * The {@code /userinfo} endpoint returns the standard OpenID Connect claims for the granted scopes + * ({@code sub} always, plus e.g. {@code email}, {@code email_verified}, {@code name}, {@code preferred_username} + * as populated by your consent app). + *

+ * Notes: + *

+ */ +@VertxGen +public interface OryAuth extends OpenIDConnectAuth { + + /** + * Create a OAuth2Auth provider for Ory + * + * @param site root URL for the provider without trailing slashes, eg. https://{project-slug}.projects.oryapis.com + * @param clientId the client id given to you by Ory + * @param clientSecret the client secret given to you by Ory + */ + static OAuth2Auth create(Vertx vertx, String site, String clientId, String clientSecret) { + return create(vertx, site, clientId, clientSecret, new HttpClientOptions()); + } + + /** + * Create a OAuth2Auth provider for Ory + * + * @param site root URL for the provider without trailing slashes, eg. https://{project-slug}.projects.oryapis.com + * @param clientId the client id given to you by Ory + * @param clientSecret the client secret given to you by Ory + * @param httpClientOptions custom http client options + */ + static OAuth2Auth create(Vertx vertx, String site, String clientId, String clientSecret, HttpClientOptions httpClientOptions) { + return + OAuth2Auth.create(vertx, new OAuth2Options() + .setHttpClientOptions(httpClientOptions) + .setClientId(clientId) + .setClientSecret(clientSecret) + .setSite(site) + // https://www.ory.com/docs/hydra/reference/api (public endpoints) + .setAuthorizationPath("/oauth2/auth") + .setTokenPath("/oauth2/token") + .setUserInfoPath("/userinfo") + // RFC 7009 + .setRevocationPath("/oauth2/revoke") + // OpenID Connect RP-Initiated Logout + .setLogoutPath("/oauth2/sessions/logout") + // RFC 7517 + .setJwkPath("/.well-known/jwks.json") + .setScopeSeparator(" ")); + } + + /** + * Create a OAuth2Auth provider for OpenID Connect Discovery. The discovery will use the given site in the + * configuration options and attempt to load the well known descriptor + * ({@code {site}/.well-known/openid-configuration}). + *

+ * If the discovered config includes a json web key url, it will be also fetched and the JWKs will be loaded + * into the OAuth provider so tokens can be decoded. + * + * @param vertx the vertx instance + * @param config the initial config, the site must be set to the Ory public API base URL + * @return future with instantiated Oauth2 provider instance handler + */ + static Future discover(final Vertx vertx, final OAuth2Options config) { + return OpenIDConnectAuth.discover(vertx, config); + } +} From 571b1729d14249a7c192fb2063826c17d677962c Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sat, 15 Aug 2026 19:15:56 -0400 Subject: [PATCH 2/7] Add OktaAuth provider config Supports both the org authorization server and custom authorization servers (e.g. "default"), with endpoints from the Okta OIDC API reference. See eclipse-vertx/vertx-auth#419 --- .../ext/auth/oauth2/providers/OktaAuth.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OktaAuth.java diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OktaAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OktaAuth.java new file mode 100644 index 000000000..fcc4de0bd --- /dev/null +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OktaAuth.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.auth.oauth2.providers; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2Options; + +/** + * Simplified factory to create an {@link OAuth2Auth} for Okta. + *

+ * Okta exposes two kinds of OpenID Connect authorization servers + * (https://developer.okta.com/docs/concepts/auth-servers/): + *

+ * The endpoint paths are documented in the Okta OpenID Connect & OAuth 2.0 API reference: + * https://developer.okta.com/docs/reference/api/oidc/. + *

+ * The {@code /userinfo} endpoint returns the OpenID Connect claims for the granted scopes: {@code sub} always; + * {@code name}, {@code nickname}, {@code preferred_username}, {@code given_name}, {@code middle_name}, + * {@code family_name}, {@code picture}, {@code website}, {@code gender}, {@code birthdate}, {@code zoneinfo}, + * {@code locale}, {@code updated_at} for {@code profile}; {@code email}, {@code email_verified} for {@code email}; + * {@code address} for {@code address}; {@code phone_number} for {@code phone}. + *

+ * Client credentials are sent using HTTP Basic authentication ({@code client_secret_basic}), Okta's default + * token endpoint authentication method + * (https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/). + */ +@VertxGen +public interface OktaAuth extends OpenIDConnectAuth { + + /** + * Create a OAuth2Auth provider for Okta using the org authorization server. + * + * @param clientId the client id given to you by Okta + * @param clientSecret the client secret given to you by Okta + * @param domain your Okta domain, eg. {@code dev-123456.okta.com} + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, String domain) { + return create(vertx, clientId, clientSecret, domain, new HttpClientOptions()); + } + + /** + * Create a OAuth2Auth provider for Okta using the org authorization server. + * + * @param clientId the client id given to you by Okta + * @param clientSecret the client secret given to you by Okta + * @param domain your Okta domain, eg. {@code dev-123456.okta.com} + * @param httpClientOptions custom http client options + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, String domain, HttpClientOptions httpClientOptions) { + return + OAuth2Auth.create(vertx, new OAuth2Options() + .setHttpClientOptions(httpClientOptions) + .setClientId(clientId) + .setClientSecret(clientSecret) + .setTenant(domain) + // https://developer.okta.com/docs/concepts/auth-servers/#org-authorization-server + .setSite("https://{tenant}") + // https://developer.okta.com/docs/reference/api/oidc/#endpoints + .setAuthorizationPath("/oauth2/v1/authorize") + .setTokenPath("/oauth2/v1/token") + .setUserInfoPath("/oauth2/v1/userinfo") + // RFC 7009 + .setRevocationPath("/oauth2/v1/revoke") + // RFC 7662 + .setIntrospectionPath("/oauth2/v1/introspect") + // RFC 7517 + .setJwkPath("/oauth2/v1/keys") + .setLogoutPath("/oauth2/v1/logout") + .setScopeSeparator(" ")); + } + + /** + * Create a OAuth2Auth provider for Okta using a custom authorization server. + * + * @param clientId the client id given to you by Okta + * @param clientSecret the client secret given to you by Okta + * @param domain your Okta domain, eg. {@code dev-123456.okta.com} + * @param authorizationServerId the custom authorization server id, eg. {@code default} + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, String domain, String authorizationServerId) { + return create(vertx, clientId, clientSecret, domain, authorizationServerId, new HttpClientOptions()); + } + + /** + * Create a OAuth2Auth provider for Okta using a custom authorization server. + * + * @param clientId the client id given to you by Okta + * @param clientSecret the client secret given to you by Okta + * @param domain your Okta domain, eg. {@code dev-123456.okta.com} + * @param authorizationServerId the custom authorization server id, eg. {@code default} + * @param httpClientOptions custom http client options + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, String domain, String authorizationServerId, HttpClientOptions httpClientOptions) { + return + OAuth2Auth.create(vertx, new OAuth2Options() + .setHttpClientOptions(httpClientOptions) + .setClientId(clientId) + .setClientSecret(clientSecret) + .setTenant(domain) + // https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server + .setSite("https://{tenant}/oauth2/" + authorizationServerId) + // https://developer.okta.com/docs/reference/api/oidc/#endpoints + .setAuthorizationPath("/v1/authorize") + .setTokenPath("/v1/token") + .setUserInfoPath("/v1/userinfo") + // RFC 7009 + .setRevocationPath("/v1/revoke") + // RFC 7662 + .setIntrospectionPath("/v1/introspect") + // RFC 7517 + .setJwkPath("/v1/keys") + .setLogoutPath("/v1/logout") + .setScopeSeparator(" ")); + } + + /** + * Create a OAuth2Auth provider for OpenID Connect Discovery. The discovery will use the site in the + * configuration options and attempt to load the well known descriptor. The site is the issuer of the + * authorization server, either {@code https://{yourOktaDomain}} (org authorization server) or + * {@code https://{yourOktaDomain}/oauth2/{authorizationServerId}} (custom authorization server). + *

+ * If the discovered config includes a json web key url, it will be also fetched and the JWKs will be loaded + * into the OAuth provider so tokens can be decoded. + * + * @param vertx the vertx instance + * @param config the initial config + * @return future with instantiated Oauth2 provider instance handler + */ + static Future discover(final Vertx vertx, final OAuth2Options config) { + return OpenIDConnectAuth.discover(vertx, config); + } +} From c5fa4b85a5c6e39ab8c44a463e502f60b9f1b2e5 Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sat, 15 Aug 2026 19:15:56 -0400 Subject: [PATCH 3/7] Add DiscordAuth provider config See eclipse-vertx/vertx-auth#419 --- .../auth/oauth2/providers/DiscordAuth.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/DiscordAuth.java diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/DiscordAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/DiscordAuth.java new file mode 100644 index 000000000..dc32925cf --- /dev/null +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/DiscordAuth.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.auth.oauth2.providers; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2Options; + +/** + * Simplified factory to create an {@link OAuth2Auth} for Discord. + *

+ * The OAuth2 endpoints are documented at + * https://docs.discord.com/developers/topics/oauth2. + *

+ * The user info endpoint is {@code GET /users/@me} + * (https://docs.discord.com/developers/resources/user#get-current-user) + * and requires the {@code identify} scope. It returns a user object with, among others, {@code id}, + * {@code username}, {@code discriminator}, {@code global_name}, {@code avatar}, {@code banner}, + * {@code accent_color}, {@code locale}, {@code mfa_enabled}, {@code flags}, {@code public_flags}, and + * {@code email}, {@code verified} when the {@code email} scope is also granted. + */ +@VertxGen +public interface DiscordAuth { + + /** + * Create a OAuth2Auth provider for Discord + * + * @param clientId the client id given to you by Discord + * @param clientSecret the client secret given to you by Discord + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret) { + return create(vertx, clientId, clientSecret, new HttpClientOptions()); + } + + /** + * Create a OAuth2Auth provider for Discord + * + * @param clientId the client id given to you by Discord + * @param clientSecret the client secret given to you by Discord + * @param httpClientOptions custom http client options + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, HttpClientOptions httpClientOptions) { + return + OAuth2Auth.create(vertx, new OAuth2Options() + .setHttpClientOptions(httpClientOptions) + .setClientId(clientId) + .setClientSecret(clientSecret) + .setSite("https://discord.com/api") + // https://docs.discord.com/developers/topics/oauth2#shared-resources-oauth2-urls + .setAuthorizationPath("https://discord.com/oauth2/authorize") + .setTokenPath("/oauth2/token") + .setRevocationPath("/oauth2/token/revoke") + // https://docs.discord.com/developers/resources/user#get-current-user + .setUserInfoPath("/users/@me") + .setScopeSeparator(" ")); + } +} From cd9afc444c2e74f34f0f49ec628b8e6adcb3d065 Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sat, 15 Aug 2026 19:15:56 -0400 Subject: [PATCH 4/7] Add TwitchAuth provider config Twitch expects the client credentials in the token request body, so basic authorization is disabled for this provider. See eclipse-vertx/vertx-auth#419 --- .../ext/auth/oauth2/providers/TwitchAuth.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/TwitchAuth.java diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/TwitchAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/TwitchAuth.java new file mode 100644 index 000000000..da4a1045d --- /dev/null +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/TwitchAuth.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.auth.oauth2.providers; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2Options; + +/** + * Simplified factory to create an {@link OAuth2Auth} for Twitch. + *

+ * Twitch is an OpenID Connect provider, the endpoints are documented at + * https://dev.twitch.tv/docs/authentication/ and + * published in the discovery document {@code https://id.twitch.tv/oauth2/.well-known/openid-configuration}. + *

+ * The {@code /userinfo} endpoint (requires the {@code openid} scope, see + * https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/) + * always returns {@code aud}, {@code exp}, {@code iat}, {@code iss} and {@code sub} (the user id); the claims + * {@code email}, {@code email_verified}, {@code picture}, {@code preferred_username} and {@code updated_at} are + * only returned when requested with the {@code claims} parameter of the authorization request. + *

+ * Twitch expects the client credentials in the body of the token request rather than in an HTTP Basic + * {@code Authorization} header + * (https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/), + * the provider is configured accordingly. + */ +@VertxGen +public interface TwitchAuth extends OpenIDConnectAuth { + + /** + * Create a OAuth2Auth provider for Twitch + * + * @param clientId the client id given to you by Twitch + * @param clientSecret the client secret given to you by Twitch + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret) { + return create(vertx, clientId, clientSecret, new HttpClientOptions()); + } + + /** + * Create a OAuth2Auth provider for Twitch + * + * @param clientId the client id given to you by Twitch + * @param clientSecret the client secret given to you by Twitch + * @param httpClientOptions custom http client options + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, HttpClientOptions httpClientOptions) { + return + OAuth2Auth.create(vertx, new OAuth2Options() + .setHttpClientOptions(httpClientOptions) + .setClientId(clientId) + .setClientSecret(clientSecret) + // issuer, https://id.twitch.tv/oauth2/.well-known/openid-configuration + .setSite("https://id.twitch.tv/oauth2") + .setAuthorizationPath("/authorize") + .setTokenPath("/token") + .setUserInfoPath("/userinfo") + // https://dev.twitch.tv/docs/authentication/revoke-tokens/ + .setRevocationPath("/revoke") + // RFC 7517 + .setJwkPath("/keys") + .setScopeSeparator(" ") + // client credentials must be sent in the request body + .setUseBasicAuthorization(false)); + } + + /** + * Create a OAuth2Auth provider for OpenID Connect Discovery. The discovery will use the default site in the + * configuration options and attempt to load the well known descriptor. If a site is provided (for example when + * running on a custom instance) that site will be used to do the lookup. + *

+ * If the discovered config includes a json web key url, it will be also fetched and the JWKs will be loaded + * into the OAuth provider so tokens can be decoded. + * + * @param vertx the vertx instance + * @param config the initial config + * @return future with instantiated Oauth2 provider instance handler + */ + static Future discover(final Vertx vertx, final OAuth2Options config) { + // don't override if already set + final String site = config.getSite() == null ? "https://id.twitch.tv/oauth2" : config.getSite(); + + return OpenIDConnectAuth.discover( + vertx, + new OAuth2Options(config) + .setSite(site) + // client credentials must be sent in the request body + .setUseBasicAuthorization(false)); + } +} From a92465754a89ad450dc2e79858237f1934e8a846 Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sat, 15 Aug 2026 19:15:56 -0400 Subject: [PATCH 5/7] Add SpotifyAuth provider config See eclipse-vertx/vertx-auth#419 --- .../auth/oauth2/providers/SpotifyAuth.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/SpotifyAuth.java diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/SpotifyAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/SpotifyAuth.java new file mode 100644 index 000000000..6b9a66529 --- /dev/null +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/SpotifyAuth.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.auth.oauth2.providers; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2Options; + +/** + * Simplified factory to create an {@link OAuth2Auth} for Spotify. + *

+ * The OAuth2 endpoints are documented in the Spotify authorization code flow guide + * (https://developer.spotify.com/documentation/web-api/tutorials/code-flow). + * The token endpoint authenticates the client with an HTTP Basic {@code Authorization} header, which is the + * default behaviour of this module. + *

+ * The user info endpoint is {@code GET https://api.spotify.com/v1/me} + * (https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile) + * and returns, among others, {@code id}, {@code display_name}, {@code uri}, {@code href}, {@code images}, + * {@code followers}, {@code external_urls}; {@code country}, {@code product}, {@code explicit_content} require the + * {@code user-read-private} scope and {@code email} requires the {@code user-read-email} scope. + */ +@VertxGen +public interface SpotifyAuth { + + /** + * Create a OAuth2Auth provider for Spotify + * + * @param clientId the client id given to you by Spotify + * @param clientSecret the client secret given to you by Spotify + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret) { + return create(vertx, clientId, clientSecret, new HttpClientOptions()); + } + + /** + * Create a OAuth2Auth provider for Spotify + * + * @param clientId the client id given to you by Spotify + * @param clientSecret the client secret given to you by Spotify + * @param httpClientOptions custom http client options + */ + static OAuth2Auth create(Vertx vertx, String clientId, String clientSecret, HttpClientOptions httpClientOptions) { + return + OAuth2Auth.create(vertx, new OAuth2Options() + .setHttpClientOptions(httpClientOptions) + .setClientId(clientId) + .setClientSecret(clientSecret) + // https://developer.spotify.com/documentation/web-api/tutorials/code-flow + .setSite("https://accounts.spotify.com") + .setAuthorizationPath("/authorize") + .setTokenPath("/api/token") + // https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile + .setUserInfoPath("https://api.spotify.com/v1/me") + .setScopeSeparator(" ")); + } +} From 65d193468609f84f29e8e301f25c6a72feb9459c Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sat, 15 Aug 2026 19:15:56 -0400 Subject: [PATCH 6/7] Test and document the new OAuth2 provider configs ProviderConfigTest asserts, without network access, that each new provider shortcut configures the endpoints documented by the provider. The docs list the new providers and the OIDC discovery URLs for Okta, Ory and Twitch. --- .../src/main/asciidoc/index.adoc | 8 ++ .../io/vertx/tests/ProviderConfigTest.java | 127 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 vertx-auth-oauth2/src/test/java/io/vertx/tests/ProviderConfigTest.java diff --git a/vertx-auth-oauth2/src/main/asciidoc/index.adoc b/vertx-auth-oauth2/src/main/asciidoc/index.adoc index 7fb5d9615..c697e7e6b 100644 --- a/vertx-auth-oauth2/src/main/asciidoc/index.adoc +++ b/vertx-auth-oauth2/src/main/asciidoc/index.adoc @@ -129,6 +129,9 @@ A couple of well known OpenID Connect Discovery providers are: * Microsoft: `https://login.windows.net/common` * IBM Cloud: `https://.appid.cloud.ibm.com/oauth/v4/` * Amazon Cognito: `"https://cognito-idp..amazonaws.com/"` +* Okta: `https://` (org authorization server) or `https:///oauth2/` +* Ory (Ory Network / Ory Hydra): `https://.projects.oryapis.com` +* Twitch: `https://id.twitch.tv/oauth2` This and the given `client id/client secret` is enough to configure your auth provider object. @@ -165,6 +168,7 @@ Currently we provide: * Azure Active Directory {@link io.vertx.ext.auth.oauth2.providers.AzureADAuth} * Box.com {@link io.vertx.ext.auth.oauth2.providers.BoxAuth} * CloudFoundry {@link io.vertx.ext.auth.oauth2.providers.CloudFoundryAuth} +* Discord {@link io.vertx.ext.auth.oauth2.providers.DiscordAuth} * Dropbox {@link io.vertx.ext.auth.oauth2.providers.DropboxAuth} * Facebook {@link io.vertx.ext.auth.oauth2.providers.FacebookAuth} * Foursquare {@link io.vertx.ext.auth.oauth2.providers.FoursquareAuth} @@ -178,11 +182,15 @@ Currently we provide: * LinkedIn {@link io.vertx.ext.auth.oauth2.providers.LinkedInAuth} * Live.com {@link io.vertx.ext.auth.oauth2.providers.LiveAuth} * Mailchimp {@link io.vertx.ext.auth.oauth2.providers.MailchimpAuth} +* Okta {@link io.vertx.ext.auth.oauth2.providers.OktaAuth} * OpenIDConnect {@link io.vertx.ext.auth.oauth2.providers.OpenIDConnectAuth} +* Ory {@link io.vertx.ext.auth.oauth2.providers.OryAuth} * Salesforce {@link io.vertx.ext.auth.oauth2.providers.SalesforceAuth} * Shopify {@link io.vertx.ext.auth.oauth2.providers.ShopifyAuth} * Soundcloud {@link io.vertx.ext.auth.oauth2.providers.SoundcloudAuth} +* Spotify {@link io.vertx.ext.auth.oauth2.providers.SpotifyAuth} * Stripe {@link io.vertx.ext.auth.oauth2.providers.StripeAuth} +* Twitch {@link io.vertx.ext.auth.oauth2.providers.TwitchAuth} * Twitter {@link io.vertx.ext.auth.oauth2.providers.TwitterAuth} === JBoss Keycloak diff --git a/vertx-auth-oauth2/src/test/java/io/vertx/tests/ProviderConfigTest.java b/vertx-auth-oauth2/src/test/java/io/vertx/tests/ProviderConfigTest.java new file mode 100644 index 000000000..b8fe26a8c --- /dev/null +++ b/vertx-auth-oauth2/src/test/java/io/vertx/tests/ProviderConfigTest.java @@ -0,0 +1,127 @@ +package io.vertx.tests; + +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2Options; +import io.vertx.ext.auth.oauth2.impl.OAuth2AuthProviderImpl; +import io.vertx.ext.auth.oauth2.providers.DiscordAuth; +import io.vertx.ext.auth.oauth2.providers.OktaAuth; +import io.vertx.ext.auth.oauth2.providers.OryAuth; +import io.vertx.ext.auth.oauth2.providers.SpotifyAuth; +import io.vertx.ext.auth.oauth2.providers.TwitchAuth; +import io.vertx.ext.unit.junit.RunTestOnContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Verifies that the provider shortcuts configure the endpoints documented by each provider. + * No network access is required: only the resulting {@link OAuth2Options} are inspected. + */ +@RunWith(VertxUnitRunner.class) +public class ProviderConfigTest { + + @Rule + public final RunTestOnContext rule = new RunTestOnContext(); + + private static OAuth2Options configOf(OAuth2Auth auth) { + return ((OAuth2AuthProviderImpl) auth).getConfig(); + } + + @Test + public void testOryNetwork() { + OAuth2Options cfg = configOf(OryAuth.create(rule.vertx(), "https://my-slug.projects.oryapis.com", "id", "secret")); + assertEquals("id", cfg.getClientId()); + assertEquals("secret", cfg.getClientSecret()); + assertEquals("https://my-slug.projects.oryapis.com", cfg.getSite()); + assertEquals("/oauth2/auth", cfg.getAuthorizationPath()); + assertEquals("/oauth2/token", cfg.getTokenPath()); + assertEquals("/userinfo", cfg.getUserInfoPath()); + assertEquals("/oauth2/revoke", cfg.getRevocationPath()); + assertEquals("/oauth2/sessions/logout", cfg.getLogoutPath()); + assertEquals("/.well-known/jwks.json", cfg.getJwkPath()); + assertEquals(" ", cfg.getScopeSeparator()); + } + + @Test + public void testOrySelfHostedTrailingSlash() { + // self hosted hydra public API, trailing slash must be tolerated + OAuth2Options cfg = configOf(OryAuth.create(rule.vertx(), "http://localhost:4444/", "id", "secret")); + assertEquals("http://localhost:4444", cfg.getSite()); + assertEquals("/oauth2/token", cfg.getTokenPath()); + } + + @Test + public void testOktaOrgAuthorizationServer() { + OAuth2Options cfg = configOf(OktaAuth.create(rule.vertx(), "id", "secret", "dev-123456.okta.com")); + assertEquals("id", cfg.getClientId()); + assertEquals("secret", cfg.getClientSecret()); + assertEquals("https://dev-123456.okta.com", cfg.getSite()); + assertEquals("/oauth2/v1/authorize", cfg.getAuthorizationPath()); + assertEquals("/oauth2/v1/token", cfg.getTokenPath()); + assertEquals("/oauth2/v1/userinfo", cfg.getUserInfoPath()); + assertEquals("/oauth2/v1/revoke", cfg.getRevocationPath()); + assertEquals("/oauth2/v1/introspect", cfg.getIntrospectionPath()); + assertEquals("/oauth2/v1/keys", cfg.getJwkPath()); + assertEquals("/oauth2/v1/logout", cfg.getLogoutPath()); + assertEquals(" ", cfg.getScopeSeparator()); + } + + @Test + public void testOktaCustomAuthorizationServer() { + OAuth2Options cfg = configOf(OktaAuth.create(rule.vertx(), "id", "secret", "dev-123456.okta.com", "default")); + assertEquals("https://dev-123456.okta.com/oauth2/default", cfg.getSite()); + assertEquals("/v1/authorize", cfg.getAuthorizationPath()); + assertEquals("/v1/token", cfg.getTokenPath()); + assertEquals("/v1/userinfo", cfg.getUserInfoPath()); + assertEquals("/v1/revoke", cfg.getRevocationPath()); + assertEquals("/v1/introspect", cfg.getIntrospectionPath()); + assertEquals("/v1/keys", cfg.getJwkPath()); + assertEquals("/v1/logout", cfg.getLogoutPath()); + } + + @Test + public void testDiscord() { + OAuth2Options cfg = configOf(DiscordAuth.create(rule.vertx(), "id", "secret")); + assertEquals("id", cfg.getClientId()); + assertEquals("secret", cfg.getClientSecret()); + assertEquals("https://discord.com/api", cfg.getSite()); + assertEquals("https://discord.com/oauth2/authorize", cfg.getAuthorizationPath()); + assertEquals("/oauth2/token", cfg.getTokenPath()); + assertEquals("/oauth2/token/revoke", cfg.getRevocationPath()); + assertEquals("/users/@me", cfg.getUserInfoPath()); + assertEquals(" ", cfg.getScopeSeparator()); + } + + @Test + public void testTwitch() { + OAuth2Options cfg = configOf(TwitchAuth.create(rule.vertx(), "id", "secret")); + assertEquals("id", cfg.getClientId()); + assertEquals("secret", cfg.getClientSecret()); + assertEquals("https://id.twitch.tv/oauth2", cfg.getSite()); + assertEquals("/authorize", cfg.getAuthorizationPath()); + assertEquals("/token", cfg.getTokenPath()); + assertEquals("/userinfo", cfg.getUserInfoPath()); + assertEquals("/revoke", cfg.getRevocationPath()); + assertEquals("/keys", cfg.getJwkPath()); + assertEquals(" ", cfg.getScopeSeparator()); + // twitch only accepts client credentials in the request body + assertFalse(cfg.isUseBasicAuthorization()); + } + + @Test + public void testSpotify() { + OAuth2Options cfg = configOf(SpotifyAuth.create(rule.vertx(), "id", "secret")); + assertEquals("id", cfg.getClientId()); + assertEquals("secret", cfg.getClientSecret()); + assertEquals("https://accounts.spotify.com", cfg.getSite()); + assertEquals("/authorize", cfg.getAuthorizationPath()); + assertEquals("/api/token", cfg.getTokenPath()); + assertEquals("https://api.spotify.com/v1/me", cfg.getUserInfoPath()); + assertEquals(" ", cfg.getScopeSeparator()); + // spotify requires HTTP basic client authentication on the token endpoint + assertTrue(cfg.isUseBasicAuthorization()); + } +} From 56c4bb5bf96aa2a25a344d23ebc049823f71bb1d Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sun, 16 Aug 2026 19:25:51 -0400 Subject: [PATCH 7/7] Verify Twitch and Okta provider configs against live discovery Add discovery-contract tests to OpenIDCDiscoveryTest, next to the existing live tests for Google, Microsoft, Salesforce, IBM and Apple: the endpoints advertised in the provider's OpenID Connect discovery document must match the endpoints hardcoded by TwitchAuth.create() and OktaAuth.create() (org and custom authorization server variants), so future endpoint drift is caught by CI. Endpoints omitted from the discovery document (Twitch does not advertise its documented revocation endpoint) are not compared. Ory Network is not reachable from non-browser clients and Discord/Spotify publish no discovery document, so those providers stay covered by ProviderConfigTest only. --- .../io/vertx/tests/OpenIDCDiscoveryTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java b/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java index 88f1c458e..b2ed6f6ea 100644 --- a/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java +++ b/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java @@ -140,4 +140,108 @@ public void testApple(TestContext should) { .onFailure(should::fail); } + /** + * Asserts that the endpoints advertised by the provider discovery document match the endpoints hardcoded by the + * provider {@code create()} shortcut. Discovery stores absolute URLs while the shortcuts store the site plus a + * relative path, so relative paths are resolved before comparing. Endpoints omitted from the discovery document + * are not compared. + */ + private static void assertSameEndpoints(TestContext should, OAuth2Options expected, OAuth2Options discovered) { + should.assertEquals(expected.getSite(), discovered.getSite()); + assertSameEndpoint(should, expected, expected.getAuthorizationPath(), discovered.getAuthorizationPath()); + assertSameEndpoint(should, expected, expected.getTokenPath(), discovered.getTokenPath()); + assertSameEndpoint(should, expected, expected.getUserInfoPath(), discovered.getUserInfoPath()); + assertSameEndpoint(should, expected, expected.getRevocationPath(), discovered.getRevocationPath()); + assertSameEndpoint(should, expected, expected.getIntrospectionPath(), discovered.getIntrospectionPath()); + assertSameEndpoint(should, expected, expected.getLogoutPath(), discovered.getLogoutPath()); + assertSameEndpoint(should, expected, expected.getJwkPath(), discovered.getJwkPath()); + } + + private static void assertSameEndpoint(TestContext should, OAuth2Options expected, String expectedPath, String discoveredUrl) { + if (discoveredUrl == null) { + // not advertised by the provider, nothing to compare against + return; + } + should.assertNotNull(expectedPath, "provider advertises " + discoveredUrl + " but the shortcut does not configure it"); + String expectedUrl = expectedPath.startsWith("http") ? expectedPath : expected.getSite() + expectedPath; + should.assertEquals(expectedUrl, discoveredUrl); + } + + @Test + public void testTwitch(TestContext should) { + final Async test = should.async(); + + final OAuth2Options expected = ((OAuth2AuthProviderImpl) TwitchAuth.create(rule.vertx(), "client-id", "client-secret")).getConfig(); + + TwitchAuth.discover(rule.vertx(), new OAuth2Options().setClientId("client-id").setClientSecret("client-secret")) + .onFailure(should::fail) + .onSuccess(oauth2 -> { + OAuth2Options discovered = ((OAuth2AuthProviderImpl) oauth2).getConfig(); + assertSameEndpoints(should, expected, discovered); + // Twitch advertises these endpoints, make sure they were really compared + should.assertNotNull(discovered.getAuthorizationPath()); + should.assertNotNull(discovered.getTokenPath()); + should.assertNotNull(discovered.getUserInfoPath()); + should.assertNotNull(discovered.getJwkPath()); + should.assertEquals("https://id.twitch.tv/oauth2", discovered.getJWTOptions().getIssuer()); + // discover() must keep the Twitch specific client authentication method + should.assertFalse(discovered.isUseBasicAuthorization()); + test.complete(); + }); + } + + @Test + public void testOktaOrgAuthorizationServer(TestContext should) { + final Async test = should.async(); + + // okta.okta.com is Okta's own public org, its discovery documents are reachable without credentials + final OAuth2Options expected = ((OAuth2AuthProviderImpl) OktaAuth.create(rule.vertx(), "client-id", "client-secret", "okta.okta.com")).getConfig(); + + OktaAuth.discover(rule.vertx(), new OAuth2Options() + .setSite("https://okta.okta.com") + .setClientId("client-id") + .setClientSecret("client-secret")) + .onFailure(should::fail) + .onSuccess(oauth2 -> { + OAuth2Options discovered = ((OAuth2AuthProviderImpl) oauth2).getConfig(); + assertSameEndpoints(should, expected, discovered); + // Okta advertises all the endpoints configured by the shortcut, make sure they were really compared + should.assertNotNull(discovered.getAuthorizationPath()); + should.assertNotNull(discovered.getTokenPath()); + should.assertNotNull(discovered.getUserInfoPath()); + should.assertNotNull(discovered.getRevocationPath()); + should.assertNotNull(discovered.getIntrospectionPath()); + should.assertNotNull(discovered.getLogoutPath()); + should.assertNotNull(discovered.getJwkPath()); + should.assertEquals("https://okta.okta.com", discovered.getJWTOptions().getIssuer()); + test.complete(); + }); + } + + @Test + public void testOktaCustomAuthorizationServer(TestContext should) { + final Async test = should.async(); + + final OAuth2Options expected = ((OAuth2AuthProviderImpl) OktaAuth.create(rule.vertx(), "client-id", "client-secret", "okta.okta.com", "default")).getConfig(); + + OktaAuth.discover(rule.vertx(), new OAuth2Options() + .setSite("https://okta.okta.com/oauth2/default") + .setClientId("client-id") + .setClientSecret("client-secret")) + .onFailure(should::fail) + .onSuccess(oauth2 -> { + OAuth2Options discovered = ((OAuth2AuthProviderImpl) oauth2).getConfig(); + assertSameEndpoints(should, expected, discovered); + should.assertNotNull(discovered.getAuthorizationPath()); + should.assertNotNull(discovered.getTokenPath()); + should.assertNotNull(discovered.getUserInfoPath()); + should.assertNotNull(discovered.getRevocationPath()); + should.assertNotNull(discovered.getIntrospectionPath()); + should.assertNotNull(discovered.getLogoutPath()); + should.assertNotNull(discovered.getJwkPath()); + should.assertEquals("https://okta.okta.com/oauth2/default", discovered.getJWTOptions().getIssuer()); + test.complete(); + }); + } + }