Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for single same uri redirects for OIDC WebClient #43938

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,15 @@ In such cases, the endpoint might report an issuer verification failure and redi
If you work with Keycloak, then start it with a `KEYCLOAK_FRONTEND_URL` system property set to the externally-accessible base URL.
If you work with other OIDC providers, check the documentation of your provider.

=== OIDC HTTP client redirects

OIDC providers behind a firewall may redirect Quarkus OIDC HTTP client's GET requests to some of its endpoints such as a well-known configuration endpoint.
By default, Quarkus OIDC HTTP client follows HTTP redirects automatically, excluding cookies which may have been set during the redirect request for security reasons.

If you would like, you can disable it with `quarkus.oidc.follow-redirects=false`.

When following redirects automatically is disabled, and Quarkus OIDC HTTP client receives a redirect request, it will attempt to recover only once by following the redirect URI, but only if it is exactly the same as the original request URI, and as long as one or more cookies were set during the redirect request.

[[oidc-saml-broker]]
== OIDC SAML identity broker

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public static Uni<OidcClientRegistration> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected static Uni<OidcClient> 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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> cookies;

public OidcClientRedirectException(String location, List<String> setCookies) {
this.location = location;
this.cookies = getCookies(setCookies);
}

private static List<String> getCookies(List<String> setCookies) {
if (setCookies != null && !setCookies.isEmpty()) {
List<String> 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<String> getCookies() {
return cookies;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = '=';
Expand Down Expand Up @@ -501,15 +504,65 @@ public static Predicate<? super Throwable> oidcEndpointNotAvailable() {
|| (t instanceof OidcEndpointAccessException && ((OidcEndpointAccessException) t).getErrorStatus() == 404));
}

public static Predicate<? super Throwable> 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<JsonObject> discoverMetadata(WebClient client,
Map<OidcEndpoint.Type, List<OidcRequestFilter>> requestFilters,
OidcRequestContextProperties contextProperties, Map<OidcEndpoint.Type, List<OidcResponseFilter>> responseFilters,
String authServerUrl,
long connectionDelayInMillisecs, Vertx vertx, boolean blockingDnsLookup) {
final String discoveryUrl = getDiscoveryUri(authServerUrl);
HttpRequest<Buffer> 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<Throwable, Uni<? extends JsonObject>>() {
@Override
public Uni<JsonObject> 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<JsonObject> doDiscoverMetadata(WebClient client,
Map<OidcEndpoint.Type, List<OidcRequestFilter>> requestFilters,
OidcRequestContextProperties requestProps, Map<OidcEndpoint.Type, List<OidcResponseFilter>> responseFilters,
String discoveryUrl,
long connectionDelayInMillisecs, Vertx vertx, boolean blockingDnsLookup,
List<String> cookies) {
HttpRequest<Buffer> 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)) {
Expand All @@ -523,8 +576,10 @@ public static Uni<JsonObject> 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);
Expand All @@ -536,12 +591,12 @@ public static Uni<JsonObject> 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<Buffer> resp) {
LOG.debug("OIDC client redirect is requested");
return new OidcClientRedirectException(resp.getHeader(LOCATION_RESPONSE_HEADER), resp.cookies());
}

private static OidcRequestContextProperties getDiscoveryRequestProps(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -87,34 +89,69 @@ public OidcConfigurationMetadata getMetadata() {
}

public Uni<JsonWebKeySet> 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<Throwable, Uni<? extends JsonWebKeySet>>() {
@Override
public Uni<JsonWebKeySet> apply(Throwable t) {
OidcClientRedirectException ex = (OidcClientRedirectException) t;
return doGetJsonWebKeySet(requestProps, ex.getCookies());
}
});
}

private Uni<JsonWebKeySet> doGetJsonWebKeySet(OidcRequestContextProperties requestProps, List<String> cookies) {
LOG.debugf("Get verification JWT Key Set at %s", metadata.getJsonWebKeySetUri());
HttpRequest<Buffer> 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<UserInfoResponse> getUserInfo(String token) {
public Uni<UserInfoResponse> getUserInfo(final String token) {

final OidcRequestContextProperties requestProps = getRequestProps(null, null);

return doGetUserInfo(requestProps, token, List.of())
.onFailure(OidcCommonUtils.validOidcClientRedirect(metadata.getUserInfoUri()))
.recoverWithUni(
new Function<Throwable, Uni<? extends UserInfoResponse>>() {
@Override
public Uni<UserInfoResponse> apply(Throwable t) {
OidcClientRedirectException ex = (OidcClientRedirectException) t;
return doGetUserInfo(requestProps, token, ex.getCookies());
}
});
}

private Uni<UserInfoResponse> doGetUserInfo(OidcRequestContextProperties requestProps, String token, List<String> cookies) {
LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token);
OidcRequestContextProperties requestProps = getRequestProps(null, null);

HttpRequest<Buffer> 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<TokenIntrospection> introspectToken(String token) {
MultiMap introspectionParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
public Uni<TokenIntrospection> 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));
}
Expand All @@ -128,7 +165,7 @@ public OidcTenantConfig getOidcConfig() {
}

public Uni<AuthorizationCodeTokens> 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);
Expand All @@ -138,16 +175,16 @@ public Uni<AuthorizationCodeTokens> 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<AuthorizationCodeTokens> 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));
}
Expand Down Expand Up @@ -205,7 +242,7 @@ private UniOnItem<HttpResponse<Buffer>> 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<HttpResponse<Buffer>> response = filterHttpRequest(requestProps, endpoint, request, buffer, null).sendBuffer(buffer)
Uni<HttpResponse<Buffer>> response = filterHttpRequest(requestProps, endpoint, request, buffer).sendBuffer(buffer)
.onFailure(ConnectException.class)
.retry()
.atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause());
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -284,8 +325,7 @@ public Key getClientJwtKey() {
}

private HttpRequest<Buffer> filterHttpRequest(OidcRequestContextProperties requestProps, OidcEndpoint.Type endpointType,
HttpRequest<Buffer> request, Buffer body,
OidcRequestContextProperties contextProperties) {
HttpRequest<Buffer> request, Buffer body) {
if (!requestFilters.isEmpty()) {
OidcRequestContext context = new OidcRequestContext(request, body, requestProps);
for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(requestFilters, endpointType)) {
Expand Down
Loading
Loading