Skip to content

Commit

Permalink
Support OIDC code flow custom 'code' query parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Oct 21, 2020
1 parent 9570694 commit 870f460
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,32 @@ public static class Authentication {
@ConfigItem(defaultValue = "false")
public boolean forceRedirectHttpsScheme;

/**
* The state cookie is created at the start of the code flow and verified after the user has been redirected back to
* the application. The authentication fails if the cookie is not available or its value is not equal to the 'state'
* query parameter's value.
*
* If an original user request URL contains the custom query parameters such as 'code' or 'state' then the only way
* to determine if they are part of the code flow redirect URL or not is by checking the state cookie availability.
* If the cookie is not available then these custom parameters may not be related to the code flow.
*
* However the state cookie may also be unavailable if either 'cookie-path' or 'cookie-domain' properties
* have not been set correctly, therefore, the authentication is rejected by default.
*
* If you need to support custom 'code' or 'state' query parameters then enable this property to allow
* a user authentication challenge if no 'state' cookie is available.
*
* This property may also be enabled if you prefer to re-challenge a user if no 'state' cookie is available
* after the redirect as opposed to failing the authentication.
*
* If you do enable this property then make sure the state cookie is not lost during the redirects
* by setting the 'cookie-path' and 'cookie-domain' properties correctly otherwise the browser may fail
* with the redirection loop if the user has already successfully authenticated and OpenId Connect provider
* has not been configured to challenge the already authenticated users.
*/
@ConfigItem(defaultValue = "false")
public boolean challengeWithoutStateCookie;

/**
* List of scopes
*/
Expand Down Expand Up @@ -877,6 +903,14 @@ public void setSessionAgeExtension(Duration sessionAgeExtension) {
this.sessionAgeExtension = sessionAgeExtension;
}

public boolean isChallengeWithoutStateCookie() {
return challengeWithoutStateCookie;
}

public void setChallengeWithoutStateCookie(boolean challengeWithoutStateCookie) {
this.challengeWithoutStateCookie = challengeWithoutStateCookie;
}

}

@ConfigGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ public Uni<ChallengeData> getChallenge(RoutingContext context) {

private Uni<SecurityIdentity> performCodeFlow(IdentityProviderManager identityProviderManager,
RoutingContext context, DefaultTenantConfigResolver resolver) {
JsonObject params = new JsonObject();

String code = context.request().getParam("code");
if (code == null) {
Expand Down Expand Up @@ -278,13 +277,18 @@ private Uni<SecurityIdentity> performCodeFlow(IdentityProviderManager identityPr
// Local redirect restoring the original request path, the state cookie is no longer needed
removeCookie(context, configContext, getStateCookieName(configContext));
}
} else if (configContext.oidcConfig.authentication.isChallengeWithoutStateCookie()) {
LOG.debug("The state cookie is missing after a redirect from IDP, re-authentication is required");
return Uni.createFrom().optional(Optional.empty());
} else {
// State cookie must be available to minimize the risk of CSRF
LOG.debug("The state cookie is missing after a redirect from IDP");
LOG.debug("The state cookie is missing after a redirect from IDP, authentication has failed");
return Uni.createFrom().failure(new AuthenticationCompletionException());
}

// Code grant request
JsonObject params = new JsonObject();

// 'code': the code grant value returned from IDP
params.put("code", code);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String resolve(RoutingContext context) {
return "tenant-logout";
}

if (path.contains("tenant-query")) {
return "tenant-query";
}

if (path.contains("tenant-listener")) {
return "tenant-listener";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package io.quarkus.it.keycloak;

import java.util.List;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;

import org.eclipse.microprofile.jwt.JsonWebToken;

Expand Down Expand Up @@ -165,7 +168,18 @@ public String getRefreshTokenTenantListener() {

@GET
@Path("refresh-query")
public String getRefreshTokenQuery(@QueryParam("a") String aValue) {
return getRefreshToken() + ":" + aValue;
public String getRefreshTokenQuery(@Context UriInfo uriInfo) {
StringBuilder sb = new StringBuilder();
sb.append(getRefreshToken());
for (List<String> values : uriInfo.getQueryParameters().values()) {
sb.append(":").append(values.get(0));
}
return sb.toString();
}

@GET
@Path("refresh-query/tenant-query")
public String getRefreshTokenCodeStateQuery(@Context UriInfo uriInfo) {
return getRefreshTokenQuery(uriInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ quarkus.oidc.authentication.cookie-domain=localhost
quarkus.oidc.authentication.extra-params.max-age=60
quarkus.oidc.application-type=web-app

quarkus.oidc.tenant-query.auth-server-url=${keycloak.url}/realms/quarkus
quarkus.oidc.tenant-query.client-id=quarkus-app
quarkus.oidc.tenant-query.credentials.secret=secret
quarkus.oidc.tenant-query.authentication.scopes=profile,email,phone
quarkus.oidc.tenant-query.authentication.cookie-path=/
quarkus.oidc.tenant-query.authentication.challenge-without-state-cookie=true
quarkus.oidc.tenant-query.application-type=web-app

# Tenant listener configuration for testing that the login event has been captured
quarkus.oidc.tenant-listener.auth-server-url=${keycloak.url}/realms/quarkus
quarkus.oidc.tenant-listener.client-id=quarkus-app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ public void testAccessAndRefreshTokenInjectionWithoutIndexHtmlAndListener() thro
}

@Test
public void testAccessAndRefreshTokenInjectionWithoutIndexHtmlWithQuery() throws Exception {
public void testAccessAndRefreshTokenInjectionWithQuery() throws Exception {
try (final WebClient webClient = createWebClient()) {
HtmlPage page = webClient.getPage("http://localhost:8081/web-app/refresh-query?a=aValue");
assertEquals("/web-app/refresh-query?a=aValue", getStateCookieSavedPath(webClient, null));
Expand All @@ -663,6 +663,27 @@ public void testAccessAndRefreshTokenInjectionWithoutIndexHtmlWithQuery() throws
}
}

@Test
public void testAccessAndRefreshTokenInjectionWithCodeAndStateQuery() throws Exception {
try (final WebClient webClient = createWebClient()) {
HtmlPage page = webClient.getPage("http://localhost:8081/web-app/refresh-query/tenant-query?state=NY&code=10001");
assertEquals("?state=NY&code=10001",
getStateCookieSavedPath(webClient, "tenant-query"));

assertEquals("Log in to quarkus", page.getTitleText());

HtmlForm loginForm = page.getForms().get(0);

loginForm.getInputByName("username").setValueAttribute("alice");
loginForm.getInputByName("password").setValueAttribute("alice");

page = loginForm.getInputByName("login").click();

assertEquals("RT injected:10001:NY", page.getBody().asText());
webClient.getCookieManager().clearCookies();
}
}

@Test
public void testXhrRequest() throws IOException, InterruptedException {
try (final WebClient webClient = createWebClient()) {
Expand Down

0 comments on commit 870f460

Please sign in to comment.