mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
Session type incorrectly set in access-token context when token created with scope=offline_access (#37701)
closes #37694 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
f7e21af82e
commit
92c96033f2
12 changed files with 95 additions and 12 deletions
|
|
@ -40,6 +40,11 @@ public interface ClientSessionContext {
|
|||
*/
|
||||
Stream<ClientScopeModel> getClientScopesStream();
|
||||
|
||||
/**
|
||||
* @return true if offline token is requested
|
||||
*/
|
||||
boolean isOfflineTokenRequested();
|
||||
|
||||
/**
|
||||
* Returns all roles including composite ones as a stream.
|
||||
* @return Stream of {@link RoleModel}. Never returns {@code null}.
|
||||
|
|
|
|||
|
|
@ -1212,8 +1212,7 @@ public class TokenManager {
|
|||
throw new IllegalStateException("accessToken not set");
|
||||
}
|
||||
|
||||
ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(realm, OAuth2Constants.OFFLINE_ACCESS);
|
||||
boolean offlineTokenRequested = offlineAccessScope==null ? false : clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId());
|
||||
boolean offlineTokenRequested = clientSessionCtx.isOfflineTokenRequested();
|
||||
generateRefreshToken(offlineTokenRequested);
|
||||
refreshToken.setScope(clientSessionCtx.getScopeString(true));
|
||||
if (realm.isRevokeRefreshToken()) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ package org.keycloak.protocol.oidc.encode;
|
|||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Some context info about the token
|
||||
*
|
||||
|
|
@ -66,7 +69,8 @@ public class AccessTokenContext {
|
|||
}
|
||||
}
|
||||
|
||||
public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
|
||||
@JsonCreator
|
||||
public AccessTokenContext(@JsonProperty("sessionType") SessionType sessionType, @JsonProperty("tokenType") TokenType tokenType, @JsonProperty("grantType") String grantType, @JsonProperty("rawTokenId") String rawTokenId) {
|
||||
Objects.requireNonNull(sessionType, "Null sessionType not allowed");
|
||||
Objects.requireNonNull(tokenType, "Null tokenType not allowed");
|
||||
Objects.requireNonNull(grantType, "Null grantType not allowed");
|
||||
|
|
@ -92,4 +96,21 @@ public class AccessTokenContext {
|
|||
public String getRawTokenId() {
|
||||
return rawTokenId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
return obj instanceof AccessTokenContext that &&
|
||||
sessionType == that.sessionType &&
|
||||
tokenType == that.tokenType &&
|
||||
Objects.equals(grantType, that.grantType) &&
|
||||
Objects.equals(rawTokenId, that.rawTokenId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sessionType, tokenType, grantType, rawTokenId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class DefaultTokenContextEncoderProvider implements TokenContextEncoderPr
|
|||
if (userSession.getPersistenceState() == UserSessionModel.SessionPersistenceState.TRANSIENT) {
|
||||
sessionType = AccessTokenContext.SessionType.TRANSIENT;
|
||||
} else {
|
||||
sessionType = userSession.isOffline() ? AccessTokenContext.SessionType.OFFLINE : AccessTokenContext.SessionType.ONLINE;
|
||||
sessionType = clientSessionContext.isOfflineTokenRequested() ? AccessTokenContext.SessionType.OFFLINE : AccessTokenContext.SessionType.ONLINE;
|
||||
}
|
||||
|
||||
boolean useLightweightToken = AbstractOIDCProtocolMapper.getShouldUseLightweightToken(session);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import org.keycloak.events.Details;
|
|||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
|
|
@ -120,6 +121,7 @@ public class ClientCredentialsGrantType extends OAuth2GrantTypeBase {
|
|||
|
||||
AuthenticationManager.setClientScopesInSession(session, authSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);
|
||||
clientSessionCtx.setAttribute(Constants.GRANT_TYPE, context.getGrantType());
|
||||
|
||||
// Notes about client details
|
||||
userSession.setNote(ServiceAccountConstants.CLIENT_ID_SESSION_NOTE, client.getClientId()); // This is for backwards compatibility
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
|
|
@ -130,6 +131,16 @@ public class DefaultClientSessionContext implements ClientSessionContext {
|
|||
return allowedClientScopes.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOfflineTokenRequested() {
|
||||
Boolean offlineAccessRequested = getAttribute(OAuth2Constants.OFFLINE_ACCESS, Boolean.class);
|
||||
if (offlineAccessRequested != null) return offlineAccessRequested;
|
||||
|
||||
ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(clientSession.getRealm(), OAuth2Constants.OFFLINE_ACCESS);
|
||||
offlineAccessRequested = offlineAccessScope == null ? false : getClientScopeIds().contains(offlineAccessScope.getId());
|
||||
setAttribute(OAuth2Constants.OFFLINE_ACCESS, offlineAccessRequested);
|
||||
return offlineAccessRequested;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getRolesStream() {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
package org.keycloak.protocol.oidc.encode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -27,6 +28,7 @@ import org.junit.Assert;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
|
@ -73,6 +75,16 @@ public class DefaultTokenContextEncoderProviderTest {
|
|||
Assert.assertEquals(tokenId, provider.encodeTokenId(ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJsonSerialization() throws IOException {
|
||||
String tokenId = "trltcc:1234";
|
||||
AccessTokenContext ctx = provider.getTokenContextFromTokenId(tokenId);
|
||||
|
||||
String s = JsonSerialization.writeValueAsString(ctx);
|
||||
AccessTokenContext deserialized = JsonSerialization.readValue(s, AccessTokenContext.class);
|
||||
Assert.assertEquals(ctx, deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncorrectGrantType() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStoreFactory;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.ResetTimeOffsetEvent;
|
||||
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
|
||||
import org.keycloak.protocol.oidc.encode.TokenContextEncoderProvider;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
|
@ -1173,4 +1175,12 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
prov.removeIncludedEvents(events.toArray(EventType[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/token-context")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccessTokenContext getTokenContext(@QueryParam("tokenId") String tokenId) {
|
||||
return session.getProvider(TokenContextEncoderProvider.class).getTokenContextFromTokenId(tokenId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import org.jboss.resteasy.reactive.NoCache;
|
|||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.enums.HostnameVerificationPolicy;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
|
|
@ -474,4 +475,9 @@ public interface TestingResource {
|
|||
@Path("/email-event-litener-provide/remove-events")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void removeEventsToEmailEventListenerProvider(List<EventType> events);
|
||||
|
||||
@GET
|
||||
@Path("/token-context")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
AccessTokenContext getTokenContext(@QueryParam("tokenId") String tokenId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package org.keycloak.testsuite.oauth;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
|
|
@ -47,6 +46,7 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
|
@ -261,6 +261,11 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
Assert.assertNull(offlineToken.getExp());
|
||||
|
||||
AccessTokenContext ctx = testingClient.testing("test").getTokenContext(token.getId());
|
||||
Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.OFFLINE);
|
||||
Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.REGULAR);
|
||||
Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.AUTHORIZATION_CODE);
|
||||
|
||||
assertTrue(tokenResponse.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
|
||||
|
||||
// check only offline session is created
|
||||
|
|
@ -365,6 +370,10 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
|
||||
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
AccessTokenContext ctx = testingClient.testing("test").getTokenContext(refreshedToken.getId());
|
||||
Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.OFFLINE);
|
||||
Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.REGULAR);
|
||||
Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.REFRESH_TOKEN);
|
||||
|
||||
// Assert new refreshToken in the response
|
||||
String newRefreshToken = response.getRefreshToken();
|
||||
|
|
|
|||
|
|
@ -19,13 +19,12 @@ package org.keycloak.testsuite.oauth;
|
|||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
|
|
@ -38,6 +37,7 @@ import org.keycloak.jose.jws.JWSInput;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
|
||||
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
|
|
@ -58,7 +58,6 @@ import org.keycloak.testsuite.util.TokenSignatureUtil;
|
|||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
import jakarta.ws.rs.ClientErrorException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.testsuite.util.oauth.LogoutResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -416,6 +415,11 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
|
|||
Assert.assertNull(accessToken.getSessionState());
|
||||
Assert.assertNull("Refresh-Token should not be present", response.getRefreshToken());
|
||||
|
||||
AccessTokenContext ctx = testingClient.testing("test").getTokenContext(accessToken.getId());
|
||||
Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.TRANSIENT);
|
||||
Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.REGULAR);
|
||||
Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.CLIENT_CREDENTIALS);
|
||||
|
||||
events.expectClientLogin()
|
||||
.client("service-account-cl")
|
||||
.user(userIdCl)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
package org.keycloak.testsuite.oidc;
|
||||
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
|
@ -42,6 +41,7 @@ import org.keycloak.models.ProtocolMapperModel;
|
|||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
|
||||
import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
|
||||
|
|
@ -70,7 +70,6 @@ import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
|
|||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.testsuite.util.ProtocolMapperUtil;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -107,7 +106,6 @@ import static org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper.PAIRWIS
|
|||
import static org.keycloak.protocol.oidc.mappers.RoleNameMapper.NEW_ROLE_NAME;
|
||||
import static org.keycloak.protocol.oidc.mappers.RoleNameMapper.ROLE_CONFIG;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
|
||||
|
||||
|
|
@ -358,7 +356,13 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest {
|
|||
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode());
|
||||
String accessToken = tokenResponse.getAccessToken();
|
||||
logger.debug("access token:" + accessToken);
|
||||
assertAccessToken(oauth.verifyToken(accessToken), true, true, true);
|
||||
AccessToken token = oauth.verifyToken(accessToken);
|
||||
assertAccessToken(token, true, true, true);
|
||||
|
||||
AccessTokenContext ctx = testingClient.testing("test").getTokenContext(token.getId());
|
||||
Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.ONLINE);
|
||||
Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.LIGHTWEIGHT);
|
||||
Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.AUTHORIZATION_CODE);
|
||||
|
||||
oauth.client(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD);
|
||||
String introspectResponse = oauth.doIntrospectionAccessTokenRequest(accessToken);
|
||||
|
|
|
|||
Loading…
Reference in a new issue