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 07aec65e6f..806b173979 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -120,15 +120,6 @@ public class TokenManager { } public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken, HttpHeaders headers) throws OAuthErrorException { - UserModel user = session.users().getUserById(oldToken.getSubject(), realm); - if (user == null) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); - } - - if (!user.isEnabled()) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled"); - } - UserSessionModel userSession = null; if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { @@ -156,6 +147,15 @@ public class TokenManager { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found"); } + UserModel user = userSession.getUser(); + if (user == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); + } + + if (!user.isEnabled()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled"); + } + ClientModel client = session.getContext().getClient(); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java index 8666e04aec..0601879004 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java @@ -28,6 +28,8 @@ import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.HttpErrorException; import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.RefreshToken; import org.keycloak.representations.UserInfo; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; @@ -41,6 +43,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.UserInfoClientUtil; +import org.keycloak.testsuite.util.UserManager; import javax.ws.rs.client.Client; import javax.ws.rs.core.Response; @@ -49,6 +52,8 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrationTest { @@ -77,6 +82,14 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati return response; } + public OIDCClientRepresentation createPairwise() throws ClientRegistrationException { + // Create pairwise client + OIDCClientRepresentation clientRep = createRep(); + clientRep.setSubjectType("pairwise"); + OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep); + return pairwiseClient; + } + private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) { try { @@ -351,6 +364,109 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati } } + @Test + public void refreshPairwiseToken() throws Exception { + // Create pairwise client + OIDCClientRepresentation pairwiseClient = createPairwise(); + + // Login to pairwise client + OAuthClient.AccessTokenResponse accessTokenResponse = login(pairwiseClient, "test-user@localhost", "password"); + + // Verify tokens + oauth.verifyRefreshToken(accessTokenResponse.getAccessToken()); + IDToken idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken()); + oauth.verifyRefreshToken(accessTokenResponse.getRefreshToken()); + + // Refresh token + OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret()); + + // Verify refreshed tokens + oauth.verifyToken(refreshTokenResponse.getAccessToken()); + RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshTokenResponse.getRefreshToken()); + IDToken refreshedIdToken = oauth.verifyIDToken(refreshTokenResponse.getIdToken()); + + // If an ID Token is returned as a result of a token refresh request, the following requirements apply: + // its iss Claim Value MUST be the same as in the ID Token issued when the original authentication occurred + Assert.assertEquals(idToken.getIssuer(), refreshedRefreshToken.getIssuer()); + + // its sub Claim Value MUST be the same as in the ID Token issued when the original authentication occurred + Assert.assertEquals(idToken.getSubject(), refreshedRefreshToken.getSubject()); + + // its iat Claim MUST represent the time that the new ID Token is issued + Assert.assertEquals(refreshedIdToken.getIssuedAt(), refreshedRefreshToken.getIssuedAt()); + + // its aud Claim Value MUST be the same as in the ID Token issued when the original authentication occurred + Assert.assertArrayEquals(idToken.getAudience(), refreshedRefreshToken.getAudience()); + + // if the ID Token contains an auth_time Claim, its value MUST represent the time of the original authentication + // - not the time that the new ID token is issued + Assert.assertEquals(idToken.getAuthTime(), refreshedIdToken.getAuthTime()); + + // its azp Claim Value MUST be the same as in the ID Token issued when the original authentication occurred; if + // no azp Claim was present in the original ID Token, one MUST NOT be present in the new ID Token + Assert.assertEquals(idToken.getIssuedFor(), refreshedIdToken.getIssuedFor()); + } + + @Test + public void refreshPairwiseTokenDeletedUser() throws Exception { + String userId = createUser(REALM_NAME, "delete-me@localhost", "password"); + + // Create pairwise client + OIDCClientRepresentation pairwiseClient = createPairwise(); + + // Login to pairwise client + oauth.clientId(pairwiseClient.getClientId()); + oauth.clientId(pairwiseClient.getClientId()); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("delete-me@localhost", "password"); + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret()); + + assertEquals(200, accessTokenResponse.getStatusCode()); + + // Delete user + adminClient.realm(REALM_NAME).users().delete(userId); + + OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret()); + assertEquals(400, refreshTokenResponse.getStatusCode()); + assertEquals("invalid_grant", refreshTokenResponse.getError()); + assertNull(refreshTokenResponse.getAccessToken()); + assertNull(refreshTokenResponse.getIdToken()); + assertNull(refreshTokenResponse.getRefreshToken()); + } + + @Test + public void refreshPairwiseTokenDisabledUser() throws Exception { + createUser(REALM_NAME, "disable-me@localhost", "password"); + + // Create pairwise client + OIDCClientRepresentation pairwiseClient = createPairwise(); + + // Login to pairwise client + oauth.clientId(pairwiseClient.getClientId()); + oauth.clientId(pairwiseClient.getClientId()); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("disable-me@localhost", "password"); + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret()); + assertEquals(200, accessTokenResponse.getStatusCode()); + + try { + UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(false); + + OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret()); + assertEquals(400, refreshTokenResponse.getStatusCode()); + assertEquals("invalid_grant", refreshTokenResponse.getError()); + assertNull(refreshTokenResponse.getAccessToken()); + assertNull(refreshTokenResponse.getIdToken()); + assertNull(refreshTokenResponse.getRefreshToken()); + } finally { + UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(true); + } + } + + private OAuthClient.AccessTokenResponse login(OIDCClientRepresentation client, String username, String password) { + oauth.clientId(client.getClientId()); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(username, password); + return oauth.doAccessTokenRequest(loginResponse.getCode(), client.getClientSecret()); + } + private String getPayload(String token) { String payloadBase64 = token.split("\\.")[1]; return new String(Base64.getDecoder().decode(payloadBase64)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 208e6e3a9e..af6cbe80aa 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -27,15 +27,18 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; 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 org.keycloak.testsuite.util.RealmManager; +import org.keycloak.testsuite.util.UserManager; import org.keycloak.util.BasicAuthHelper; import javax.ws.rs.client.Client; @@ -488,6 +491,61 @@ public class RefreshTokenTest extends AbstractKeycloakTest { } + @Test + public void refreshTokenUserDisabled() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = response.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + events.expectCodeToToken(codeId, sessionId).assertEvent(); + + try { + UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(false); + response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_grant", response.getError()); + + events.expectRefresh(refreshToken.getId(), sessionId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent(); + } finally { + UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(true); + } + } + + @Test + public void refreshTokenUserDeleted() throws Exception { + String userId = createUser("test", "temp-user@localhost", "password"); + oauth.doLogin("temp-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = response.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent(); + + UserManager.realm(adminClient.realm("test")).username("temp-user@localhost").enabled(false); + response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_grant", response.getError()); + + events.expectRefresh(refreshToken.getId(), sessionId).user(userId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent(); + } + protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) { String header = BasicAuthHelper.createHeader("test-app", "password"); Form form = new Form(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java index ed797b5baa..6bd3789696 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java @@ -61,6 +61,12 @@ public class UserManager { userResource.update(user); } + public void enabled(Boolean enabled) { + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(enabled); + userResource.update(user); + } + private UserRepresentation initializeRequiredActions() { UserRepresentation user = userResource.toRepresentation();