Merge pull request #3029 from mposolda/master
KEYCLOAK-3281 OIDC 'state' parameter is url-encoded twice when respon…
This commit is contained in:
commit
16184d1e9d
4 changed files with 86 additions and 10 deletions
|
@ -125,7 +125,7 @@ public abstract class OIDCRedirectUriBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
|
public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
|
||||||
params.put(paramName, Encode.encodeQueryParam(paramValue));
|
params.put(paramName, paramValue);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.rest;
|
package org.keycloak.testsuite.rest;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -25,7 +27,6 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||||
import org.keycloak.services.resource.RealmResourceProvider;
|
import org.keycloak.services.resource.RealmResourceProvider;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.testsuite.events.EventsListenerProvider;
|
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
@ -34,8 +35,10 @@ import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -110,6 +113,35 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Produces(MediaType.TEXT_HTML)
|
||||||
|
@Path("/{action}")
|
||||||
|
public String post(@PathParam("action") String action) {
|
||||||
|
String title = "APP_REQUEST";
|
||||||
|
if (action.equals("auth")) {
|
||||||
|
title = "AUTH_RESPONSE";
|
||||||
|
} else if (action.equals("logout")) {
|
||||||
|
title = "LOGOUT_REQUEST";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<html><head><title>" + title + "</title></head><body>");
|
||||||
|
|
||||||
|
sb.append("<b>Form parameters: </b><br>");
|
||||||
|
HttpRequest request = ResteasyProviderFactory.getContextData(HttpRequest.class);
|
||||||
|
MultivaluedMap<String, String> formParams = request.getDecodedFormParameters();
|
||||||
|
for (String paramName : formParams.keySet()) {
|
||||||
|
sb.append(paramName).append(": ").append("<span id=\"").append(paramName).append("\">").append(formParams.getFirst(paramName)).append("</span><br>");
|
||||||
|
}
|
||||||
|
sb.append("<br>");
|
||||||
|
|
||||||
|
UriBuilder base = UriBuilder.fromUri("http://localhost:8180/auth");
|
||||||
|
sb.append("<a href=\"" + RealmsResource.accountUrl(base).build("test").toString() + "\" id=\"account\">account</a>");
|
||||||
|
|
||||||
|
sb.append("</body></html>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.TEXT_HTML)
|
@Produces(MediaType.TEXT_HTML)
|
||||||
@Path("/{action}")
|
@Path("/{action}")
|
||||||
|
|
|
@ -36,9 +36,6 @@ import org.keycloak.admin.client.Keycloak;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.constants.AdapterConstants;
|
import org.keycloak.constants.AdapterConstants;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
|
||||||
import org.keycloak.jose.jwk.JWKBuilder;
|
|
||||||
import org.keycloak.jose.jwk.JWKParser;
|
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
@ -99,6 +96,10 @@ public class OAuthClient {
|
||||||
|
|
||||||
private String maxAge;
|
private String maxAge;
|
||||||
|
|
||||||
|
private String responseType = OAuth2Constants.CODE;
|
||||||
|
|
||||||
|
private String responseMode;
|
||||||
|
|
||||||
private Map<String, PublicKey> publicKeys = new HashMap<>();
|
private Map<String, PublicKey> publicKeys = new HashMap<>();
|
||||||
|
|
||||||
public void init(Keycloak adminClient, WebDriver driver) {
|
public void init(Keycloak adminClient, WebDriver driver) {
|
||||||
|
@ -486,7 +487,12 @@ public class OAuthClient {
|
||||||
|
|
||||||
public String getLoginFormUrl() {
|
public String getLoginFormUrl() {
|
||||||
UriBuilder b = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(AUTH_SERVER_ROOT));
|
UriBuilder b = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(AUTH_SERVER_ROOT));
|
||||||
b.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE);
|
if (responseType != null) {
|
||||||
|
b.queryParam(OAuth2Constants.RESPONSE_TYPE, responseType);
|
||||||
|
}
|
||||||
|
if (responseMode != null) {
|
||||||
|
b.queryParam(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode);
|
||||||
|
}
|
||||||
if (clientId != null) {
|
if (clientId != null) {
|
||||||
b.queryParam(OAuth2Constants.CLIENT_ID, clientId);
|
b.queryParam(OAuth2Constants.CLIENT_ID, clientId);
|
||||||
}
|
}
|
||||||
|
@ -598,6 +604,16 @@ public class OAuthClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OAuthClient responseType(String responseType) {
|
||||||
|
this.responseType = responseType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthClient responseMode(String responseMode) {
|
||||||
|
this.responseMode = responseMode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRealm() {
|
public String getRealm() {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,14 @@ package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
@ -65,15 +67,21 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void clientConfiguration() {
|
||||||
|
oauth.responseType(OAuth2Constants.CODE);
|
||||||
|
oauth.responseMode(null);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authorizationRequest() throws IOException {
|
public void authorizationRequest() throws IOException {
|
||||||
oauth.state("mystate");
|
oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk");
|
||||||
|
|
||||||
OAuthClient.AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password");
|
OAuthClient.AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
Assert.assertTrue(response.isRedirected());
|
Assert.assertTrue(response.isRedirected());
|
||||||
Assert.assertNotNull(response.getCode());
|
Assert.assertNotNull(response.getCode());
|
||||||
assertEquals("mystate", response.getState());
|
assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", response.getState());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
|
||||||
testingClient.testing().verifyCode("test", response.getCode());
|
testingClient.testing().verifyCode("test", response.getCode());
|
||||||
|
@ -137,8 +145,8 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authorizationRequestImplicitFlowDisabled() throws IOException {
|
public void authorizationRequestImplicitFlowDisabled() throws IOException {
|
||||||
|
oauth.responseType("token id_token");
|
||||||
UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
|
UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
|
||||||
b.replaceQueryParam(OAuth2Constants.RESPONSE_TYPE, "token id_token");
|
|
||||||
driver.navigate().to(b.build().toURL());
|
driver.navigate().to(b.build().toURL());
|
||||||
assertEquals("Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.", errorPage.getError());
|
assertEquals("Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.", errorPage.getError());
|
||||||
events.expectLogin().error(Errors.NOT_ALLOWED).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token id_token").assertEvent();
|
events.expectLogin().error(Errors.NOT_ALLOWED).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token id_token").assertEvent();
|
||||||
|
@ -146,13 +154,33 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authorizationRequestInvalidResponseType() throws IOException {
|
public void authorizationRequestInvalidResponseType() throws IOException {
|
||||||
|
oauth.responseType("tokenn");
|
||||||
UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
|
UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
|
||||||
b.replaceQueryParam(OAuth2Constants.RESPONSE_TYPE, "tokenn");
|
|
||||||
driver.navigate().to(b.build().toURL());
|
driver.navigate().to(b.build().toURL());
|
||||||
assertEquals("Invalid parameter: response_type", errorPage.getError());
|
assertEquals("Invalid parameter: response_type", errorPage.getError());
|
||||||
events.expectLogin().error(Errors.INVALID_REQUEST).client((String) null).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "tokenn").assertEvent();
|
events.expectLogin().error(Errors.INVALID_REQUEST).client((String) null).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "tokenn").assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-3281
|
||||||
|
@Test
|
||||||
|
public void authorizationRequestFormPostResponseMode() throws IOException {
|
||||||
|
oauth.responseMode(OIDCResponseMode.FORM_POST.toString().toLowerCase());
|
||||||
|
oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk");
|
||||||
|
oauth.doLoginGrant("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String sources = driver.getPageSource();
|
||||||
|
System.out.println(sources);
|
||||||
|
|
||||||
|
String code = driver.findElement(By.id("code")).getText();
|
||||||
|
String state = driver.findElement(By.id("state")).getText();
|
||||||
|
|
||||||
|
assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", state);
|
||||||
|
|
||||||
|
testingClient.testing().verifyCode("test", code);
|
||||||
|
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
||||||
|
assertCode(codeId, code);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertCode(String expectedCodeId, String actualCode) {
|
private void assertCode(String expectedCodeId, String actualCode) {
|
||||||
String code = testingClient.testing().verifyCode("test", actualCode);
|
String code = testingClient.testing().verifyCode("test", actualCode);
|
||||||
assertEquals(expectedCodeId, code);
|
assertEquals(expectedCodeId, code);
|
||||||
|
|
Loading…
Reference in a new issue