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 58c4488115f9a..0016d5b585422 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 @@ -117,7 +117,7 @@ public static Uni createOidcClientRegistrationUni(OidcCl } WebClientOptions options = new WebClientOptions(); - + options.setFollowRedirects(oidcConfig.followRedirects); OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsSupport.forConfig(oidcConfig.tls)); final io.vertx.mutiny.core.Vertx vertx = new io.vertx.mutiny.core.Vertx(vertxSupplier.get()); 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 3955d89010960..7d3f85cd23065 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 @@ -130,7 +130,7 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig } WebClientOptions options = new WebClientOptions(); - + options.setFollowRedirects(oidcConfig.followRedirects); OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsSupport.forConfig(oidcConfig.tls)); var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx.get()); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientRedirectException.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientRedirectException.java new file mode 100644 index 0000000000000..da892da887751 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientRedirectException.java @@ -0,0 +1,37 @@ +package io.quarkus.oidc.common.runtime; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("serial") +public class OidcClientRedirectException extends RuntimeException { + + private final String location; + private final List cookies; + + public OidcClientRedirectException(String location, List setCookies) { + this.location = location; + this.cookies = getCookies(setCookies); + } + + private static List getCookies(List setCookies) { + if (setCookies != null && !setCookies.isEmpty()) { + List cookies = new ArrayList<>(); + for (String setCookie : setCookies) { + int index = setCookie.indexOf(";"); + cookies.add(setCookie.substring(0, index)); + } + return cookies; + } + return List.of(); + } + + public String getLocation() { + return location; + } + + public List getCookies() { + return cookies; + } + +} 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 673ea7a8ffee4..9fb2108b6af4e 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 @@ -71,6 +71,14 @@ public class OidcCommonConfig { @ConfigItem public OptionalInt maxPoolSize = OptionalInt.empty(); + /** + * Follow redirects automatically when WebClient gets HTTP 302. + * When this property is disabled only a single redirect to exactly the same original URI + * is allowed but only if one or more cookies were set during the redirect request. + */ + @ConfigItem(defaultValue = "true") + public boolean followRedirects = true; + /** * Options to configure the proxy the OIDC adapter uses to talk with the OIDC server. */ 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 6766bee4749bf..469e9c57cff54 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 @@ -62,6 +62,7 @@ import io.smallrye.jwt.util.ResourceUtils; import io.smallrye.mutiny.Uni; import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.json.JsonObject; import io.vertx.core.net.KeyStoreOptions; import io.vertx.core.net.ProxyOptions; @@ -74,6 +75,8 @@ public class OidcCommonUtils { public static final Duration CONNECTION_BACKOFF_DURATION = Duration.ofSeconds(2); + public static final String LOCATION_RESPONSE_HEADER = String.valueOf(HttpHeaders.LOCATION); + public static final String COOKIE_REQUEST_HEADER = String.valueOf(HttpHeaders.COOKIE); static final byte AMP = '&'; static final byte EQ = '='; @@ -501,15 +504,65 @@ public static Predicate oidcEndpointNotAvailable() { || (t instanceof OidcEndpointAccessException && ((OidcEndpointAccessException) t).getErrorStatus() == 404)); } + public static Predicate validOidcClientRedirect(String originalUri) { + return t -> (t instanceof OidcClientRedirectException + && isValidOidcClientRedirectRequest((OidcClientRedirectException) t, originalUri)); + } + + private static boolean isValidOidcClientRedirectRequest(OidcClientRedirectException ex, + String originalUrl) { + if (!originalUrl.equals(ex.getLocation())) { + LOG.warnf("Redirect is only allowed to %s but redirect to %s is requested", + originalUrl, ex.getLocation()); + return false; + } + if (ex.getCookies().isEmpty()) { + LOG.warnf("Redirect is requested to %s but no cookies are set", originalUrl); + return false; + } + LOG.debugf("Single redirect to %s with cookies is approved", originalUrl); + return true; + } + public static Uni discoverMetadata(WebClient client, Map> requestFilters, OidcRequestContextProperties contextProperties, Map> responseFilters, String authServerUrl, long connectionDelayInMillisecs, Vertx vertx, boolean blockingDnsLookup) { final String discoveryUrl = getDiscoveryUri(authServerUrl); - HttpRequest request = client.getAbs(discoveryUrl); final OidcRequestContextProperties requestProps = requestFilters.isEmpty() ? null : getDiscoveryRequestProps(contextProperties, discoveryUrl); + + return doDiscoverMetadata(client, requestFilters, contextProperties, responseFilters, discoveryUrl, + connectionDelayInMillisecs, vertx, blockingDnsLookup, List.of()) + .onFailure(validOidcClientRedirect(discoveryUrl)) + .recoverWithUni( + new Function>() { + @Override + public Uni apply(Throwable t) { + OidcClientRedirectException ex = (OidcClientRedirectException) t; + return doDiscoverMetadata(client, requestFilters, requestProps, responseFilters, + discoveryUrl, connectionDelayInMillisecs, vertx, blockingDnsLookup, ex.getCookies()); + } + }) + .onFailure().transform(t -> { + LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t); + // don't wrap it to avoid information leak + return new RuntimeException("OIDC Server is not available"); + }); + + } + + public static Uni doDiscoverMetadata(WebClient client, + Map> requestFilters, + OidcRequestContextProperties requestProps, Map> responseFilters, + String discoveryUrl, + long connectionDelayInMillisecs, Vertx vertx, boolean blockingDnsLookup, + List cookies) { + HttpRequest request = client.getAbs(discoveryUrl); + if (!cookies.isEmpty()) { + request.putHeader(COOKIE_REQUEST_HEADER, cookies); + } if (!requestFilters.isEmpty()) { OidcRequestContext context = new OidcRequestContext(request, null, requestProps); for (OidcRequestFilter filter : getMatchingOidcRequestFilters(requestFilters, OidcEndpoint.Type.DISCOVERY)) { @@ -523,8 +576,10 @@ public static Uni discoverMetadata(WebClient client, if (resp.statusCode() == 200) { return buffer.toJsonObject(); + } else if (resp.statusCode() == 302) { + throw createOidcClientRedirectException(resp); } else { - String errorMessage = buffer.toString(); + String errorMessage = buffer != null ? buffer.toString() : null; if (errorMessage != null && !errorMessage.isEmpty()) { LOG.warnf("Discovery request %s has failed, status code: %d, error message: %s", discoveryUrl, resp.statusCode(), errorMessage); @@ -536,12 +591,12 @@ public static Uni discoverMetadata(WebClient client, }).onFailure(oidcEndpointNotAvailable()) .retry() .withBackOff(CONNECTION_BACKOFF_DURATION, CONNECTION_BACKOFF_DURATION) - .expireIn(connectionDelayInMillisecs) - .onFailure().transform(t -> { - LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t); - // don't wrap it to avoid information leak - return new RuntimeException("OIDC Server is not available"); - }); + .expireIn(connectionDelayInMillisecs); + } + + public static OidcClientRedirectException createOidcClientRedirectException(HttpResponse resp) { + LOG.debug("OIDC client redirect is requested"); + return new OidcClientRedirectException(resp.getHeader(LOCATION_RESPONSE_HEADER), resp.cookies()); } private static OidcRequestContextProperties getDiscoveryRequestProps( diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index 41e2e211b3b40..c818e0d28549e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.jboss.logging.Logger; @@ -20,6 +21,7 @@ import io.quarkus.oidc.common.OidcRequestFilter.OidcRequestContext; import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret.Method; +import io.quarkus.oidc.common.runtime.OidcClientRedirectException; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Uni; @@ -87,34 +89,69 @@ public OidcConfigurationMetadata getMetadata() { } public Uni getJsonWebKeySet(OidcRequestContextProperties contextProperties) { - OidcRequestContextProperties requestProps = getRequestProps(contextProperties); + final OidcRequestContextProperties requestProps = getRequestProps(contextProperties); + return doGetJsonWebKeySet(requestProps, List.of()) + .onFailure(OidcCommonUtils.validOidcClientRedirect(metadata.getJsonWebKeySetUri())) + .recoverWithUni( + new Function>() { + @Override + public Uni apply(Throwable t) { + OidcClientRedirectException ex = (OidcClientRedirectException) t; + return doGetJsonWebKeySet(requestProps, ex.getCookies()); + } + }); + } + + private Uni doGetJsonWebKeySet(OidcRequestContextProperties requestProps, List cookies) { + LOG.debugf("Get verification JWT Key Set at %s", metadata.getJsonWebKeySetUri()); + HttpRequest request = client.getAbs(metadata.getJsonWebKeySetUri()); + if (!cookies.isEmpty()) { + request.putHeader(OidcCommonUtils.COOKIE_REQUEST_HEADER, cookies); + } return OidcCommonUtils .sendRequest(vertx, - filterHttpRequest(requestProps, OidcEndpoint.Type.JWKS, client.getAbs(metadata.getJsonWebKeySetUri()), - null, - contextProperties), + filterHttpRequest(requestProps, OidcEndpoint.Type.JWKS, request, null), oidcConfig.useBlockingDnsLookup) .onItem() .transform(resp -> getJsonWebKeySet(requestProps, resp)); } - public Uni getUserInfo(String token) { + public Uni getUserInfo(final String token) { + + final OidcRequestContextProperties requestProps = getRequestProps(null, null); + + return doGetUserInfo(requestProps, token, List.of()) + .onFailure(OidcCommonUtils.validOidcClientRedirect(metadata.getUserInfoUri())) + .recoverWithUni( + new Function>() { + @Override + public Uni apply(Throwable t) { + OidcClientRedirectException ex = (OidcClientRedirectException) t; + return doGetUserInfo(requestProps, token, ex.getCookies()); + } + }); + } + + private Uni doGetUserInfo(OidcRequestContextProperties requestProps, String token, List cookies) { LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token); - OidcRequestContextProperties requestProps = getRequestProps(null, null); + + HttpRequest request = client.getAbs(metadata.getUserInfoUri()); + if (!cookies.isEmpty()) { + request.putHeader(OidcCommonUtils.COOKIE_REQUEST_HEADER, cookies); + } return OidcCommonUtils .sendRequest(vertx, - filterHttpRequest(requestProps, OidcEndpoint.Type.USERINFO, client.getAbs(metadata.getUserInfoUri()), - null, null) + filterHttpRequest(requestProps, OidcEndpoint.Type.USERINFO, request, null) .putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token), oidcConfig.useBlockingDnsLookup) .onItem().transform(resp -> getUserInfo(requestProps, resp)); } - public Uni introspectToken(String token) { - MultiMap introspectionParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); + public Uni introspectToken(final String token) { + final MultiMap introspectionParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN, token); introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN_TYPE_HINT, OidcConstants.ACCESS_TOKEN_VALUE); - OidcRequestContextProperties requestProps = getRequestProps(null, null); + final OidcRequestContextProperties requestProps = getRequestProps(null, null); return getHttpResponse(requestProps, metadata.getIntrospectionUri(), introspectionParams, true) .transform(resp -> getTokenIntrospection(requestProps, resp)); } @@ -128,7 +165,7 @@ public OidcTenantConfig getOidcConfig() { } public Uni getAuthorizationCodeTokens(String code, String redirectUri, String codeVerifier) { - MultiMap codeGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); + final MultiMap codeGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); codeGrantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.AUTHORIZATION_CODE); codeGrantParams.add(OidcConstants.CODE_FLOW_CODE, code); codeGrantParams.add(OidcConstants.CODE_FLOW_REDIRECT_URI, redirectUri); @@ -138,16 +175,16 @@ public Uni getAuthorizationCodeTokens(String code, Stri if (oidcConfig.codeGrant.extraParams != null) { codeGrantParams.addAll(oidcConfig.codeGrant.extraParams); } - OidcRequestContextProperties requestProps = getRequestProps(OidcConstants.AUTHORIZATION_CODE); + final OidcRequestContextProperties requestProps = getRequestProps(OidcConstants.AUTHORIZATION_CODE); return getHttpResponse(requestProps, metadata.getTokenUri(), codeGrantParams, false) .transform(resp -> getAuthorizationCodeTokens(requestProps, resp)); } public Uni refreshAuthorizationCodeTokens(String refreshToken) { - MultiMap refreshGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); + final MultiMap refreshGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); refreshGrantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.REFRESH_TOKEN_GRANT); refreshGrantParams.add(OidcConstants.REFRESH_TOKEN_VALUE, refreshToken); - OidcRequestContextProperties requestProps = getRequestProps(OidcConstants.REFRESH_TOKEN_GRANT); + final OidcRequestContextProperties requestProps = getRequestProps(OidcConstants.REFRESH_TOKEN_GRANT); return getHttpResponse(requestProps, metadata.getTokenUri(), refreshGrantParams, false) .transform(resp -> getAuthorizationCodeTokens(requestProps, resp)); } @@ -205,7 +242,7 @@ private UniOnItem> getHttpResponse(OidcRequestContextProper // Retry up to three times with a one-second delay between the retries if the connection is closed. OidcEndpoint.Type endpoint = introspect ? OidcEndpoint.Type.INTROSPECTION : OidcEndpoint.Type.TOKEN; - Uni> response = filterHttpRequest(requestProps, endpoint, request, buffer, null).sendBuffer(buffer) + Uni> response = filterHttpRequest(requestProps, endpoint, request, buffer).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause()); @@ -245,6 +282,8 @@ private JsonObject getJsonObject(OidcRequestContextProperties requestProps, Stri if (resp.statusCode() == 200) { LOG.debugf("Request succeeded: %s", resp.bodyAsJsonObject()); return buffer.toJsonObject(); + } else if (resp.statusCode() == 302) { + throw OidcCommonUtils.createOidcClientRedirectException(resp); } else { throw responseException(requestUri, resp, buffer); } @@ -257,6 +296,8 @@ private String getString(final OidcRequestContextProperties requestProps, String if (resp.statusCode() == 200) { LOG.debugf("Request succeeded: %s", resp.bodyAsString()); return buffer.toString(); + } else if (resp.statusCode() == 302) { + throw OidcCommonUtils.createOidcClientRedirectException(resp); } else { throw responseException(requestUri, resp, buffer); } @@ -284,8 +325,7 @@ public Key getClientJwtKey() { } private HttpRequest filterHttpRequest(OidcRequestContextProperties requestProps, OidcEndpoint.Type endpointType, - HttpRequest request, Buffer body, - OidcRequestContextProperties contextProperties) { + HttpRequest request, Buffer body) { if (!requestFilters.isEmpty()) { OidcRequestContext context = new OidcRequestContext(request, body, requestProps); for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(requestFilters, endpointType)) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index d06efc38e43af..ff8b8b326c71c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -533,7 +533,7 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig); WebClientOptions options = new WebClientOptions(); - + options.setFollowRedirects(oidcConfig.followRedirects); OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsSupport.forConfig(oidcConfig.tls)); var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx); WebClient client = WebClient.create(mutinyVertx, options); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop.java new file mode 100644 index 0000000000000..8c5b556223ab0 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.oidc.Tenant; +import io.quarkus.security.Authenticated; + +@Tenant("redirect-loop") +@Path("/api-redirect-loop") +@Authenticated +public class TenantRedirectLoop { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getTenant() { + return "redirect-loop"; + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop2.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop2.java new file mode 100644 index 0000000000000..eab473760bc47 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop2.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.oidc.Tenant; +import io.quarkus.security.Authenticated; + +@Tenant("redirect-loop2") +@Path("/api-redirect-loop2") +@Authenticated +public class TenantRedirectLoop2 { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getTenant() { + return "redirect-loop2"; + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop3.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop3.java new file mode 100644 index 0000000000000..3abf43c761c59 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantRedirectLoop3.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.oidc.Tenant; +import io.quarkus.security.Authenticated; + +@Tenant("redirect-loop3") +@Path("/api-redirect-loop3") +@Authenticated +public class TenantRedirectLoop3 { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getTenant() { + return "redirect-loop3"; + } +} 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 3e5fb78ce14fd..9c05debd5e48f 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 @@ -34,6 +34,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.oidc.hr.credentials.secret", "secret"), Map.entry("quarkus.oidc.hr.tenant-paths", "/api/tenant-echo/http-security-policy-applies-all-same"), Map.entry("quarkus.oidc.hr.token.audience", "http://hr.service"), + Map.entry("quarkus.oidc.hr.follow-redirects", "false"), Map.entry("quarkus.http.auth.policy.roles1.roles-allowed", "role1"), Map.entry("quarkus.http.auth.policy.roles2.roles-allowed", "role2"), Map.entry("quarkus.http.auth.policy.roles3.roles-allowed", "role3,role2"), @@ -68,7 +69,13 @@ public Map getConfigOverrides() { Map.entry("quarkus.http.auth.permission.tenant-annotation-applies-all.paths", "/api/tenant-echo/http-security-policy-applies-all-diff,/api/tenant-echo/http-security-policy-applies-all-same"), Map.entry("quarkus.http.auth.permission.tenant-annotation-applies-all.policy", "admin-role"), - Map.entry("quarkus.http.auth.policy.admin-role.roles-allowed", "admin")); + Map.entry("quarkus.http.auth.policy.admin-role.roles-allowed", "admin"), + Map.entry("quarkus.oidc.redirect-loop.auth-server-url", "http://localhost:8180/auth/realms/quarkus3/"), + Map.entry("quarkus.oidc.redirect-loop.follow-redirects", "false"), + Map.entry("quarkus.oidc.redirect-loop2.auth-server-url", "http://localhost:8180/auth/realms/quarkus4/"), + Map.entry("quarkus.oidc.redirect-loop2.follow-redirects", "false"), + Map.entry("quarkus.oidc.redirect-loop3.auth-server-url", "http://localhost:8180/auth/realms/quarkus5/"), + Map.entry("quarkus.oidc.redirect-loop3.follow-redirects", "false")); } @Override @@ -101,6 +108,18 @@ public void testClassLevelAnnotation() { .body(Matchers.equalTo(("tenant-id=hr, static.tenant.id=hr, name=alice, " + OidcUtils.TENANT_ID_SET_BY_ANNOTATION + "=hr"))); + RestAssured.given().auth().oauth2(token) + .when().get("/api-redirect-loop") + .then().statusCode(500); + + RestAssured.given().auth().oauth2(token) + .when().get("/api-redirect-loop2") + .then().statusCode(500); + + RestAssured.given().auth().oauth2(token) + .when().get("/api-redirect-loop3") + .then().statusCode(500); + } finally { server.stop(); } 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 51e3b088fd874..e0596bae9875a 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,6 +40,17 @@ public void start() { server.stubFor( get(urlEqualTo("/auth/realms/quarkus2/.well-known/openid-configuration")) + .withHeader("Filter", equalTo("OK")) + .withHeader("Cookie", absent()) + .willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "http://localhost:" + port + + "/auth/realms/quarkus2/.well-known/openid-configuration") + .withHeader("Set-Cookie", "redirect=true; Path=/; Domain=some.domain.com"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus2/.well-known/openid-configuration")) + .withHeader("Cookie", equalTo("redirect=true")) .withHeader("Filter", equalTo("OK")) .withHeader("tenant-id", not(absent())) .willReturn(aResponse() @@ -68,6 +79,125 @@ public void start() { " ]\n" + "}"))); + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus3/.well-known/openid-configuration")) + .withHeader("Filter", equalTo("OK")) + .withHeader("Cookie", absent()) + .willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "http://localhost:" + port + + "/auth/realms/quarkus3/.well-known/openid-configuration") + .withHeader("Set-Cookie", "redirect1=true; Path=/; Domain=some.domain.com"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus3/.well-known/openid-configuration")) + .withHeader("Filter", equalTo("OK")) + .withHeader("Cookie", equalTo("redirect1=true")) + .willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "http://localhost:" + port + + "/auth/realms/quarkus3/.well-known/openid-configuration") + .withHeader("Set-Cookie", "redirect2=true; Path=/; Domain=some.domain.com"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus3/.well-known/openid-configuration")) + .withHeader("Cookie", equalTo("redirect2=true")) + .withHeader("Filter", equalTo("OK")) + .withHeader("tenant-id", not(absent())) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + (issuer == null ? "" : " \"" + ISSUER + "\": " + "\"" + issuer + "\",\n") + + " \"jwks_uri\": \"" + server.baseUrl() + + "/auth/realms/quarkus3/protocol/openid-connect/certs\"" + + "}"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus3/protocol/openid-connect/certs")) + .withHeader("Filter", equalTo("OK")) + .withHeader("tenant-id", not(absent())) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"keys\" : [\n" + + " {\n" + + " \"kid\": \"1\",\n" + + " \"kty\":\"RSA\",\n" + + " \"n\":\"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ\",\n" + + + " \"e\":\"AQAB\"\n" + + " }\n" + + " ]\n" + + "}"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus4/.well-known/openid-configuration")) + .withHeader("Filter", equalTo("OK")) + .withHeader("Cookie", absent()) + .willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "http://localhost:" + port + + "/auth/realms/quarkus4/.well-known/different-openid-configuration") + .withHeader("Set-Cookie", "redirect=true; Path=/; Domain=some.domain.com"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus4/.well-known/different-openid-configuration")) + .withHeader("Cookie", equalTo("redirect=true")) + .withHeader("Filter", equalTo("OK")) + .withHeader("tenant-id", not(absent())) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + (issuer == null ? "" : " \"" + ISSUER + "\": " + "\"" + issuer + "\",\n") + + " \"jwks_uri\": \"" + server.baseUrl() + + "/auth/realms/quarkus4/protocol/openid-connect/certs\"" + + "}"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus4/protocol/openid-connect/certs")) + .withHeader("Filter", equalTo("OK")) + .withHeader("tenant-id", not(absent())) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"keys\" : [\n" + + " {\n" + + " \"kid\": \"1\",\n" + + " \"kty\":\"RSA\",\n" + + " \"n\":\"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ\",\n" + + + " \"e\":\"AQAB\"\n" + + " }\n" + + " ]\n" + + "}"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus5/.well-known/openid-configuration")) + .withHeader("Filter", equalTo("OK")) + .withHeader("Cookie", absent()) + .willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "http://localhost:" + port + + "/auth/realms/quarkus5/.well-known/openid-configuration"))); + + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus5/protocol/openid-connect/certs")) + .withHeader("Filter", equalTo("OK")) + .withHeader("tenant-id", not(absent())) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"keys\" : [\n" + + " {\n" + + " \"kid\": \"1\",\n" + + " \"kty\":\"RSA\",\n" + + " \"n\":\"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ\",\n" + + + " \"e\":\"AQAB\"\n" + + " }\n" + + " ]\n" + + "}"))); + LOG.infof("Keycloak started in mock mode: %s", server.baseUrl()); }