diff --git a/docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc b/docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc index 50704b96cdc88..4cd814fd04848 100644 --- a/docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc +++ b/docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc @@ -77,6 +77,7 @@ See the <> section for more details. |Property name |Default |Description |quarkus.oidc.auth-server-url ||OIDC base URL +|quarkus.oidc.discovery-path |.well-known/openid-configuration|Discovery path |quarkus.oidc.discovery-enabled |true|Enable discovery |quarkus.oidc.authorization-path ||Authorization path |quarkus.oidc.token-path ||Token path @@ -88,7 +89,10 @@ See the <> section for more details. |quarkus.oidc.registration-path ||OIDC client registration path |==== -`quarkus.oidc.auth-server-url` is a key base OIDC URL property. By default, Quarkus OIDC adds a `.well-known/openid-configuration` path segment to this URL and discovers the OIDC provider metadata. +`quarkus.oidc.auth-server-url` is a key base OIDC URL property. + +By default, Quarkus OIDC adds a `.well-known/openid-configuration` path segment to this URL and discovers the OIDC provider metadata. +The discovery path can be customized with `quarkus.oidc.discovery-path`, for example, it can be set to `.well-known/oauth-authorization-server`. You can disable metadata discovery with `quarkus.oidc.discovery-enabled=false` when the provider does not support it (most OAuth2 providers do not), when you would like to optimize start up time by skipping a remote discovery call and configure individual OIDC provider endpoint URLs instead. @@ -552,7 +556,8 @@ The common cookie properties impact both the authorization code flow session and |quarkus.oidc.authentication.cookie-path |'/'| The cookie path |quarkus.oidc.authentication.cookie-domain || The cookie domain |quarkus.oidc.authentication.cookie-path-header || The cookie path header -|quarkus.oidc.authentication.cookie-same-site |lax| The cookie SameSite status +|quarkus.oidc.authentication.cookie-same-site |lax| The session cookie SameSite status +|quarkus.oidc.authentication.state-cookie-same-site |lax| The state cookie SameSite status |quarkus.oidc.authentication.cookie-suffix || The cookie suffix |quarkus.oidc.authentication.cookie-force-secure |false| The cookie force secure |==== @@ -562,7 +567,11 @@ You may also want to restrict it when the specific endpoint paths should only be `quarkus.oidc.authentication.cookie-path-header` can be used to dynamically set the required cookie path - this property should be used with care and only when it fits your deployment requirements. -`quarkus.oidc.authentication.cookie-same-site` defines a `Same-Site` attribute as `lax` by default, since setting it to `strict` proved to be breaking some deployments. However, setting it to `strict` is RECOMMENDED when it is known to work in your deployment, for example, when the OIDC provider is hosted in the same domain as the application, etc. +`quarkus.oidc.authentication.cookie-same-site` defines a `Same-Site` attribute for a session cookie as `lax` by default, since setting it to `strict` proved to be breaking some deployments. However, setting it to `strict` is RECOMMENDED when it is known to work in your deployment, for example, when the OIDC provider is hosted in the same domain as the application, etc. + +Similarly, `quarkus.oidc.authentication.state-cookie-same-site` defines a `Same-Site` attribute for a state cookie as `lax` by default, but setting it to `strict` is RECOMMENDED when it is known to work in your deployment. + +Ideally both the session and state cookies have the same `Same-Site` attribute value, however having the same `Same-Site` `strict` value for both cookies also proved to be problematic in some cases. `quarkus.oidc.authentication.cookie-suffix` can be used to customize the state and session cookie names. For example, by setting `%test.quarkus.oidc.authentication.cookie-suffix=test` you can have the session cookie name qualified with the `_test` suffix in the test profile only. diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java index 04bff2d50e592..a4ce31b46f609 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java @@ -238,9 +238,10 @@ private static Uni discoverRegistrationUri(WebClient Map> oidcResponseFilters, String authServerUrl, io.vertx.mutiny.core.Vertx vertx, OidcClientRegistrationConfig oidcConfig) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); + final String discoveryUri = OidcCommonUtils.getDiscoveryUri(authServerUrl, oidcConfig.discoveryPath()); return OidcCommonUtils .discoverMetadata(client, oidcRequestFilters, new OidcRequestContextProperties(), - oidcResponseFilters, authServerUrl, + oidcResponseFilters, discoveryUri, connectionDelayInMillisecs, vertx, oidcConfig.useBlockingDnsLookup()) .onItem().transform(json -> new OidcConfigurationMetadata(json.getString("registration_endpoint"))); diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientHealthCheck.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientHealthCheck.java index 109e01849bda2..632153c6a3c5c 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientHealthCheck.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientHealthCheck.java @@ -80,7 +80,7 @@ private String checkClient(HealthCheckResponseBuilder builder, String clientId, } else if (oidcClientConfig.discoveryEnabled().orElse(true) && oidcClientConfig.authServerUrl().isPresent()) { try { String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcClientConfig); - String discoveryUri = getDiscoveryUri(authServerUriString); + String discoveryUri = getDiscoveryUri(authServerUriString, oidcClientConfig.discoveryPath()); status = checkHealth(oidcClientImpl, discoveryUri).await().indefinitely(); } catch (Exception e) { status = ERROR_STATUS + ": " + e.getMessage(); diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java index 2ba6ac8b2b2b7..124d3e0d5e6f6 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java @@ -232,8 +232,9 @@ private static Uni discoverTokenUris(WebClient client final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); OidcRequestContextProperties contextProps = new OidcRequestContextProperties( Map.of(CLIENT_ID_ATTRIBUTE, oidcConfig.id().orElse(DEFAULT_OIDC_CLIENT_ID))); + final String discoveryUrl = OidcCommonUtils.getDiscoveryUri(authServerUrl, oidcConfig.discoveryPath()); return OidcCommonUtils.discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, - authServerUrl, connectionDelayInMillisecs, vertx, oidcConfig.useBlockingDnsLookup()) + discoveryUrl, connectionDelayInMillisecs, vertx, oidcConfig.useBlockingDnsLookup()) .onItem().transform(json -> new OidcConfigurationMetadata(json.getString("token_endpoint"), json.getString("revocation_endpoint"))); } diff --git a/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java index 8952309039f99..26fe5abf778a0 100644 --- a/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java +++ b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java @@ -15,6 +15,7 @@ final class OidcClientConfigImpl implements OidcClientConfig { enum ConfigMappingMethods { ID, AUTH_SERVER_URL, + DISCOVERY_PATH, DISCOVERY_ENABLED, REGISTRATION_PATH, CONNECTION_DELAY, @@ -433,6 +434,12 @@ public Optional authServerUrl() { return Optional.empty(); } + @Override + public String discoveryPath() { + invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_PATH, true); + return null; + } + @Override public Optional discoveryEnabled() { invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_ENABLED, true); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index d9a5afde7c148..b1b8a55d3bc91 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -13,6 +13,7 @@ public OidcCommonConfig() { protected OidcCommonConfig(io.quarkus.oidc.common.runtime.config.OidcCommonConfig mapping) { this.authServerUrl = mapping.authServerUrl(); + this.discoveryPath = mapping.discoveryPath(); this.discoveryEnabled = mapping.discoveryEnabled(); this.registrationPath = mapping.registrationPath(); this.connectionDelay = mapping.connectionDelay(); @@ -46,6 +47,14 @@ protected OidcCommonConfig(io.quarkus.oidc.common.runtime.config.OidcCommonConfi @Deprecated(since = "3.18", forRemoval = true) public Optional discoveryEnabled = Optional.empty(); + /** + * The relative path of the OIDC discovery endpoint. + * + * @deprecated use {@link #discoveryPath()} method instead + */ + @Deprecated(forRemoval = true) + public String discoveryPath; + /** * The relative path or absolute URL of the OIDC dynamic client registration endpoint. * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. @@ -138,6 +147,11 @@ public Optional discoveryEnabled() { return discoveryEnabled; } + @Override + public String discoveryPath() { + return discoveryPath; + } + @Override public Optional registrationPath() { return registrationPath; diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index 16be93f864394..6e2b2dd103042 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -576,9 +576,8 @@ private static boolean isValidOidcClientRedirectRequest(OidcClientRedirectExcept public static Uni discoverMetadata(WebClient client, Map> requestFilters, OidcRequestContextProperties contextProperties, Map> responseFilters, - String authServerUrl, + String discoveryUrl, long connectionDelayInMillisecs, Vertx vertx, boolean blockingDnsLookup) { - final String discoveryUrl = getDiscoveryUri(authServerUrl); final OidcRequestContextProperties requestProps = requestFilters.isEmpty() ? null : getDiscoveryRequestProps(contextProperties, discoveryUrl); @@ -695,8 +694,8 @@ public static Buffer getResponseBuffer(OidcRequestContextProperties requestProps return updatedResponseBody == null ? buffer : updatedResponseBody; } - public static String getDiscoveryUri(String authServerUrl) { - return authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; + public static String getDiscoveryUri(String authServerUrl, String discoveryPath) { + return authServerUrl + prependSlash(discoveryPath != null ? discoveryPath : OidcConstants.WELL_KNOWN_CONFIGURATION); } private static byte[] getFileContent(Path path) throws IOException { diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java index d815e2796054c..c149b46d64e6b 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java @@ -13,23 +13,33 @@ @ConfigGroup public interface OidcCommonConfig { /** - * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. - * Do not set this property if you use 'quarkus-oidc' and the public key verification ({@link #publicKey}) - * or certificate chain verification only ({@link #certificateChain}) is required. - * The OIDC discovery endpoint is called by default by appending a `.well-known/openid-configuration` path to this URL. + * The base URL of an OpenID Connect (OIDC) server, for example, `https://host:port/auth`. + * Do not set this property if you use the public key verification ({@link #publicKey}) + * or certificate chain verification only ({@link #certificateChain}). + *

+ * By default, when an OIDC configuration metadata discovery is enabled with the {@link #discoveryEnabled()} property, + * it is retrieved from a well known provider endpoint with its URL calculated by appending a value + * of the {@link #discoveryPath()} path such as `.well-known/openid-configuration` to this URL. + *

* For Keycloak, use `https://host:port/realms/{realm}`, replacing `{realm}` with the Keycloak realm name. */ Optional authServerUrl(); /** - * Discovery of the OIDC endpoints. + * Enable discovery of the OIDC endpoints. * If not enabled, you must configure the OIDC endpoint URLs individually. */ @ConfigDocDefault("true") Optional discoveryEnabled(); /** - * The relative path or absolute URL of the OIDC dynamic client registration endpoint. + * The relative path of the OIDC discovery endpoint. + */ + @WithDefault(".well-known/openid-configuration") + String discoveryPath(); + + /** + * The relative path of the OIDC dynamic client registration endpoint. * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. */ Optional registrationPath(); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfigBuilder.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfigBuilder.java index c598c773cf179..9d36eaa204b34 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfigBuilder.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfigBuilder.java @@ -26,6 +26,7 @@ private record ProxyImpl(Optional host, int port, Optional usern protected static class OidcCommonConfigImpl implements OidcCommonConfig { private final Optional authServerUrl; + private final String discoveryPath; private final Optional discoveryEnabled; private final Optional registrationPath; private final Optional connectionDelay; @@ -39,6 +40,7 @@ protected static class OidcCommonConfigImpl implements OidcCommonConfig { protected OidcCommonConfigImpl(OidcCommonConfigBuilder builder) { this.authServerUrl = builder.authServerUrl; + this.discoveryPath = builder.discoveryPath; this.discoveryEnabled = builder.discoveryEnabled; this.registrationPath = builder.registrationPath; this.connectionDelay = builder.connectionDelay; @@ -57,6 +59,11 @@ public Optional authServerUrl() { return authServerUrl; } + @Override + public String discoveryPath() { + return discoveryPath; + } + @Override public Optional discoveryEnabled() { return discoveryEnabled; @@ -109,6 +116,7 @@ public Tls tls() { } private Optional authServerUrl; + private String discoveryPath; private Optional discoveryEnabled; private Optional registrationPath; private Optional connectionDelay; @@ -126,6 +134,7 @@ public Tls tls() { protected OidcCommonConfigBuilder(OidcCommonConfig oidcCommonConfig) { this.authServerUrl = oidcCommonConfig.authServerUrl(); + this.discoveryPath = oidcCommonConfig.discoveryPath(); this.discoveryEnabled = oidcCommonConfig.discoveryEnabled(); this.registrationPath = oidcCommonConfig.registrationPath(); this.connectionDelay = oidcCommonConfig.connectionDelay(); @@ -153,6 +162,15 @@ public T authServerUrl(String authServerUrl) { return getBuilder(); } + /** + * @param discoveryPath {@link OidcCommonConfig#discoveryPath()} + * @return T builder + */ + public T discoveryPath(String discoveryPath) { + this.discoveryPath = discoveryPath; + return getBuilder(); + } + /** * @param discoveryEnabled {@link OidcCommonConfig#discoveryEnabled()} * @return T builder diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index ebfddf33f0098..51303a3a364d2 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -1431,6 +1431,13 @@ public io.quarkus.oidc.runtime.OidcTenantConfig.Authentication.CookieSameSite co : io.quarkus.oidc.runtime.OidcTenantConfig.Authentication.CookieSameSite.valueOf(cookieSameSite.toString()); } + @Override + public io.quarkus.oidc.runtime.OidcTenantConfig.Authentication.CookieSameSite stateCookieSameSite() { + return stateCookieSameSite == null ? null + : io.quarkus.oidc.runtime.OidcTenantConfig.Authentication.CookieSameSite + .valueOf(stateCookieSameSite.toString()); + } + @Override public boolean allowMultipleCodeFlows() { return allowMultipleCodeFlows; @@ -1713,6 +1720,11 @@ public enum ResponseMode { */ public CookieSameSite cookieSameSite = CookieSameSite.LAX; + /** + * SameSite attribute for the state cookie. + */ + public CookieSameSite stateCookieSameSite = CookieSameSite.LAX; + /** * If a state cookie is present, a `state` query parameter must also be present and both the state * cookie name suffix and state cookie value must match the value of the `state` query parameter when @@ -2137,6 +2149,7 @@ private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Aut cookiePathHeader = mapping.cookiePathHeader(); cookieDomain = mapping.cookieDomain(); cookieSameSite = CookieSameSite.valueOf(mapping.cookieSameSite().toString()); + stateCookieSameSite = CookieSameSite.valueOf(mapping.stateCookieSameSite().toString()); allowMultipleCodeFlows = mapping.allowMultipleCodeFlows(); failOnMissingStateParam = mapping.failOnMissingStateParam(); failOnUnresolvedKid = mapping.failOnUnresolvedKid(); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index db6ced1ce39cd..db0bc551d71a9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -62,7 +62,9 @@ import io.smallrye.mutiny.Uni; import io.vertx.core.MultiMap; import io.vertx.core.http.Cookie; +import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.impl.ServerCookie; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; @@ -1299,9 +1301,12 @@ private String generateCodeFlowState(RoutingContext context, TenantConfigContext cookieValue += (COOKIE_DELIM + encodeExtraStateValue(extraStateValue, configContext)); } String stateCookieNameSuffix = configContext.oidcConfig().authentication().allowMultipleCodeFlows() ? "_" + uuid : ""; - OidcUtils.createCookie(context, configContext.oidcConfig(), + ServerCookie stateCookie = OidcUtils.createCookie(context, configContext.oidcConfig(), getStateCookieName(configContext.oidcConfig()) + stateCookieNameSuffix, cookieValue, configContext.oidcConfig().authentication().stateCookieAge().toSeconds()); + stateCookie + .setSameSite(CookieSameSite.valueOf(configContext.oidcConfig().authentication().stateCookieSameSite().name())); + return uuid; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java index a98c1f604715a..58576f6f410e3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java @@ -856,6 +856,12 @@ String directive() { @WithDefault("lax") CookieSameSite cookieSameSite(); + /** + * SameSite attribute for the state cookie. + */ + @WithDefault("lax") + CookieSameSite stateCookieSameSite(); + /** * If a state cookie is present, a `state` query parameter must also be present and both the state * cookie name suffix and state cookie value must match the value of the `state` query parameter when diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 636c0987c5b21..6c374ef9d1af2 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -502,6 +502,18 @@ static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenant if (auth.cookieDomain().isPresent()) { cookie.setDomain(auth.cookieDomain().get()); } + + CookieSameSite sameSite = null; + if (cookie.getName().startsWith(SESSION_COOKIE_NAME)) { + sameSite = CookieSameSite.valueOf(oidcConfig.authentication().cookieSameSite().name()); + } else if (cookie.getName().startsWith(STATE_COOKIE_NAME)) { + sameSite = CookieSameSite.valueOf(oidcConfig.authentication().stateCookieSameSite().name()); + } + if (CookieSameSite.NONE == sameSite) { + // browsers may want it if the same site is none + cookie.setSameSite(sameSite); + cookie.setSecure(oidcConfig.authentication().cookieForceSecure() || context.request().isSSL()); + } } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java index 4d33401abc194..1f49d637b0aff 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java @@ -467,10 +467,11 @@ private Uni createOidcClientUni(OidcTenantConfig oidcCon metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString)); } else { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); + final String discoveryUri = OidcCommonUtils.getDiscoveryUri(authServerUriString, oidcConfig.discoveryPath()); OidcRequestContextProperties contextProps = new OidcRequestContextProperties( Map.of(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.tenantId().orElse(OidcUtils.DEFAULT_TENANT_ID))); metadataUni = OidcCommonUtils - .discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, authServerUriString, + .discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, discoveryUri, connectionDelayInMillisecs, mutinyVertx, oidcConfig.useBlockingDnsLookup()) @@ -479,7 +480,7 @@ private Uni createOidcClientUni(OidcTenantConfig oidcCon @Override public OidcConfigurationMetadata apply(JsonObject json) { return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString), - OidcCommonUtils.getDiscoveryUri(authServerUriString)); + discoveryUri); } }); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/builders/AuthenticationConfigBuilder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/builders/AuthenticationConfigBuilder.java index c71dd07eef8da..b4ce0478e039a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/builders/AuthenticationConfigBuilder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/builders/AuthenticationConfigBuilder.java @@ -30,7 +30,8 @@ private record AuthenticationImpl(Optional responseMode, Optional< Optional> scopes, Optional scopeSeparator, boolean nonceRequired, Optional addOpenidScope, Map extraParams, Optional> forwardParams, boolean cookieForceSecure, Optional cookieSuffix, String cookiePath, Optional cookiePathHeader, - Optional cookieDomain, CookieSameSite cookieSameSite, Optional> cacheControl, + Optional cookieDomain, CookieSameSite cookieSameSite, CookieSameSite stateCookieSameSite, + Optional> cacheControl, boolean allowMultipleCodeFlows, boolean failOnMissingStateParam, boolean failOnUnresolvedKid, Optional userInfoRequired, Optional sessionAgeExtension, Duration stateCookieAge, boolean javaScriptAutoRedirect, Optional idTokenRequired, @@ -60,6 +61,7 @@ private record AuthenticationImpl(Optional responseMode, Optional< private Optional cookiePathHeader; private Optional cookieDomain; private CookieSameSite cookieSameSite; + private CookieSameSite stateCookieSameSite; private Set cacheControl = new HashSet<>(); private boolean allowMultipleCodeFlows; private boolean failOnMissingStateParam; @@ -107,6 +109,7 @@ public AuthenticationConfigBuilder(OidcTenantConfigBuilder builder) { this.cookiePathHeader = authentication.cookiePathHeader(); this.cookieDomain = authentication.cookieDomain(); this.cookieSameSite = authentication.cookieSameSite(); + this.stateCookieSameSite = authentication.stateCookieSameSite(); if (authentication.cacheControl().isPresent()) { this.cacheControl.addAll(authentication.cacheControl().get()); } @@ -395,6 +398,15 @@ public AuthenticationConfigBuilder cookieSameSite(CookieSameSite cookieSameSite) return this; } + /** + * @param stateCookieSameSite {@link Authentication#stateCookieSameSite()} + * @return this builder + */ + public AuthenticationConfigBuilder stateCookieSameSite(CookieSameSite cookieSameSite) { + this.stateCookieSameSite = Objects.requireNonNull(stateCookieSameSite); + return this; + } + /** * Sets {@link Authentication#allowMultipleCodeFlows()} to true. * @@ -657,7 +669,8 @@ public Authentication build() { return new AuthenticationImpl(responseMode, redirectPath, restorePathAfterRedirect, removeRedirectParameters, errorPath, sessionExpiredPath, verifyAccessToken, forceRedirectHttpsScheme, optionalScopes, scopeSeparator, nonceRequired, addOpenidScope, Map.copyOf(extraParams), optionalForwardParams, cookieForceSecure, cookieSuffix, cookiePath, - cookiePathHeader, cookieDomain, cookieSameSite, optionalCacheControl, allowMultipleCodeFlows, + cookiePathHeader, cookieDomain, cookieSameSite, stateCookieSameSite, optionalCacheControl, + allowMultipleCodeFlows, failOnMissingStateParam, failOnUnresolvedKid, userInfoRequired, sessionAgeExtension, stateCookieAge, javaScriptAutoRedirect, idTokenRequired, diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java index ab53487db473f..2c017815c912d 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java @@ -26,6 +26,7 @@ final class OidcTenantConfigImpl implements OidcTenantConfig { enum ConfigMappingMethods { AUTH_SERVER_URL, + DISCOVERY_PATH, DISCOVERY_ENABLED, REGISTRATION_PATH, CONNECTION_DELAY, @@ -143,6 +144,7 @@ enum ConfigMappingMethods { AUTHENTICATION_COOKIE_PATH_HEADER, AUTHENTICATION_COOKIE_DOMAIN, AUTHENTICATION_COOKIE_SAME_SITE, + AUTHENTICATION_STATE_COOKIE_SAME_SITE, AUTHENTICATION_CACHE_CONTROL, AUTHENTICATION_ALLOW_MULTIPLE_CODE_FLOWS, AUTHENTICATION_FAIL_ON_MISSING_STATE_PARAM, @@ -776,6 +778,12 @@ public CookieSameSite cookieSameSite() { return CookieSameSite.LAX; } + @Override + public CookieSameSite stateCookieSameSite() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_STATE_COOKIE_SAME_SITE, true); + return CookieSameSite.LAX; + } + @Override public Optional> cacheControl() { invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_CACHE_CONTROL, true); @@ -1237,6 +1245,12 @@ public Optional authServerUrl() { return Optional.empty(); } + @Override + public String discoveryPath() { + invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_PATH, true); + return null; + } + @Override public Optional discoveryEnabled() { invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_ENABLED, true); diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index 0d07a702748ea..c90894e8d2e30 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -137,6 +137,7 @@ quarkus.oidc.tenant-https.authentication.pkce-required=true quarkus.oidc.tenant-https.authentication.nonce-required=true quarkus.oidc.tenant-https.authentication.pkce-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU quarkus.oidc.tenant-https.authentication.cookie-same-site=strict +quarkus.oidc.tenant-https.authentication.state-cookie-same-site=none quarkus.oidc.tenant-https.authentication.fail-on-missing-state-param=true quarkus.oidc.tenant-nonce.auth-server-url=${quarkus.oidc.auth-server-url} diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 77d19a5b1660d..7c75b63b81331 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -75,7 +75,7 @@ public void testCodeFlowNoConsent() throws IOException { Cookie stateCookie = getStateCookie(webClient, null); assertNotNull(stateCookie); assertEquals(stateCookie.getName(), "q_auth_Default_test_" + getStateCookieStateParam(stateCookie)); - assertNull(stateCookie.getSameSite()); + assertEquals("lax", stateCookie.getSameSite()); webClient.getCookieManager().clearCookies(); @@ -294,7 +294,7 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception { String endpointLocation = webResponse.getResponseHeaderValue("location"); Cookie stateCookie = getStateCookie(webClient, "tenant-https_test"); - assertNull(stateCookie.getSameSite()); + assertEquals("none", stateCookie.getSameSite()); verifyCodeVerifierAndNonce(stateCookie, keycloakUrl); assertTrue(endpointLocation.startsWith("https")); @@ -369,7 +369,7 @@ public void testStateCookieIsPresentButStateParamNot() throws Exception { // State cookie is present Cookie stateCookie = getStateCookie(webClient, "tenant-https_test"); - assertNull(stateCookie.getSameSite()); + assertEquals("none", stateCookie.getSameSite()); verifyCodeVerifierAndNonce(stateCookie, keycloakUrl); // Make a call without an extra state query param, status is 401 diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryJwksRequestCustomizer.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryJwksRequestCustomizer.java index ac10fcb482248..c5e436a7b8124 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryJwksRequestCustomizer.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryJwksRequestCustomizer.java @@ -18,7 +18,10 @@ public class OidcDiscoveryJwksRequestCustomizer implements OidcRequestFilter { @Override public void filter(OidcRequestContext rc) { - if (!rc.request().uri().endsWith(".well-known/openid-configuration") + String discoveryPath = !rc.request().uri().contains("quarkus2") ? ".well-known/openid-configuration" + : ".well-known/oauth-authorization-server"; + + if (!rc.request().uri().endsWith(discoveryPath) && !isJwksRequest(rc.request())) { throw new OIDCException("Filter is applied to the wrong endpoint: " + rc.request().uri()); } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 05b56712042a9..4b4e3333c1f00 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -2,6 +2,7 @@ quarkus.oidc.health.enabled=true # Configuration file quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/quarkus2/ +quarkus.oidc.discovery-path=.well-known/oauth-authorization-server quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret quarkus.oidc.authentication.scopes=profile,email,phone @@ -322,18 +323,21 @@ quarkus.grpc.clients.saluter.port=8081 quarkus.grpc.server.use-separate-server=false %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.auth-server-url=http://localhost:8185/auth/realms/quarkus2 +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.discovery-path=.well-known/oauth-authorization-server %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.client-id=quarkus-app %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.token.required-claims.client-name=a %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.credentials.secret=secret %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.token.audience=https://correct-issuer.edu %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-a.token.allow-jwt-introspection=false %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.auth-server-url=http://localhost:8185/auth/realms/quarkus2 +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.discovery-path=/.well-known/oauth-authorization-server %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.client-id=quarkus-app %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.token.required-claims.client-name=b %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.credentials.secret=secret %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.token.audience=https://correct-issuer.edu %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-b.token.allow-jwt-introspection=false %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-abc.auth-server-url=http://localhost:8185/auth/realms/quarkus2 +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-abc.discovery-path=.well-known/oauth-authorization-server %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-abc.client-id=quarkus-app %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-abc.token.required-claims.client-name=a,b,c %issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver-abc.credentials.secret=secret diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java index e6e03a3578f46..46caf42f661ba 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java @@ -53,6 +53,7 @@ public Map getConfigOverrides() { return Map.ofEntries(Map.entry("quarkus.http.auth.proactive", "false"), Map.entry("quarkus.oidc.hr.authentication.user-info-required", "false"), Map.entry("quarkus.oidc.hr.auth-server-url", "http://localhost:8180/auth/realms/quarkus2/"), + Map.entry("quarkus.oidc.hr.discovery-path", "/.well-known/oauth-authorization-server"), Map.entry("quarkus.oidc.hr.client-id", "quarkus-app"), Map.entry("quarkus.oidc.hr.credentials.secret", "secret"), Map.entry("quarkus.oidc.hr.tenant-paths", "/api/tenant-echo/http-security-policy-applies-all-same"), diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java index 373260cce2d8e..218a41803a4bd 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java @@ -40,21 +40,21 @@ public void start() { server.start(); server.stubFor( - head(urlEqualTo("/auth/realms/quarkus2/.well-known/openid-configuration")) + head(urlEqualTo("/auth/realms/quarkus2/.well-known/oauth-authorization-server")) .willReturn(aResponse().withStatus(200))); server.stubFor( - get(urlEqualTo("/auth/realms/quarkus2/.well-known/openid-configuration")) + get(urlEqualTo("/auth/realms/quarkus2/.well-known/oauth-authorization-server")) .withHeader("Filter", equalTo("OK")) .withHeader("Cookie", absent()) .willReturn(aResponse() .withStatus(302) .withHeader("Location", "http://localhost:" + port - + "/auth/realms/quarkus2/.well-known/openid-configuration") + + "/auth/realms/quarkus2/.well-known/oauth-authorization-server") .withHeader("Set-Cookie", "redirect=true; Path=/; Domain=some.domain.com"))); server.stubFor( - get(urlEqualTo("/auth/realms/quarkus2/.well-known/openid-configuration")) + get(urlEqualTo("/auth/realms/quarkus2/.well-known/oauth-authorization-server")) .withHeader("Cookie", equalTo("redirect=true")) .withHeader("Filter", equalTo("OK")) .withHeader("tenant-id", not(absent()))