From bc6a746780ab797519886c4448e12edebaac9cf5 Mon Sep 17 00:00:00 2001 From: Michito Okai Date: Fri, 14 May 2021 15:07:19 +0900 Subject: [PATCH] KEYCLOAK-18112 Token introspection of the revoked refresh token --- .../keycloak/protocol/oidc/TokenManager.java | 74 +++++++++++++----- .../oauth/TokenIntrospectionTest.java | 78 +++++++++++++++++++ 2 files changed, 133 insertions(+), 19 deletions(-) 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 83713f2412..9969ee86b7 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -263,6 +263,13 @@ public class TokenManager { valid = false; } + String tokenType = token.getType(); + if (realm.isRevokeRefreshToken() + && (tokenType.equals(TokenUtil.TOKEN_TYPE_REFRESH) || tokenType.equals(TokenUtil.TOKEN_TYPE_OFFLINE)) + && !validateTokenReuseForIntrospection(session, realm, token)) { + return false; + } + if (valid) { int currentTime = Time.currentTime(); userSession.setLastSessionRefresh(currentTime); @@ -276,6 +283,25 @@ public class TokenManager { return valid; } + private boolean validateTokenReuseForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token) { + UserSessionModel userSession = null; + if (token.getType().equals(TokenUtil.TOKEN_TYPE_REFRESH)) { + userSession = session.sessions().getUserSession(realm, token.getSessionState()); + } else { + UserSessionManager sessionManager = new UserSessionManager(session); + userSession = sessionManager.findOfflineUserSession(realm, token.getSessionState()); + } + + ClientModel client = realm.getClientByClientId(token.getIssuedFor()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); + + try { + return validateTokenReuse(session, realm, token, clientSession, false); + } catch (OAuthErrorException e) { + return false; + } + } + private boolean isUserValid(KeycloakSession session, RealmModel realm, AccessToken token, UserModel user) { if (user == null) { return false; @@ -331,7 +357,7 @@ public class TokenManager { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token. Token client and authorized client don't match"); } - validateTokenReuse(session, realm, refreshToken, validation); + validateTokenReuseForRefresh(session, realm, refreshToken, validation); int currentTime = Time.currentTime(); clientSession.setTimestamp(currentTime); @@ -373,33 +399,43 @@ public class TokenManager { return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType())); } - private void validateTokenReuse(KeycloakSession session, RealmModel realm, RefreshToken refreshToken, - TokenValidation validation) throws OAuthErrorException { + private void validateTokenReuseForRefresh(KeycloakSession session, RealmModel realm, RefreshToken refreshToken, + TokenValidation validation) throws OAuthErrorException { if (realm.isRevokeRefreshToken()) { AuthenticatedClientSessionModel clientSession = validation.clientSessionCtx.getClientSession(); - - int clusterStartupTime = session.getProvider(ClusterProvider.class).getClusterStartupTime(); - - if (clientSession.getCurrentRefreshToken() != null && - !refreshToken.getId().equals(clientSession.getCurrentRefreshToken()) && - refreshToken.getIssuedAt() < clientSession.getTimestamp() && - clusterStartupTime <= clientSession.getTimestamp()) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token"); + if (validateTokenReuse(session, realm, refreshToken, clientSession, true)) { + int currentCount = clientSession.getCurrentRefreshTokenUseCount(); + clientSession.setCurrentRefreshTokenUseCount(currentCount + 1); } + } + } + private boolean validateTokenReuse(KeycloakSession session, RealmModel realm, AccessToken refreshToken, + AuthenticatedClientSessionModel clientSession, boolean refreshFlag) throws OAuthErrorException { + int clusterStartupTime = session.getProvider(ClusterProvider.class).getClusterStartupTime(); - if (!refreshToken.getId().equals(clientSession.getCurrentRefreshToken())) { + if (clientSession.getCurrentRefreshToken() != null + && !refreshToken.getId().equals(clientSession.getCurrentRefreshToken()) + && refreshToken.getIssuedAt() < clientSession.getTimestamp() + && clusterStartupTime <= clientSession.getTimestamp()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token"); + } + + if (!refreshToken.getId().equals(clientSession.getCurrentRefreshToken())) { + if (refreshFlag) { clientSession.setCurrentRefreshToken(refreshToken.getId()); clientSession.setCurrentRefreshTokenUseCount(0); + } else { + return true; } - - int currentCount = clientSession.getCurrentRefreshTokenUseCount(); - if (currentCount > realm.getRefreshTokenMaxReuse()) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Maximum allowed refresh token reuse exceeded", - "Maximum allowed refresh token reuse exceeded"); - } - clientSession.setCurrentRefreshTokenUseCount(currentCount + 1); } + + int currentCount = clientSession.getCurrentRefreshTokenUseCount(); + if (currentCount > realm.getRefreshTokenMaxReuse()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Maximum allowed refresh token reuse exceeded", + "Maximum allowed refresh token reuse exceeded"); + } + return true; } public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm, ClientModel client, HttpRequest request, String encodedRefreshToken, boolean checkExpiration) throws OAuthErrorException { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java index b4bd43537a..aab01071d7 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java @@ -471,6 +471,70 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { assertEquals(OAuthErrorException.INVALID_REQUEST, errorRep.getError()); } + @Test + public void testIntrospectRevokeRefreshToken() throws Exception { + RealmRepresentation realm = adminClient.realm(oauth.getRealm()).toRepresentation(); + realm.setRevokeRefreshToken(true); + adminClient.realm(oauth.getRealm()).update(realm); + try { + JsonNode jsonNode = introspectRevokedToken(); + assertFalse(jsonNode.get("active").asBoolean()); + } finally { + realm.setRevokeRefreshToken(false); + adminClient.realm(oauth.getRealm()).update(realm); + } + } + + @Test + public void testIntrospectRevokeOfflineToken() throws Exception { + RealmRepresentation realm = adminClient.realm(oauth.getRealm()).toRepresentation(); + realm.setRevokeRefreshToken(true); + adminClient.realm(oauth.getRealm()).update(realm); + try { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + JsonNode jsonNode = introspectRevokedToken(); + assertFalse(jsonNode.get("active").asBoolean()); + } finally { + realm.setRevokeRefreshToken(false); + adminClient.realm(oauth.getRealm()).update(realm); + } + } + + @Test + public void testIntrospectRefreshTokenAfterRefreshTokenRequest() throws Exception { + RealmRepresentation realm = adminClient.realm(oauth.getRealm()).toRepresentation(); + realm.setRevokeRefreshToken(true); + realm.setRefreshTokenMaxReuse(1); + adminClient.realm(oauth.getRealm()).update(realm); + try { + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); + String oldRefreshToken = accessTokenResponse.getRefreshToken(); + + setTimeOffset(1); + + accessTokenResponse = oauth.doRefreshTokenRequest(oldRefreshToken, "password"); + + accessTokenResponse = oauth.doRefreshTokenRequest(oldRefreshToken, "password"); + String newRefreshToken = accessTokenResponse.getRefreshToken(); + String tokenResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", + newRefreshToken); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(tokenResponse); + assertTrue(jsonNode.get("active").asBoolean()); + + accessTokenResponse = oauth.doRefreshTokenRequest(newRefreshToken, "password"); + tokenResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", oldRefreshToken); + jsonNode = objectMapper.readTree(tokenResponse); + assertFalse(jsonNode.get("active").asBoolean()); + } finally { + realm.setRevokeRefreshToken(false); + realm.setRefreshTokenMaxReuse(0); + adminClient.realm(oauth.getRealm()).update(realm); + } + } + private String introspectAccessTokenWithDuplicateParams(String clientId, String clientSecret, String tokenToIntrospect) { HttpPost post = new HttpPost(oauth.getTokenIntrospectionUrl()); @@ -501,4 +565,18 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { throw new RuntimeException("Failed to retrieve access token", e); } } + + private JsonNode introspectRevokedToken() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); + String stringRefreshToken = accessTokenResponse.getRefreshToken(); + + accessTokenResponse = oauth.doRefreshTokenRequest(stringRefreshToken, "password"); + + String tokenResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", + stringRefreshToken); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readTree(tokenResponse); + } }