Skip to content

Commit 96c1720

Browse files
authored
Merge pull request quarkusio#52728 from sberyozkin/oidc_second_redirect_absolute_uri
Use absolute redirect path in all redirects to Quarkus
2 parents 5413922 + 2171bdc commit 96c1720

File tree

6 files changed

+244
-20
lines changed

6 files changed

+244
-20
lines changed

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

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
218218
.transformToUni(new Function<TenantConfigContext, Uni<? extends SecurityIdentity>>() {
219219
@Override
220220
public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
221-
URI absoluteUri = URI.create(context.request().absoluteURI());
222221

223222
String userQuery = null;
224223

@@ -233,12 +232,11 @@ public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
233232
}
234233
}
235234

236-
StringBuilder errorUri = new StringBuilder(buildUri(context,
237-
isForceHttps(oidcTenantConfig),
238-
absoluteUri.getAuthority(),
239-
oidcTenantConfig.authentication().errorPath().get()));
235+
StringBuilder errorUri = prepareRedirectPathBuilder(context, oidcTenantConfig,
236+
oidcTenantConfig.authentication().errorPath().get());
237+
240238
errorUri.append('?')
241-
.append(getRequestParametersAsQuery(absoluteUri, requestParams, oidcTenantConfig));
239+
.append(getRequestParametersAsQuery(context, requestParams, oidcTenantConfig));
242240
if (userQuery != null) {
243241
errorUri.append('&').append(userQuery);
244242
}
@@ -268,6 +266,17 @@ public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
268266

269267
}
270268

269+
protected StringBuilder prepareRedirectPathBuilder(RoutingContext context, OidcTenantConfig oidcTenantConfig, String path) {
270+
StringBuilder sb = new StringBuilder();
271+
272+
if (path.startsWith(HTTP_SCHEME)) {
273+
sb.append(path);
274+
} else {
275+
sb.append(buildUri(context, isForceHttps(oidcTenantConfig), context.request().authority().toString(), path));
276+
}
277+
return sb;
278+
}
279+
271280
private static String filterRedirect(RoutingContext context,
272281
TenantConfigContext tenantContext, String redirectUri, Redirect.Location location) {
273282
List<OidcRedirectFilter> redirectFilters = tenantContext.getOidcRedirectFilters(location);
@@ -332,11 +341,11 @@ private void removeStateCookies(OidcTenantConfig oidcTenantConfig, RoutingContex
332341

333342
}
334343

335-
private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParams, OidcTenantConfig oidcConfig) {
344+
private String getRequestParametersAsQuery(RoutingContext context, MultiMap requestParams, OidcTenantConfig oidcConfig) {
336345
if (ResponseMode.FORM_POST == oidcConfig.authentication().responseMode().orElse(ResponseMode.QUERY)) {
337346
return OidcCommonUtils.encodeForm(new io.vertx.mutiny.core.MultiMap(requestParams)).toString();
338347
} else {
339-
return requestUri.getRawQuery();
348+
return context.request().query();
340349
}
341350
}
342351

@@ -505,11 +514,9 @@ private Uni<SecurityIdentity> autoRefreshIsNotPossible(RoutingContext context, T
505514
}
506515

507516
private Uni<SecurityIdentity> redirectToSessionExpiredPage(RoutingContext context, TenantConfigContext configContext) {
508-
URI absoluteUri = URI.create(context.request().absoluteURI());
509-
StringBuilder sessionExpired = new StringBuilder(buildUri(context,
510-
isForceHttps(configContext.oidcConfig()),
511-
absoluteUri.getAuthority(),
512-
configContext.oidcConfig().authentication().sessionExpiredPath().get()));
517+
StringBuilder sessionExpired = prepareRedirectPathBuilder(context, configContext.oidcConfig(),
518+
configContext.oidcConfig().authentication().sessionExpiredPath().get());
519+
513520
String sessionExpiredUri = sessionExpired.toString();
514521
LOG.debugf("Session Expired URI: %s", sessionExpiredUri);
515522
return removeSessionCookie(context, configContext.oidcConfig())
@@ -956,17 +963,32 @@ public SecurityIdentity apply(SecurityIdentity identity) {
956963
if (removeRedirectParams || finalUserPath != null
957964
|| finalUserQuery != null) {
958965

959-
URI absoluteUri = URI.create(context.request().absoluteURI());
966+
StringBuilder finalUriWithoutQuery = new StringBuilder();
960967

961-
StringBuilder finalUriWithoutQuery = new StringBuilder(buildUri(context,
962-
isForceHttps(configContext.oidcConfig()),
963-
absoluteUri.getAuthority(),
964-
(finalUserPath != null ? finalUserPath
965-
: absoluteUri.getRawPath())));
968+
String redirectPath = configContext.oidcConfig().authentication()
969+
.redirectPath().orElse(null);
970+
if (redirectPath != null && redirectPath.startsWith(HTTP_SCHEME)) {
971+
// This is the actual URI that OIDC provider used to redirect the user back to Quarkus
972+
if (finalUserPath == null) {
973+
// No need to restore the original request path
974+
finalUriWithoutQuery.append(redirectPath);
975+
} else {
976+
URI redirectUri = URI.create(redirectPath);
977+
finalUriWithoutQuery.append(
978+
buildUri(redirectUri.getScheme(), redirectUri.getAuthority(), "",
979+
finalUserPath));
980+
}
981+
} else {
982+
finalUriWithoutQuery.append(
983+
buildUri(context, isForceHttps(configContext.oidcConfig()),
984+
context.request().authority().toString(),
985+
(finalUserPath != null ? finalUserPath
986+
: context.request().path())));
987+
}
966988

967989
if (!removeRedirectParams) {
968990
finalUriWithoutQuery.append('?')
969-
.append(getRequestParametersAsQuery(absoluteUri, requestParams,
991+
.append(getRequestParametersAsQuery(context, requestParams,
970992
configContext.oidcConfig()));
971993
}
972994
if (finalUserQuery != null) {
@@ -1375,6 +1397,10 @@ private String buildUri(RoutingContext context, boolean forceHttps, String autho
13751397
}
13761398
}
13771399
}
1400+
return buildUri(scheme, authority, forwardedPrefix, path);
1401+
}
1402+
1403+
private static String buildUri(String scheme, String authority, String forwardedPrefix, String path) {
13781404
return new StringBuilder(scheme).append("://")
13791405
.append(authority)
13801406
.append(forwardedPrefix)

integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ public String resolve(RoutingContext context) {
7878
}
7979
}
8080

81+
if (path.endsWith("tenant-absolute-redirect") || path.endsWith("tenant-absolute-redirect/callback")) {
82+
return "tenant-absolute-redirect";
83+
}
84+
85+
if (path.endsWith("tenant-restore-path-absolute-redirect")
86+
|| path.endsWith("tenant-restore-path-absolute-redirect/callback")) {
87+
return "tenant-restore-path-absolute-redirect";
88+
}
89+
8190
if (path.contains("tenant-xhr")) {
8291
return "tenant-xhr";
8392
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.quarkus.it.keycloak;
2+
3+
import jakarta.ws.rs.GET;
4+
import jakarta.ws.rs.Path;
5+
import jakarta.ws.rs.core.Context;
6+
import jakarta.ws.rs.core.UriInfo;
7+
8+
import io.quarkus.security.Authenticated;
9+
10+
@Path("/tenant-absolute-redirect")
11+
public class TenantAbsoluteRedirect {
12+
13+
@Context
14+
UriInfo ui;
15+
16+
@GET
17+
@Authenticated
18+
public String getTenant() {
19+
throw new RuntimeException("/tenant-absolute-redirect/callback is a callback method");
20+
}
21+
22+
@GET
23+
@Authenticated
24+
@Path("/callback")
25+
public String getTenantCallback() {
26+
return ui.getAbsolutePath().toString();
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.quarkus.it.keycloak;
2+
3+
import jakarta.ws.rs.GET;
4+
import jakarta.ws.rs.Path;
5+
import jakarta.ws.rs.core.Context;
6+
import jakarta.ws.rs.core.UriInfo;
7+
8+
import io.quarkus.security.Authenticated;
9+
10+
@Path("/tenant-restore-path-absolute-redirect")
11+
public class TenantRestorePathAbsoluteRedirect {
12+
13+
@Context
14+
UriInfo ui;
15+
16+
@GET
17+
@Authenticated
18+
public String getTenant() {
19+
return ui.getAbsolutePath().toString();
20+
}
21+
22+
@GET
23+
@Authenticated
24+
@Path("/callback")
25+
public String getTenantCallback() {
26+
throw new RuntimeException("/tenant-restore-path-absolute-redirect must be restored");
27+
}
28+
}

integration-tests/oidc-code-flow/src/main/resources/application.properties

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,23 @@ quarkus.oidc.tenant-nonce.resource-metadata.resource=/metadata
156156
quarkus.oidc.tenant-nonce.resource-metadata.scopes=read
157157
quarkus.oidc.tenant-nonce.resource-metadata.authorization-server=http://localhost:8080/q/oidc
158158

159+
quarkus.oidc.tenant-absolute-redirect.auth-server-url=${quarkus.oidc.auth-server-url}
160+
quarkus.oidc.tenant-absolute-redirect.client-id=quarkus-app
161+
quarkus.oidc.tenant-absolute-redirect.credentials.secret=secret
162+
quarkus.oidc.tenant-absolute-redirect.authentication.scopes=profile,email,phone
163+
quarkus.oidc.tenant-absolute-redirect.authentication.redirect-path=http://localhost:8081/tenant-absolute-redirect/callback
164+
quarkus.oidc.tenant-absolute-redirect.authentication.extra-params.max-age=60
165+
quarkus.oidc.tenant-absolute-redirect.application-type=web-app
166+
167+
quarkus.oidc.tenant-restore-path-absolute-redirect.auth-server-url=${quarkus.oidc.auth-server-url}
168+
quarkus.oidc.tenant-restore-path-absolute-redirect.client-id=quarkus-app
169+
quarkus.oidc.tenant-restore-path-absolute-redirect.credentials.secret=secret
170+
quarkus.oidc.tenant-restore-path-absolute-redirect.authentication.scopes=profile,email,phone
171+
quarkus.oidc.tenant-restore-path-absolute-redirect.authentication.redirect-path=http://localhost:8081/tenant-restore-path-absolute-redirect/callback
172+
quarkus.oidc.tenant-restore-path-absolute-redirect.authentication.extra-params.max-age=60
173+
quarkus.oidc.tenant-restore-path-absolute-redirect.authentication.restore-path-after-redirect=true
174+
quarkus.oidc.tenant-restore-path-absolute-redirect.application-type=web-app
175+
159176
quarkus.oidc.tenant-javascript.auth-server-url=${quarkus.oidc.auth-server-url}
160177
quarkus.oidc.tenant-javascript.client-id=quarkus-app
161178
quarkus.oidc.tenant-javascript.credentials.secret=secret

integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,122 @@ public void testCodeFlowMissingNonce() throws Exception {
638638
}
639639
}
640640

641+
@Test
642+
public void testCodeFlowAbsoluteRedirect() throws Exception {
643+
try (final WebClient webClient = createWebClient()) {
644+
webClient.getOptions().setRedirectEnabled(false);
645+
646+
WebResponse webResponse = webClient
647+
.loadWebResponse(
648+
new WebRequest(
649+
URI.create("http://localhost:8081/tenant-absolute-redirect?custom=customValue").toURL()));
650+
String keycloakUrl = webResponse.getResponseHeaderValue("location");
651+
verifyLocationHeader(webClient, keycloakUrl, "tenant-absolute-redirect", "tenant-absolute-redirect%2Fcallback",
652+
false);
653+
654+
HtmlPage page = webClient.getPage(keycloakUrl);
655+
656+
assertEquals("Sign in to quarkus", page.getTitleText());
657+
HtmlForm loginForm = page.getForms().get(0);
658+
loginForm.getInputByName("username").setValueAttribute("alice");
659+
loginForm.getInputByName("password").setValueAttribute("alice");
660+
661+
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
662+
webResponse = loginForm.getButtonByName("login").click().getWebResponse();
663+
webClient.getOptions().setThrowExceptionOnFailingStatusCode(true);
664+
665+
// This is a redirect from the OIDC server to the endpoint, with technical paramerts like `code`
666+
// but without a request specific query parameter
667+
String endpointLocation = webResponse.getResponseHeaderValue("location");
668+
669+
URI endpointLocationUri = URI.create(endpointLocation);
670+
assertEquals("http://localhost:8081/tenant-absolute-redirect/callback",
671+
endpointLocationUri.getScheme() + "://" + endpointLocationUri.getAuthority()
672+
+ endpointLocationUri.getPath());
673+
assertTrue(endpointLocationUri.getQuery().contains("code="));
674+
assertTrue(endpointLocationUri.getQuery().contains("state="));
675+
assertFalse(endpointLocationUri.getQuery().contains("custom="));
676+
677+
// This is a final redirect dropping the technical parameters like `code`
678+
// but restoring the custom query parameter
679+
webResponse = webClient.loadWebResponse(new WebRequest(endpointLocationUri.toURL()));
680+
endpointLocation = webResponse.getResponseHeaderValue("location");
681+
endpointLocationUri = URI.create(endpointLocation);
682+
assertEquals("http://localhost:8081/tenant-absolute-redirect/callback",
683+
endpointLocationUri.getScheme() + "://" + endpointLocationUri.getAuthority()
684+
+ endpointLocationUri.getPath());
685+
686+
assertFalse(endpointLocationUri.getQuery().contains("code="));
687+
assertFalse(endpointLocationUri.getQuery().contains("state="));
688+
assertTrue(endpointLocationUri.getQuery().contains("custom=customValue"));
689+
690+
webResponse = webClient.loadWebResponse(new WebRequest(endpointLocationUri.toURL()));
691+
assertEquals(200, webResponse.getStatusCode());
692+
assertEquals("http://localhost:8081/tenant-absolute-redirect/callback", webResponse.getContentAsString());
693+
694+
webClient.getCookieManager().clearCookies();
695+
}
696+
}
697+
698+
@Test
699+
public void testCodeFlowRestorePathAbsoluteRedirect() throws Exception {
700+
try (final WebClient webClient = createWebClient()) {
701+
webClient.getOptions().setRedirectEnabled(false);
702+
703+
WebResponse webResponse = webClient
704+
.loadWebResponse(
705+
new WebRequest(
706+
URI.create("http://localhost:8081/tenant-restore-path-absolute-redirect?custom=customValue")
707+
.toURL()));
708+
String keycloakUrl = webResponse.getResponseHeaderValue("location");
709+
verifyLocationHeader(webClient, keycloakUrl, "tenant-restore-path-absolute-redirect",
710+
"tenant-restore-path-absolute-redirect%2Fcallback",
711+
false);
712+
713+
HtmlPage page = webClient.getPage(keycloakUrl);
714+
715+
assertEquals("Sign in to quarkus", page.getTitleText());
716+
HtmlForm loginForm = page.getForms().get(0);
717+
loginForm.getInputByName("username").setValueAttribute("alice");
718+
loginForm.getInputByName("password").setValueAttribute("alice");
719+
720+
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
721+
webResponse = loginForm.getButtonByName("login").click().getWebResponse();
722+
webClient.getOptions().setThrowExceptionOnFailingStatusCode(true);
723+
724+
// This is a redirect from the OIDC server to the endpoint, with technical paramerts like `code`
725+
// but without a request specific query parameter
726+
String endpointLocation = webResponse.getResponseHeaderValue("location");
727+
728+
URI endpointLocationUri = URI.create(endpointLocation);
729+
assertEquals("http://localhost:8081/tenant-restore-path-absolute-redirect/callback",
730+
endpointLocationUri.getScheme() + "://" + endpointLocationUri.getAuthority()
731+
+ endpointLocationUri.getPath());
732+
assertTrue(endpointLocationUri.getQuery().contains("code="));
733+
assertTrue(endpointLocationUri.getQuery().contains("state="));
734+
assertFalse(endpointLocationUri.getQuery().contains("custom="));
735+
736+
// This is a final redirect dropping the technical parameters like `code`
737+
// but restoring the custom query parameter, as well as the original request path
738+
webResponse = webClient.loadWebResponse(new WebRequest(endpointLocationUri.toURL()));
739+
endpointLocation = webResponse.getResponseHeaderValue("location");
740+
endpointLocationUri = URI.create(endpointLocation);
741+
assertEquals("http://localhost:8081/tenant-restore-path-absolute-redirect",
742+
endpointLocationUri.getScheme() + "://" + endpointLocationUri.getAuthority()
743+
+ endpointLocationUri.getPath());
744+
745+
assertFalse(endpointLocationUri.getQuery().contains("code="));
746+
assertFalse(endpointLocationUri.getQuery().contains("state="));
747+
assertTrue(endpointLocationUri.getQuery().contains("custom=customValue"));
748+
749+
webResponse = webClient.loadWebResponse(new WebRequest(endpointLocationUri.toURL()));
750+
assertEquals(200, webResponse.getStatusCode());
751+
assertEquals("http://localhost:8081/tenant-restore-path-absolute-redirect", webResponse.getContentAsString());
752+
753+
webClient.getCookieManager().clearCookies();
754+
}
755+
}
756+
641757
@Test
642758
public void testCodeFlowForceHttpsRedirectUriAndPkceMissingCodeVerifier() throws Exception {
643759
try (final WebClient webClient = createWebClient()) {

0 commit comments

Comments
 (0)