Skip to content

Commit 15ed934

Browse files
authored
Merge pull request #94 from sberyozkin/state_cookie
Set OIDC proxy state cookie during the local redirect
2 parents 9e25152 + 8094412 commit 15ed934

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-5
lines changed

integration-tests/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ quarkus.oidc.web-app.authentication.user-info-required=true
1313
quarkus.oidc.web-app.authentication.cookie-path=/web-app
1414
quarkus.oidc.web-app.logout.path=/web-app/logout
1515
quarkus.oidc.web-app.tenant-paths=/web-app,/web-app/logout
16+
quarkus.oidc.web-app.authentication.allow-multiple-code-flows=false
1617

1718
quarkus.rest-client.service-api-client.url=http://localhost:8081/service
1819

integration-tests/src/test/java/io/quarkus/oidc/proxy/OidcProxyTestCase.java

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertNotNull;
55
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.net.URI;
69

710
import org.htmlunit.SilentCssErrorHandler;
811
import org.htmlunit.TextPage;
912
import org.htmlunit.WebClient;
13+
import org.htmlunit.WebRequest;
14+
import org.htmlunit.WebResponse;
1015
import org.htmlunit.html.HtmlForm;
1116
import org.htmlunit.html.HtmlPage;
1217
import org.htmlunit.util.Cookie;
@@ -21,7 +26,43 @@ public class OidcProxyTestCase {
2126
public void testOidcProxy() throws Exception {
2227

2328
try (final WebClient webClient = createWebClient()) {
24-
HtmlPage page = webClient.getPage("http://localhost:8081/web-app");
29+
// Disable auto-redirect
30+
webClient.getOptions().setRedirectEnabled(false);
31+
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
32+
33+
// This is the protected endpoint redirect to the OIDC provider which is represented by OIDC proxy
34+
WebResponse webResponse = webClient
35+
.loadWebResponse(new WebRequest(URI.create("http://localhost:8081/web-app").toURL()));
36+
37+
// Original state cookie created by `quarkus-oidc`
38+
Cookie stateCookie = getStateCookie(webClient);
39+
assertNotNull(stateCookie);
40+
assertEquals(stateCookie.getName(), "q_auth_web-app");
41+
42+
// Confirm the OIDC proxy is the redirect target
43+
String oidcUrl = webResponse.getResponseHeaderValue("location");
44+
assertTrue(oidcUrl.startsWith("http://localhost:8081/q/oidc/authorize"));
45+
// `quarkus-oidc` expects the OIDC provider redirect the user back to the protected endpoint
46+
assertTrue(oidcUrl.contains("redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fweb-app"));
47+
// No OIDC proxy state cookie available yet
48+
Cookie proxyStateCookie = getProxyStateCookie(webClient);
49+
assertNull(proxyStateCookie);
50+
51+
// This is a redirect from OIDC proxy to Keycloak but expecting a redirect
52+
// to the OIDC proxy managed local redirect endpoint
53+
webResponse = webClient.loadWebResponse(new WebRequest(URI.create(oidcUrl).toURL()));
54+
String keycloakUrl = webResponse.getResponseHeaderValue("location");
55+
assertTrue(keycloakUrl.contains("/protocol/openid-connect/auth"));
56+
assertTrue(keycloakUrl.contains("redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Flocal-redirect"));
57+
58+
// OIDC proxy state cookie must be set by now
59+
proxyStateCookie = getProxyStateCookie(webClient);
60+
assertNotNull(proxyStateCookie);
61+
assertEquals(proxyStateCookie.getName(), "q_proxy_auth");
62+
assertEquals(proxyStateCookie.getValue(), stateCookie.getValue());
63+
64+
// This is a challenge from Keycloak
65+
HtmlPage page = webClient.getPage(keycloakUrl);
2566

2667
assertEquals("Sign in to quarkus", page.getTitleText());
2768

@@ -30,15 +71,44 @@ public void testOidcProxy() throws Exception {
3071
loginForm.getInputByName("username").setValueAttribute("alice");
3172
loginForm.getInputByName("password").setValueAttribute("alice");
3273

33-
TextPage textPage = loginForm.getButtonByName("login").click();
74+
webResponse = loginForm.getButtonByName("login").click().getWebResponse();
75+
76+
// This is a redirect from Keycloak to the OIDC proxy managed local redirect endpoint
77+
String localRedirectUrl = webResponse.getResponseHeaderValue("location");
78+
assertTrue(localRedirectUrl.startsWith("http://localhost:8081/local-redirect"));
79+
80+
// This is a redirect from the OIDC proxy managed local redirect endpoint to the protected endpoint
81+
webResponse = webClient.loadWebResponse(new WebRequest(URI.create(localRedirectUrl).toURL()));
82+
String webAppRedirectUrl = webResponse.getResponseHeaderValue("location");
83+
assertTrue(webAppRedirectUrl.startsWith("http://localhost:8081/web-app"));
84+
85+
// No session cookie is available yet
86+
assertNull(getSessionCookie(webClient));
87+
88+
// Original state cookie is still expected
89+
assertNotNull(getStateCookie(webClient));
90+
// But the OIDC proxy state cookie must be gone by now
91+
assertNull(getProxyStateCookie(webClient));
92+
93+
webClient.getOptions().setRedirectEnabled(true);
94+
95+
// Access the protected endpoint, complete the code flow, get the response
96+
TextPage textPage = webClient.getPage(webAppRedirectUrl);
3497

3598
assertEquals("web-app: ID alice, service: Bearer alice", textPage.getContent());
3699

100+
// Session cookie is ready
37101
assertNotNull(getSessionCookie(webClient));
38102

103+
// Both state cookies must be null
104+
assertNull(getStateCookie(webClient));
105+
assertNull(getProxyStateCookie(webClient));
106+
107+
// Logout
39108
textPage = webClient.getPage("http://localhost:8081/web-app/logout");
40109
assertEquals("You have been logged out", textPage.getContent());
41110

111+
// Session cookie must be null
42112
assertNull(getSessionCookie(webClient));
43113

44114
webClient.getCookieManager().clearCookies();
@@ -55,4 +125,12 @@ private WebClient createWebClient() {
55125
private Cookie getSessionCookie(WebClient webClient) {
56126
return webClient.getCookieManager().getCookie("q_session_web-app");
57127
}
128+
129+
private Cookie getStateCookie(WebClient webClient) {
130+
return webClient.getCookieManager().getCookie("q_auth_web-app");
131+
}
132+
133+
private Cookie getProxyStateCookie(WebClient webClient) {
134+
return webClient.getCookieManager().getCookie("q_proxy_auth");
135+
}
58136
}

runtime/src/main/java/io/quarkus/oidc/proxy/runtime/OidcProxy.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
public class OidcProxy {
4545
private static final Logger LOG = Logger.getLogger(OidcProxy.class);
46+
private static final String OIDC_PROXY_STATE_COOKIE = "q_proxy_auth";
4647
final OidcConfigurationMetadata oidcMetadata;
4748
final OidcTenantConfig oidcTenantConfig;
4849
final OidcProxyConfig oidcProxyConfig;
@@ -150,6 +151,11 @@ public void authorize(RoutingContext context) {
150151
codeFlowParams.append("&").append(OidcConstants.CODE_FLOW_STATE).append("=")
151152
.append(state);
152153

154+
if (localAuthorizationCodeFlowRedirect) {
155+
OidcUtils.createCookie(context, oidcTenantConfig, OIDC_PROXY_STATE_COOKIE, state,
156+
oidcTenantConfig.authentication().stateCookieAge().getSeconds());
157+
}
158+
153159
// nonce
154160
final String nonce = queryParams.get(OidcConstants.NONCE);
155161
if (nonce != null) {
@@ -241,8 +247,29 @@ public void localAuthorizationCodeFlowRedirect(RoutingContext context) {
241247
// code
242248
codeFlowParams.append(OidcConstants.CODE_FLOW_CODE).append("=").append(code);
243249
// state
244-
codeFlowParams.append("&").append(OidcConstants.CODE_FLOW_STATE).append("=")
245-
.append(queryParams.get(OidcConstants.CODE_FLOW_STATE));
250+
String state = queryParams.get(OidcConstants.CODE_FLOW_STATE);
251+
if (state == null) {
252+
LOG.error("State query parameter is missing");
253+
context.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.code());
254+
context.response().end();
255+
return;
256+
}
257+
String oidcProxyState = OidcUtils.removeCookie(context, oidcTenantConfig, OIDC_PROXY_STATE_COOKIE);
258+
if (oidcProxyState == null) {
259+
LOG.error("Proxy state cookie is missing or could not be retrieved");
260+
context.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.code());
261+
context.response().end();
262+
return;
263+
}
264+
if (!oidcProxyState.equals(state)) {
265+
LOG.error("State query parameter is not equal to the proxy state");
266+
context.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.code());
267+
context.response().end();
268+
return;
269+
}
270+
271+
codeFlowParams.append("&").append(OidcConstants.CODE_FLOW_STATE).append("=").append(state);
272+
246273
} else {
247274
String error = queryParams.get(OidcConstants.CODE_FLOW_ERROR);
248275
codeFlowParams.append(OidcConstants.CODE_FLOW_ERROR).append("=").append(error);
@@ -548,7 +575,7 @@ private String getClientId(String providedClientId) {
548575
}
549576

550577
private String getRedirectUri(RoutingContext context, String redirectUri) {
551-
if (oidcTenantConfig.authentication.redirectPath.isPresent()) {
578+
if (localAuthorizationCodeFlowRedirect) {
552579
return buildUri(context, oidcTenantConfig.authentication.redirectPath.get());
553580
} else {
554581
return redirectUri;

0 commit comments

Comments
 (0)