mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Check offline scope is still assigned when performing a refresh
Closes #43734 (cherry picked from commit e0c1f2ee0fd14ba76338d9c2c213d45d0e857450) Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
4cd381edbf
commit
50102e50de
@ -205,6 +205,14 @@ public class AccessTokenIntrospectionProvider<T extends AccessToken> implements
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userSession.isOffline() && !UserSessionUtil.isOfflineAccessGranted(
|
||||
session, userSession.getAuthenticatedClientSessionByClient(client.getId()))) {
|
||||
logger.debugf("Offline session invalid because offline access not granted anymore");
|
||||
eventBuilder.detail(Details.REASON, "Offline session invalid because offline access not granted anymore");
|
||||
eventBuilder.error(Errors.SESSION_EXPIRED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!verifyTokenReuse()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -228,6 +228,10 @@ public class TokenManager {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||
}
|
||||
|
||||
if (userSession.isOffline() && !UserSessionUtil.isOfflineAccessGranted(session, clientSession)) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session invalid because offline access not granted anymore");
|
||||
}
|
||||
|
||||
// Case when offline token is migrated from previous version
|
||||
if (oldTokenScope == null && userSession.isOffline()) {
|
||||
logger.debugf("Migrating offline token of user '%s' for client '%s' of realm '%s'", user.getUsername(), client.getClientId(), realm.getName());
|
||||
|
||||
@ -6,12 +6,14 @@ import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.ImpersonationSessionNote;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@ -189,6 +191,16 @@ public class UserSessionUtil {
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isOfflineAccessGranted(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
|
||||
if (clientSession == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(
|
||||
clientSession, OAuth2Constants.OFFLINE_ACCESS, session);
|
||||
return clientSessionCtx.getClientScopesStream().anyMatch((s -> OAuth2Constants.OFFLINE_ACCESS.equals(s.getName())));
|
||||
}
|
||||
|
||||
private static void attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, ClientModel client) {
|
||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getRealm());
|
||||
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
|
||||
|
||||
@ -164,6 +164,11 @@ public class ClientAttributeUpdater extends ServerResourceUpdater<ClientAttribut
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientAttributeUpdater removeOptionalClientScope(String clientScope) {
|
||||
rep.getOptionalClientScopes().remove(clientScope);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientAttributeUpdater setDirectAccessGrantsEnabled(Boolean directAccessGranted) {
|
||||
rep.setDirectAccessGrantsEnabled(directAccessGranted);
|
||||
return this;
|
||||
|
||||
@ -23,6 +23,7 @@ import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
@ -61,6 +62,7 @@ import org.keycloak.testsuite.ProfileAssume;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
@ -71,6 +73,7 @@ import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
import org.keycloak.testsuite.util.oauth.IntrospectionResponse;
|
||||
import org.keycloak.testsuite.util.oauth.LogoutResponse;
|
||||
import org.keycloak.testsuite.utils.tls.TLSUtils;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
@ -1561,4 +1564,90 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
changeOfflineSessionSettings(false, prevOfflineSession[0], prevOfflineSession[1], prevOfflineSession[2], prevOfflineSession[3]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineRefreshWhenNoOfflineScope() throws Exception {
|
||||
// login to obtain a refresh token
|
||||
oauth.scope("openid " + OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
String code = oauth.parseLoginResponse().getCode();
|
||||
AccessTokenResponse response = oauth.doAccessTokenRequest(code);
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin()
|
||||
.client("offline-client")
|
||||
.detail(Details.REDIRECT_URI, offlineClientAppUri)
|
||||
.assertEvent();
|
||||
|
||||
events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
|
||||
.client("offline-client")
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
.assertEvent();
|
||||
|
||||
// check refresh is successful
|
||||
RefreshToken offlineToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
oauth.scope(null);
|
||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken());
|
||||
assertEquals(200, response.getStatusCode());
|
||||
Assert.assertEquals(0, response.getRefreshExpiresIn());
|
||||
events.expectRefresh(offlineToken.getId(), loginEvent.getSessionId())
|
||||
.client("offline-client")
|
||||
.user(userId)
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
.assertEvent();
|
||||
offlineToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
|
||||
IntrospectionResponse introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response.getAccessToken());
|
||||
assertTrue(introspectionResponse.asJsonNode().get("active").asBoolean());
|
||||
events.expect(EventType.INTROSPECT_TOKEN)
|
||||
.client("offline-client")
|
||||
.session(loginEvent.getSessionId())
|
||||
.assertEvent();
|
||||
|
||||
introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response.getRefreshToken());
|
||||
assertTrue(introspectionResponse.asJsonNode().get("active").asBoolean());
|
||||
events.expect(EventType.INTROSPECT_TOKEN)
|
||||
.client("offline-client")
|
||||
.session(loginEvent.getSessionId())
|
||||
.assertEvent();
|
||||
|
||||
// remove offline scope from the client and perform a second refresh
|
||||
try (ClientAttributeUpdater updater = ClientAttributeUpdater.forClient(adminClient, TEST, "offline-client")
|
||||
.removeOptionalClientScope("offline_access").update()) {
|
||||
|
||||
introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response.getAccessToken());
|
||||
assertFalse(introspectionResponse.asJsonNode().get("active").asBoolean());
|
||||
events.expect(EventType.INTROSPECT_TOKEN_ERROR)
|
||||
.client("offline-client")
|
||||
.session(loginEvent.getSessionId())
|
||||
.error(Errors.SESSION_EXPIRED)
|
||||
.detail(Details.REASON, "Offline session invalid because offline access not granted anymore")
|
||||
.assertEvent();
|
||||
|
||||
introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response.getRefreshToken());
|
||||
assertFalse(introspectionResponse.asJsonNode().get("active").asBoolean());
|
||||
events.expect(EventType.INTROSPECT_TOKEN_ERROR)
|
||||
.client("offline-client")
|
||||
.session(loginEvent.getSessionId())
|
||||
.error(Errors.SESSION_EXPIRED)
|
||||
.detail(Details.REASON, "Offline session invalid because offline access not granted anymore")
|
||||
.assertEvent();
|
||||
|
||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken());
|
||||
assertEquals(400, response.getStatusCode());
|
||||
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
|
||||
assertEquals("Offline session invalid because offline access not granted anymore", response.getErrorDescription());
|
||||
events.expect(EventType.REFRESH_TOKEN_ERROR)
|
||||
.client("offline-client")
|
||||
.session(loginEvent.getSessionId())
|
||||
.user((String) null)
|
||||
.error(Errors.INVALID_TOKEN)
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
.detail(Details.REASON, "Offline session invalid because offline access not granted anymore")
|
||||
.assertEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user