diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 54bf9ae689..4fedd8323d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -244,16 +244,23 @@ public class TokenManager { } public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException { + return verifyRefreshToken(realm, encodedRefreshToken, true); + } + + public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken, boolean checkExpiration) throws OAuthErrorException { try { RefreshToken refreshToken = toRefreshToken(realm, encodedRefreshToken); - if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired"); + if (checkExpiration) { + if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired"); + } + + if (refreshToken.getIssuedAt() < realm.getNotBefore()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token"); + } } - if (refreshToken.getIssuedAt() < realm.getNotBefore()) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token"); - } return refreshToken; } catch (JWSInputException e) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 3f8d32aace..85fea77501 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -187,7 +187,7 @@ public class LogoutEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST); } try { - RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken); + RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken, false); UserSessionModel userSessionModel = session.sessions().getUserSession(realm, token.getSessionState()); if (userSessionModel != null) { logout(userSessionModel); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java new file mode 100644 index 0000000000..38dde746dc --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.oauth; + +import org.apache.http.HttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.common.util.Time; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.RealmBuilder; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; + +/** + * @author Stian Thorgersen + */ +public class LogoutTest extends AbstractKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Override + public void beforeAbstractKeycloakTest() throws Exception { + super.beforeAbstractKeycloakTest(); + } + + @Before + public void clientConfiguration() { + ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true); + } + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener(); + + testRealms.add(realm.build()); + } + + @Test + public void postLogout() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.clientSessionState("client-session"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = tokenResponse.getRefreshToken(); + + HttpResponse response = oauth.doLogout(refreshTokenString, "password"); + assertEquals(204, response.getStatusLine().getStatusCode()); + + assertNotNull(testingClient.testApp().getAdminLogoutAction()); + } + + @Test + public void postLogoutExpiredRefreshToken() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.clientSessionState("client-session"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = tokenResponse.getRefreshToken(); + + adminClient.realm("test").update(RealmBuilder.create().notBefore(Time.currentTime() + 1).build()); + + // Logout should succeed with expired refresh token, see KEYCLOAK-3302 + HttpResponse response = oauth.doLogout(refreshTokenString, "password"); + assertEquals(204, response.getStatusLine().getStatusCode()); + + assertNotNull(testingClient.testApp().getAdminLogoutAction()); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java index c9746d2ca6..3653065d66 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java @@ -132,6 +132,11 @@ public class RealmBuilder { return this; } + public RealmBuilder notBefore(int i) { + rep.setNotBefore(i); + return this; + } + public RealmBuilder otpLookAheadWindow(int i) { rep.setOtpPolicyLookAheadWindow(i); return this; diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json index 542707551d..11e25d3f6f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json @@ -105,7 +105,7 @@ "redirectUris": [ "http://localhost:8180/auth/realms/master/app/auth/*" ], - "adminUrl": "http://localhost:8180/auth/realms/master/app/logout", + "adminUrl": "http://localhost:8180/auth/realms/master/app/admin", "secret": "password" }, {