Always include offline_access scope when refreshing with offline token

Closes #27878

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-03-20 12:19:42 +01:00 committed by Marek Posolda
parent 99478887a4
commit 939420cea1
2 changed files with 37 additions and 15 deletions

View file

@ -414,7 +414,7 @@ public class TokenManager {
//if scope parameter is not null, remove every scope that is not part of scope parameter //if scope parameter is not null, remove every scope that is not part of scope parameter
if (scopeParameter != null && ! scopeParameter.isEmpty()) { if (scopeParameter != null && ! scopeParameter.isEmpty()) {
Set<String> scopeParamScopes = Arrays.stream(scopeParameter.split(" ")).collect(Collectors.toSet()); Set<String> scopeParamScopes = Arrays.stream(scopeParameter.split(" ")).collect(Collectors.toSet());
oldTokenScope = Arrays.stream(oldTokenScope.split(" ")).filter(sc -> scopeParamScopes.contains(sc)) oldTokenScope = Arrays.stream(oldTokenScope.split(" ")).filter(sc -> scopeParamScopes.contains(sc) || sc.equals(OAuth2Constants.OFFLINE_ACCESS))
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
} }

View file

@ -260,7 +260,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
assertTrue(tokenResponse.getScope().contains(OAuth2Constants.OFFLINE_ACCESS)); assertTrue(tokenResponse.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId, false); String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
// Change offset to very big value to ensure offline session expires // Change offset to very big value to ensure offline session expires
setTimeOffset(3000000); setTimeOffset(3000000);
@ -281,7 +281,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
} }
private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString, private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
final String sessionId, String userId, boolean scopeParameterExist) { final String sessionId, String userId) {
// Change offset to big value to ensure userSession expired // Change offset to big value to ensure userSession expired
setTimeOffset(99999); setTimeOffset(99999);
assertFalse(oldToken.isActive()); assertFalse(oldToken.isActive());
@ -306,7 +306,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId()); Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
// scope parameter contains "offline_access" if not filter via scope parameter // scope parameter contains "offline_access" if not filter via scope parameter
assertTrue(scopeParameterExist ? !refreshedToken.getScope().contains(OAuth2Constants.OFFLINE_ACCESS) : refreshedToken.getScope().contains(OAuth2Constants.OFFLINE_ACCESS)); assertTrue(refreshedToken.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
// Assert refresh token scope parameter contains "offline_access" // Assert refresh token scope parameter contains "offline_access"
assertTrue(newRefreshTokenFull.getScope().contains(OAuth2Constants.OFFLINE_ACCESS)); assertTrue(newRefreshTokenFull.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, newRefreshTokenFull.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, newRefreshTokenFull.getType());
@ -358,10 +358,10 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId, false); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
// Assert same token can be refreshed again // Assert same token can be refreshed again
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId, false); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
} }
@Test @Test
@ -393,7 +393,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId, false); String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
RefreshToken offlineToken2 = oauth.parseRefreshToken(offlineTokenString2); RefreshToken offlineToken2 = oauth.parseRefreshToken(offlineTokenString2);
// Assert second refresh with same refresh token will fail // Assert second refresh with same refresh token will fail
@ -442,7 +442,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
// Now retrieve another offline token and verify that previous offline token is still valid // Now retrieve another offline token and verify that previous offline token is still valid
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1"); tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
@ -462,8 +462,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
.assertEvent(); .assertEvent();
// Refresh with both offline tokens is fine // Refresh with both offline tokens is fine
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId, false); testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
} }
@Test @Test
@ -978,7 +978,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
// Now retrieve another offline token and decode that previous offline token is still valid // Now retrieve another offline token and decode that previous offline token is still valid
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1"); tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
@ -998,8 +998,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
.assertEvent(); .assertEvent();
// Refresh with both offline tokens is fine // Refresh with both offline tokens is fine
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId, false); testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
} }
@ -1050,9 +1050,9 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
//refresh token without sending offline_access scope => access token without it and same refresh token //refresh token without sending offline_access scope
oauth.scope("phone"); oauth.scope("phone");
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, true); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
} }
@Test @Test
@ -1289,4 +1289,26 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
client.update(clientRepresentation); client.update(clientRepresentation);
} }
} }
@Test
public void offlineTokenRefreshWithoutOfflineAccessScope() {
ClientManager.realm(adminClient.realm("test")).clientId("offline-client").fullScopeAllowed(false);
try {
oauth.scope("openid " + OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "secret1");
oauth.scope("openid");
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
assertEquals(200, response.getStatusCode());
}
finally {
ClientManager.realm(adminClient.realm("test")).clientId("offline-client").fullScopeAllowed(true);
}
}
} }