Skip to content

Commit 3e791db

Browse files
committed
Support for OIDC RP-Initiated form post logout mode
1 parent 71c8d21 commit 3e791db

File tree

13 files changed

+193
-9
lines changed

13 files changed

+193
-9
lines changed

docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ Quarkus OIDC supports three standard OIDC logout options: <<rp-initiated-logout>
643643
|quarkus.oidc.logout.post-logout-path || Post logout path
644644
|quarkus.oidc.logout.post-logout-uri-param || Post logout uri param
645645
|quarkus.oidc.logout.extra-params || Logout extra params
646+
|quarkus.oidc.logout.logout-mode |query| Logout mode
646647
|====
647648

648649
`quarkus.oidc.logout.path` is a relative path where a user logout request should be sent to. For example, given `quarkus.oidc.logout.path=/logout`, a `Logout` link in the SPA page can point to `http://localhost:8080/logout`. This path can be virtual, you do not have to create a JAX-RS endpoint or route handler listening on `/logout`. But for the `quarkus.oidc.logout.path` be effective, it must be secured, see the https://quarkus.io/guides/security-oidc-code-flow-authentication#user-initiated-logout[User-initiated logout] section for more details.
@@ -653,6 +654,9 @@ For the post logout redirect to work, OIDC providers usually require registering
653654

654655
`quarkus.oidc.logout.post-logout-uri-param` and `quarkus.oidc.logout.extra-params` can be used to customize the RP-initiated logout query parameters, for example, Auth0 might expect Auth0-specific logout query parameters, see the https://quarkus.io/guides/security-oidc-code-flow-authentication#user-initiated-logout[User-initiated logout] section for more details.
655656

657+
By default, all the logout parameters are serialized as logout URL query parameters.
658+
Some OIDC providers may require that logout parameters are encoded as HTML form values and auto-submitted in the browser with the HTTP POST method and the `application/x-www-form-urlencoded` content type: set `quarkus.oidc.logout.logout-mode=form-post` in this case.
659+
656660
[[back-channel-logout]]
657661
=== Back-channel Logout
658662

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.quarkus.oidc;
2+
3+
import java.util.Map;
4+
5+
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
6+
import io.quarkus.oidc.common.runtime.OidcConstants;
7+
import io.quarkus.oidc.runtime.TenantConfigContext;
8+
import io.vertx.ext.web.RoutingContext;
9+
10+
public final class LogoutUtils {
11+
12+
private static final String FORM_POST_LOGOUT_START = "<html>"
13+
+ " <head><title>Logout Form</title></head>"
14+
+ " <body onload=\"javascript:document.forms[0].submit()\">"
15+
+ " <form method=\"post\" action=\"";
16+
private static final String FORM_POST_LOGOUT_END = " </form></body></html>";
17+
18+
private LogoutUtils() {
19+
20+
}
21+
22+
public static String createFormPostLogout(TenantConfigContext configContext, RoutingContext context,
23+
String idToken, String postLogoutUrl, String postLogoutState) {
24+
StringBuilder sb = new StringBuilder();
25+
sb.append(FORM_POST_LOGOUT_START);
26+
sb.append(configContext.provider().getMetadata().getEndSessionUri()).append("\">");
27+
if (idToken != null) {
28+
addInput(sb, OidcConstants.LOGOUT_ID_TOKEN_HINT, idToken);
29+
}
30+
if (postLogoutUrl != null) {
31+
addInput(sb, configContext.oidcConfig().logout().postLogoutUriParam(), postLogoutUrl);
32+
}
33+
if (postLogoutState != null) {
34+
addInput(sb, OidcConstants.LOGOUT_STATE, postLogoutState);
35+
}
36+
37+
Map<String, String> extraParams = configContext.oidcConfig().logout().extraParams();
38+
if (extraParams != null) {
39+
for (Map.Entry<String, String> entry : extraParams.entrySet()) {
40+
addInput(sb, entry.getKey(), entry.getValue());
41+
}
42+
}
43+
sb.append(FORM_POST_LOGOUT_END);
44+
return sb.toString();
45+
}
46+
47+
private static void addInput(StringBuilder sb, String name, String value) {
48+
sb.append("<input type=\"hidden\" name=\"").append(name)
49+
.append("\" value=\"").append(OidcCommonUtils.urlEncode(value)).append("\"");
50+
}
51+
52+
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,8 @@ public static class Logout implements io.quarkus.oidc.runtime.OidcTenantConfig.L
488488
*/
489489
Optional<Set<ClearSiteData>> clearSiteData = Optional.of(Set.of());
490490

491+
LogoutMode logoutMode = LogoutMode.QUERY;
492+
491493
/**
492494
* Back-Channel Logout configuration
493495
*/
@@ -552,6 +554,7 @@ private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Log
552554
postLogoutUriParam = mapping.postLogoutUriParam();
553555
extraParams = mapping.extraParams();
554556
clearSiteData = mapping.clearSiteData();
557+
logoutMode = mapping.logoutMode();
555558
backchannel.addConfigMappingValues(mapping.backchannel());
556559
frontchannel.addConfigMappingValues(mapping.frontchannel());
557560
}
@@ -590,6 +593,11 @@ public io.quarkus.oidc.runtime.OidcTenantConfig.Frontchannel frontchannel() {
590593
public Optional<Set<ClearSiteData>> clearSiteData() {
591594
return clearSiteData;
592595
}
596+
597+
@Override
598+
public LogoutMode logoutMode() {
599+
return logoutMode;
600+
}
593601
}
594602

595603
/**

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.quarkus.oidc.AuthorizationCodeTokens;
2929
import io.quarkus.oidc.IdTokenCredential;
3030
import io.quarkus.oidc.JavaScriptRequestChecker;
31+
import io.quarkus.oidc.LogoutUtils;
3132
import io.quarkus.oidc.OidcRedirectFilter;
3233
import io.quarkus.oidc.OidcRedirectFilter.OidcRedirectContext;
3334
import io.quarkus.oidc.OidcTenantConfig;
@@ -39,6 +40,7 @@
3940
import io.quarkus.oidc.common.runtime.OidcConstants;
4041
import io.quarkus.oidc.runtime.OidcTenantConfig.Authentication;
4142
import io.quarkus.oidc.runtime.OidcTenantConfig.Authentication.ResponseMode;
43+
import io.quarkus.oidc.runtime.OidcTenantConfig.Logout.LogoutMode;
4244
import io.quarkus.security.AuthenticationCompletionException;
4345
import io.quarkus.security.AuthenticationFailedException;
4446
import io.quarkus.security.AuthenticationRedirectException;
@@ -47,6 +49,7 @@
4749
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
4850
import io.quarkus.security.spi.runtime.SecurityEventHelper;
4951
import io.quarkus.vertx.http.runtime.security.ChallengeData;
52+
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
5053
import io.smallrye.jwt.build.Jwt;
5154
import io.smallrye.jwt.build.JwtClaimsBuilder;
5255
import io.smallrye.jwt.build.JwtSignatureBuilder;
@@ -1435,8 +1438,10 @@ private Uni<AuthorizationCodeTokens> getCodeFlowTokensUni(RoutingContext context
14351438

14361439
private String buildLogoutRedirectUri(TenantConfigContext configContext, String idToken, RoutingContext context) {
14371440
String logoutPath = configContext.provider().getMetadata().getEndSessionUri();
1441+
Map<String, String> extraParams = configContext.oidcConfig().logout().extraParams();
14381442
StringBuilder logoutUri = new StringBuilder(logoutPath);
1439-
if (idToken != null || configContext.oidcConfig().logout().postLogoutPath().isPresent()) {
1443+
if (idToken != null || configContext.oidcConfig().logout().postLogoutPath().isPresent()
1444+
|| (extraParams != null && !extraParams.isEmpty())) {
14401445
logoutUri.append("?");
14411446
}
14421447
if (idToken != null) {
@@ -1477,10 +1482,29 @@ private Uni<Void> buildLogoutRedirectUriUni(RoutingContext context, TenantConfig
14771482
.map(new Function<Void, Void>() {
14781483
@Override
14791484
public Void apply(Void t) {
1480-
String logoutUri = buildLogoutRedirectUri(configContext, idToken, context);
1481-
LOG.debugf("Logout uri: %s", logoutUri);
1482-
throw new AuthenticationRedirectException(
1483-
filterRedirect(context, configContext, logoutUri, Redirect.Location.OIDC_LOGOUT));
1485+
if (configContext.oidcConfig().logout().logoutMode() == LogoutMode.QUERY) {
1486+
String logoutUri = buildLogoutRedirectUri(configContext, idToken, context);
1487+
LOG.debugf("Logout uri: %s", logoutUri);
1488+
throw new AuthenticationRedirectException(
1489+
filterRedirect(context, configContext, logoutUri, Redirect.Location.OIDC_LOGOUT));
1490+
} else {
1491+
String postLogoutUrl = null;
1492+
String postLogoutState = null;
1493+
if (configContext.oidcConfig().logout().postLogoutPath().isPresent()) {
1494+
postLogoutUrl = OidcCommonUtils
1495+
.urlEncode(buildUri(context, isForceHttps(configContext.oidcConfig()),
1496+
configContext.oidcConfig().logout().postLogoutPath().get()));
1497+
postLogoutState = generatePostLogoutState(context, configContext);
1498+
}
1499+
String formPostLogout = LogoutUtils.createFormPostLogout(configContext, context, idToken,
1500+
postLogoutUrl, postLogoutState);
1501+
context.put(HttpSecurityUtils.FORM_POST_REDIRECT, formPostLogout);
1502+
1503+
LOG.debugf("Initiating form post logout");
1504+
String logoutUrl = filterRedirect(context, configContext,
1505+
configContext.provider().getMetadata().getEndSessionUri(), Redirect.Location.OIDC_LOGOUT);
1506+
throw new AuthenticationRedirectException(200, logoutUrl);
1507+
}
14841508
}
14851509
});
14861510
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,25 @@ public String directive() {
339339
* Clear-Site-Data header directives
340340
*/
341341
Optional<Set<ClearSiteData>> clearSiteData();
342+
343+
enum LogoutMode {
344+
/**
345+
* Logout parameters are encoded in the query string
346+
*/
347+
QUERY,
348+
349+
/**
350+
* Logout parameters are encoded as HTML form values that are auto-submitted in the browser
351+
* and transmitted by the HTTP POST method using the application/x-www-form-urlencoded content type
352+
*/
353+
FORM_POST
354+
}
355+
356+
/**
357+
* Logout mode
358+
*/
359+
@WithDefault("query")
360+
LogoutMode logoutMode();
342361
}
343362

344363
interface Backchannel {

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/builders/LogoutConfigBuilder.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.quarkus.oidc.OidcTenantConfigBuilder;
1313
import io.quarkus.oidc.runtime.OidcTenantConfig;
1414
import io.quarkus.oidc.runtime.OidcTenantConfig.Logout.ClearSiteData;
15+
import io.quarkus.oidc.runtime.OidcTenantConfig.Logout.LogoutMode;
1516

1617
/**
1718
* Builder for the {@link OidcTenantConfig.Logout}.
@@ -20,7 +21,8 @@ public final class LogoutConfigBuilder {
2021
private record LogoutImpl(Optional<String> path, Optional<String> postLogoutPath, String postLogoutUriParam,
2122
Map<String, String> extraParams, OidcTenantConfig.Backchannel backchannel,
2223
OidcTenantConfig.Frontchannel frontchannel,
23-
Optional<Set<ClearSiteData>> clearSiteData) implements OidcTenantConfig.Logout {
24+
Optional<Set<ClearSiteData>> clearSiteData,
25+
LogoutMode logoutMode) implements OidcTenantConfig.Logout {
2426
}
2527

2628
private record FrontchannelImpl(Optional<String> path) implements OidcTenantConfig.Frontchannel {
@@ -34,6 +36,7 @@ private record FrontchannelImpl(Optional<String> path) implements OidcTenantConf
3436
private OidcTenantConfig.Backchannel backchannel;
3537
private OidcTenantConfig.Frontchannel frontchannel;
3638
private Optional<Set<ClearSiteData>> clearSiteData = Optional.of(new HashSet<>());
39+
private LogoutMode logoutMode;
3740

3841
public LogoutConfigBuilder() {
3942
this(new OidcTenantConfigBuilder());
@@ -51,6 +54,7 @@ public LogoutConfigBuilder(OidcTenantConfigBuilder builder) {
5154
this.backchannel = logout.backchannel();
5255
this.frontchannel = logout.frontchannel();
5356
this.clearSiteData = logout.clearSiteData();
57+
this.logoutMode = logout.logoutMode();
5458
}
5559

5660
/**
@@ -132,6 +136,21 @@ public LogoutConfigBuilder clearSiteData(List<ClearSiteData> directives) {
132136
return this;
133137
}
134138

139+
public LogoutConfigBuilder logoutMode() {
140+
this.logoutMode(LogoutMode.QUERY);
141+
return this;
142+
}
143+
144+
/**
145+
* @param clear site data directives {@link OidcTenantConfig.Logout#clearSiteData()}
146+
* @return this builder
147+
*/
148+
public LogoutConfigBuilder logoutMode(LogoutMode logoutMode) {
149+
Objects.requireNonNull(logoutMode);
150+
this.logoutMode = logoutMode;
151+
return this;
152+
}
153+
135154
/**
136155
* @param backchannel {@link OidcTenantConfig.Logout#backchannel()}
137156
* @return this builder
@@ -162,7 +181,7 @@ public OidcTenantConfigBuilder end() {
162181
*/
163182
public OidcTenantConfig.Logout build() {
164183
return new LogoutImpl(path, postLogoutPath, postLogoutUriParam, Map.copyOf(extraParams), backchannel, frontchannel,
165-
clearSiteData);
184+
clearSiteData, logoutMode);
166185
}
167186

168187
/**

extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ enum ConfigMappingMethods {
162162
LOGOUT_POST_LOGOUT_PATH,
163163
LOGOUT_POST_LOGOUT_URI_PARAM,
164164
LOGOUT_CLEAR_SITE_DATA,
165+
LOGOUT_MODE,
165166
LOGOUT_EXTRA_PARAMS,
166167
LOGOUT_BACK_CHANNEL,
167168
LOGOUT_FRONT_CHANNEL,
@@ -506,6 +507,12 @@ public Optional<Set<ClearSiteData>> clearSiteData() {
506507
return Optional.of(Set.of());
507508
}
508509

510+
@Override
511+
public LogoutMode logoutMode() {
512+
invocationsRecorder.put(ConfigMappingMethods.LOGOUT_MODE, true);
513+
return LogoutMode.QUERY;
514+
}
515+
509516
@Override
510517
public Backchannel backchannel() {
511518
invocationsRecorder.put(ConfigMappingMethods.LOGOUT_BACK_CHANNEL, true);

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
import java.nio.charset.StandardCharsets;
1313
import java.nio.file.Files;
1414
import java.nio.file.Path;
15-
import java.util.*;
15+
import java.util.HashMap;
16+
import java.util.HashSet;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Properties;
20+
import java.util.Set;
1621
import java.util.concurrent.CompletionException;
1722
import java.util.function.BiConsumer;
1823
import java.util.function.Consumer;
@@ -207,9 +212,15 @@ public void accept(Throwable throwable) {
207212
proceed(throwable);
208213
} else if (throwable instanceof AuthenticationRedirectException redirectEx) {
209214
event.response().setStatusCode(redirectEx.getCode());
210-
event.response().headers().set(HttpHeaders.LOCATION, redirectEx.getRedirectUri());
211215
event.response().headers().set(HttpHeaders.CACHE_CONTROL, "no-store");
212216
event.response().headers().set("Pragma", "no-cache");
217+
218+
String formPostRedirect = event.get(HttpSecurityUtils.FORM_POST_REDIRECT);
219+
if (formPostRedirect == null) {
220+
event.response().headers().set(HttpHeaders.LOCATION, redirectEx.getRedirectUri());
221+
} else {
222+
event.response().write(formPostRedirect);
223+
}
213224
proceed(throwable);
214225
} else {
215226
event.put(OTHER_AUTHENTICATION_FAILURE, Boolean.TRUE);

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
public final class HttpSecurityUtils {
1616
// keep in sync with QuarkusPermissionSecurityIdentityAugmentor
1717
public final static String ROUTING_CONTEXT_ATTRIBUTE = "quarkus.http.routing.context";
18+
public final static String FORM_POST_REDIRECT = "quarkus.http.form.post.redirect";
1819
static final String SECURITY_IDENTITIES_ATTRIBUTE = "io.quarkus.security.identities";
1920
static final String COMMON_NAME = "CN";
2021
private static final String AUTHENTICATION_FAILURE_KEY = "io.quarkus.vertx.http.runtime.security#authentication-failure";

integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.it.keycloak;
22

3+
import java.net.URI;
34
import java.security.PublicKey;
45
import java.time.Duration;
56
import java.util.Arrays;
@@ -16,6 +17,7 @@
1617
import jakarta.ws.rs.Produces;
1718
import jakarta.ws.rs.QueryParam;
1819
import jakarta.ws.rs.core.Context;
20+
import jakarta.ws.rs.core.Response;
1921
import jakarta.ws.rs.core.UriInfo;
2022

2123
import org.eclipse.microprofile.jwt.JsonWebToken;
@@ -369,6 +371,12 @@ public boolean disableRotate() {
369371
return rotate;
370372
}
371373

374+
@POST
375+
@Path("form-post-logout")
376+
public Response formPostLogout(@FormParam("post_logout_redirect_uri") String postLogoutRedirectUri) throws Exception {
377+
return Response.seeOther(URI.create(postLogoutRedirectUri)).build();
378+
}
379+
372380
private String jwt(String audience, String subject, String kid) {
373381
return jwt(audience, subject, kid, false);
374382
}

0 commit comments

Comments
 (0)