diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index fa450fa3e5..6c1d0086c5 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -99,6 +99,8 @@ public class TokenRevocationEndpoint { formParams = request.getDecodedFormParameters(); + checkParameterDuplicated(formParams); + try { session.clientPolicy().triggerOnEvent(new TokenRevokeContext(formParams)); } catch (ClientPolicyException cpe) { @@ -219,6 +221,14 @@ public class TokenRevocationEndpoint { event.user(user); } + private void checkParameterDuplicated(MultivaluedMap formParams) { + for (String key : formParams.keySet()) { + if (formParams.get(key).size() != 1) { + throw new CorsErrorResponseException(cors, Errors.INVALID_REQUEST, "duplicated parameter", Response.Status.BAD_REQUEST); + } + } + } + private void revokeClient() { session.users().revokeConsentForClient(realm, user.getId(), client.getId()); if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType())) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java index f6eee7d7be..fe7ad3308f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenRevocationTest.java @@ -25,19 +25,30 @@ import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.ws.rs.core.Response.Status; +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.representations.oidc.TokenMetadataRepresentation; @@ -211,6 +222,22 @@ public class TokenRevocationTest extends AbstractKeycloakTest { isTokenDisabled(tokenResponse, "test-app"); } + // KEYCLOAK-17300 + @Test + public void testRevokeRequestParamsMoreThanOnce() throws Exception { + oauth.clientId("test-app"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", + "password"); + + isTokenEnabled(tokenResponse, "test-app"); + + String revokeResponse = doTokenRevokeWithDuplicateParams(tokenResponse.getRefreshToken(), "refresh_token", "password"); + + OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(revokeResponse, OAuth2ErrorRepresentation.class); + assertEquals("duplicated parameter", errorRep.getErrorDescription()); + assertEquals(OAuthErrorException.INVALID_REQUEST, errorRep.getError()); + } + private AccessTokenResponse login(String clientId, String username, String password) { oauth.clientId(clientId); oauth.openLoginForm(); @@ -248,4 +275,30 @@ public class TokenRevocationTest extends AbstractKeycloakTest { TokenMetadataRepresentation rep = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class); assertFalse(rep.isActive()); } + + private String doTokenRevokeWithDuplicateParams(String token, String tokenTypeHint, String clientSecret) + throws IOException { + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + HttpPost post = new HttpPost(oauth.getTokenRevocationUrl()); + + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair("token", token)); + parameters.add(new BasicNameValuePair("token", "foo")); + parameters.add(new BasicNameValuePair("token_type_hint", tokenTypeHint)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId())); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, clientSecret)); + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + post.setEntity(formEntity); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + client.execute(post).getEntity().writeTo(out); + return new String(out.toByteArray()); + } + } }