KEYCLOAK-18112 Token introspection of the revoked refresh token

This commit is contained in:
Michito Okai 2021-05-14 15:07:19 +09:00 committed by Marek Posolda
parent 2bf727d408
commit bc6a746780
2 changed files with 133 additions and 19 deletions

View file

@ -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 {

View file

@ -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);
}
}