Skip to content

Commit

Permalink
Support for single same uri redirects for OIDC WebClient
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Oct 17, 2024
1 parent 5c3944b commit 65f1fb7
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 30 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ protected static Uni<OidcProviderClient> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}
Loading

0 comments on commit 65f1fb7

Please sign in to comment.