KEYCLOAK-18112 Token introspection of the revoked refresh token
This commit is contained in:
parent
2bf727d408
commit
bc6a746780
2 changed files with 133 additions and 19 deletions
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue