Simplified checks in IntrospectionEndpoint (#28642)

Closes #24466

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>


Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-04-12 21:19:04 +02:00 committed by GitHub
parent a3b4b487d5
commit 4672366eb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 105 additions and 157 deletions

View file

@ -31,7 +31,6 @@ import org.jboss.logging.Logger;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider; import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -56,8 +55,7 @@ public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
public Response introspect(String token, EventBuilder eventBuilder) { public Response introspect(String token, EventBuilder eventBuilder) {
LOGGER.debug("Introspecting requesting party token"); LOGGER.debug("Introspecting requesting party token");
try { try {
AccessToken accessToken = verifyAccessToken(token, eventBuilder); AccessToken accessToken = verifyAccessToken(token, eventBuilder, true);
ObjectNode tokenMetadata; ObjectNode tokenMetadata;
if (accessToken != null) { if (accessToken != null) {

View file

@ -65,12 +65,13 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
public Response introspect(String token, EventBuilder eventBuilder) { public Response introspect(String token, EventBuilder eventBuilder) {
AccessToken accessToken = null; AccessToken accessToken = null;
try { try {
accessToken = verifyAccessToken(token, eventBuilder); accessToken = verifyAccessToken(token, eventBuilder, false);
accessToken = transformAccessToken(accessToken); UserSessionModel userSession = tokenManager.getValidUserSessionIfTokenIsValid(session, realm, accessToken, eventBuilder);
ObjectNode tokenMetadata;
ObjectNode tokenMetadata;
if (userSession != null) {
accessToken = transformAccessToken(accessToken, userSession);
if (accessToken != null) {
UserSessionModel userSession = accessToken.getSessionId() == null ? null : session.sessions().getUserSession(realm, accessToken.getSessionId());
tokenMetadata = JsonSerialization.createObjectNode(accessToken); tokenMetadata = JsonSerialization.createObjectNode(accessToken);
tokenMetadata.put("client_id", accessToken.getIssuedFor()); tokenMetadata.put("client_id", accessToken.getIssuedFor());
@ -83,22 +84,17 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
if (accessToken.getPreferredUsername() != null) { if (accessToken.getPreferredUsername() != null) {
tokenMetadata.put("username", accessToken.getPreferredUsername()); tokenMetadata.put("username", accessToken.getPreferredUsername());
} else { } else {
UserModel userModel = accessToken.getSubject() == null ? null : session.users().getUserById(realm, accessToken.getSubject()); UserModel userModel = userSession.getUser();
if (userModel != null) { if (userModel != null) {
tokenMetadata.put("username", userModel.getUsername()); tokenMetadata.put("username", userModel.getUsername());
} else if (userSession != null && userSession.getUser() != null) {
tokenMetadata.put("username", userSession.getUser().getUsername());
} }
} }
} }
if (userSession != null) { String actor = userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString());
String actor = userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString()); if (actor != null) {
// for token exchange delegation semantics when an entity (actor) other than the subject is the acting party to whom authority has been delegated
if (actor != null) { tokenMetadata.putObject("act").put("sub", actor);
// for token exchange delegation semantics when an entity (actor) other than the subject is the acting party to whom authority has been delegated
tokenMetadata.putObject("act").put("sub", actor);
}
} }
tokenMetadata.put(OAuth2Constants.TOKEN_TYPE, accessToken.getType()); tokenMetadata.put(OAuth2Constants.TOKEN_TYPE, accessToken.getType());
@ -109,7 +105,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
eventBuilder.error(Errors.TOKEN_INTROSPECTION_FAILED); eventBuilder.error(Errors.TOKEN_INTROSPECTION_FAILED);
} }
tokenMetadata.put("active", accessToken != null); tokenMetadata.put("active", userSession != null);
return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build(); return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build();
} catch (Exception e) { } catch (Exception e) {
@ -121,35 +117,20 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
} }
} }
public AccessToken transformAccessToken(AccessToken token) {
if (token == null) { public AccessToken transformAccessToken(AccessToken token, UserSessionModel userSession) {
return null; ClientModel client = realm.getClientByClientId(token.getIssuedFor());
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if(clientSession == null) {
return token;
} }
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
EventBuilder event = new EventBuilder(realm, session, session.getContext().getConnection())
.event(EventType.INTROSPECT_TOKEN)
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN);
UserSessionModel userSession;
try {
userSession = UserSessionUtil.findValidSession(session, realm, token, event, client);
} catch (Exception e) {
logger.debugf("Can not get user session: %s", e.getMessage());
// Backwards compatibility
return token;
}
if (userSession.getUser() == null) {
logger.debugf("User not found");
// Backwards compatibility
return token;
}
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, token.getScope(), session); ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, token.getScope(), session);
AccessToken smallToken = getAccessTokenFromStoredData(token, userSession); AccessToken smallToken = getAccessTokenFromStoredData(token);
return tokenManager.transformIntrospectionAccessToken(session, smallToken, userSession, clientSessionCtx); return tokenManager.transformIntrospectionAccessToken(session, smallToken, userSession, clientSessionCtx);
} }
private AccessToken getAccessTokenFromStoredData(AccessToken token, UserSessionModel userSession) { private AccessToken getAccessTokenFromStoredData(AccessToken token) {
// Copy just "basic" claims from the initial token. The same like filled in TokenManager.initToken. The rest should be possibly added by protocol mappers (only if configured for introspection response) // Copy just "basic" claims from the initial token. The same like filled in TokenManager.initToken. The rest should be possibly added by protocol mappers (only if configured for introspection response)
AccessToken newToken = new AccessToken(); AccessToken newToken = new AccessToken();
newToken.id(token.getId()); newToken.id(token.getId());
@ -171,8 +152,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
return newToken; return newToken;
} }
protected AccessToken verifyAccessToken(String token, EventBuilder eventBuilder) { protected AccessToken verifyAccessToken(String token, EventBuilder eventBuilder, boolean validateSession) {
AccessToken accessToken;
try { try {
TokenVerifier<AccessToken> verifier = TokenVerifier.create(token, AccessToken.class) TokenVerifier<AccessToken> verifier = TokenVerifier.create(token, AccessToken.class)
@ -181,16 +161,17 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId());
verifier.verifierContext(verifierContext); verifier.verifierContext(verifierContext);
accessToken = verifier.verify().getToken(); AccessToken accessToken = verifier.verify().getToken();
if (validateSession) {
return tokenManager.checkTokenValidForIntrospection(session, realm, verifier.verify().getToken(), eventBuilder);
}
return accessToken;
} catch (VerificationException e) { } catch (VerificationException e) {
logger.debugf("Introspection access token : JWT check failed: %s", e.getMessage()); logger.debugf("Introspection access token : JWT check failed: %s", e.getMessage());
eventBuilder.detail(Details.REASON,"Access token JWT check failed"); eventBuilder.detail(Details.REASON,"Access token JWT check failed");
return null; return null;
} }
RealmModel realm = this.session.getContext().getRealm();
return tokenManager.checkTokenValidForIntrospection(session, realm, accessToken, false, eventBuilder) ? accessToken : null;
} }
@Override @Override

View file

@ -90,6 +90,7 @@ import org.keycloak.services.util.AuthorizationContextUtil;
import org.keycloak.services.util.DPoPUtil; import org.keycloak.services.util.DPoPUtil;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.MtlsHoKTokenUtil; import org.keycloak.services.util.MtlsHoKTokenUtil;
import org.keycloak.services.util.UserSessionUtil;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
@ -240,25 +241,38 @@ public class TokenManager {
} }
/** /**
* Checks if the token is valid. Optionally the session last refresh and client session timestamp * Checks if the token is valid.
* are updated if the token was valid. This is used to keep the session alive when long lived tokens are used.
* *
* @param session * @param session
* @param realm * @param realm
* @param token * @param token
* @param updateTimestamps
* @return * @return
*/ */
public boolean checkTokenValidForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token, boolean updateTimestamps, EventBuilder eventBuilder) { public AccessToken checkTokenValidForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder eventBuilder) {
return getValidUserSessionIfTokenIsValid(session, realm, token, eventBuilder) != null ? token : null;
}
/**
* Checks if the token is valid and return a valid user session.
*
* @param session
* @param realm
* @param token
* @return
*/
public UserSessionModel getValidUserSessionIfTokenIsValid(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder eventBuilder) {
if (token == null) {
return null;
}
ClientModel client = realm.getClientByClientId(token.getIssuedFor()); ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null) { if (client == null) {
logger.debugf("Introspection access token : client with clientId %s does not exist", token.getIssuedFor() ); logger.debugf("Introspection access token : client with clientId %s does not exist", token.getIssuedFor() );
eventBuilder.detail(Details.REASON, String.format("Could not find client for %s", token.getIssuedFor())); eventBuilder.detail(Details.REASON, String.format("Could not find client for %s", token.getIssuedFor()));
return false; return null;
} else if (!client.isEnabled()) { } else if (!client.isEnabled()) {
logger.debugf("Introspection access token : client with clientId %s is disabled", token.getIssuedFor() ); logger.debugf("Introspection access token : client with clientId %s is disabled", token.getIssuedFor() );
eventBuilder.detail(Details.REASON, String.format("Client with clientId %s is disabled", token.getIssuedFor())); eventBuilder.detail(Details.REASON, String.format("Client with clientId %s is disabled", token.getIssuedFor()));
return false; return null;
} }
try { try {
@ -268,86 +282,42 @@ public class TokenManager {
} catch (VerificationException e) { } catch (VerificationException e) {
logger.debugf("Introspection access token for %s client: JWT check failed: %s", token.getIssuedFor(), e.getMessage()); logger.debugf("Introspection access token for %s client: JWT check failed: %s", token.getIssuedFor(), e.getMessage());
eventBuilder.detail(Details.REASON, "Introspection access token for "+token.getIssuedFor() +" client: JWT check failed"); eventBuilder.detail(Details.REASON, "Introspection access token for "+token.getIssuedFor() +" client: JWT check failed");
return false; return null;
} }
boolean valid = false; UserSessionModel userSession;
try {
userSession = UserSessionUtil.findValidSession(session, realm, token, eventBuilder, client);
} catch (Exception e) {
logger.debugf( "Introspection access token for " + token.getIssuedFor() + " client:" + e.getMessage());
eventBuilder.detail(Details.REASON, "Introspection access token for " + token.getIssuedFor() + " client:" + e.getMessage());
return null;
}
// Tokens without sessions are considered valid. Signature check and revocation check are sufficient checks for them if (!isUserValid(session, realm, token, userSession.getUser())) {
if (token.getSessionState() == null) { logger.debugf("Could not find valid user from user");
UserModel user = lookupUserFromStatelessToken(session, realm, token); eventBuilder.detail(Details.REASON, "Could not find valid user from user");
valid = isUserValid(session, realm, token, user); return null;
if (!valid) }
eventBuilder.detail(Details.REASON, "Could not find valid transient user session");
} else {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId()); String tokenType = token.getType();
if (realm.isRevokeRefreshToken()
if (userSession == null) {
// also try to resolve sessions created during token exchange when the user is impersonated
userSession = session.sessions().getUserSessionWithPredicate(realm,
token.getSessionState(), false,
model -> client.getId().equals(model.getNote(ImpersonationSessionNote.IMPERSONATOR_CLIENT.toString())));
}
if (AuthenticationManager.isSessionValid(realm, userSession)) {
valid = isUserValid(session, realm, token, userSession.getUser());
} else {
userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
valid = isUserValid(session, realm, token, userSession.getUser());
}
}
if (!valid) {
logger.debugf("Could not find valid user session for session_state = %s", token.getSessionState());
eventBuilder.detail(Details.REASON, String.format("Could not find valid user session for session_state = %s", token.getSessionState()));
}
if (valid && (token.isIssuedBeforeSessionStart(userSession.getStarted()))) {
valid = false;
logger.debugf("Token is issued (%s) before session () has started", String.valueOf(token.getIat()), String.valueOf(userSession.getStarted()));
eventBuilder.detail(Details.REASON, String.format("Token is issued (%s) before user session () has started", String.valueOf(token.getIat()), String.valueOf(userSession.getStarted())));
}
AuthenticatedClientSessionModel clientSession = userSession == null ? null : userSession.getAuthenticatedClientSessionByClient(client.getId());
if (clientSession != null) {
if (valid && (token.isIssuedBeforeSessionStart(clientSession.getStarted()))) {
valid = false;
logger.debugf("Token is issued (%s) before session () has started", String.valueOf(token.getIat()), String.valueOf(clientSession.getStarted()));
eventBuilder.detail(Details.REASON, String.format("Token is issued (%s) before client session () has started", String.valueOf(token.getIat()), String.valueOf(clientSession.getStarted())));
}
}
String tokenType = token.getType();
if (realm.isRevokeRefreshToken()
&& (tokenType.equals(TokenUtil.TOKEN_TYPE_REFRESH) || tokenType.equals(TokenUtil.TOKEN_TYPE_OFFLINE)) && (tokenType.equals(TokenUtil.TOKEN_TYPE_REFRESH) || tokenType.equals(TokenUtil.TOKEN_TYPE_OFFLINE))
&& !validateTokenReuseForIntrospection(session, realm, token)) { && !validateTokenReuseForIntrospection(session, realm, token)) {
logger.debug("Introspection access token for "+token.getIssuedFor() +" client: failed to validate Token reuse for introspection"); logger.debug("Introspection access token for "+token.getIssuedFor() +" client: failed to validate Token reuse for introspection");
eventBuilder.detail(Details.REASON, "Realm revoke refresh token, token type is "+tokenType+ " and token is not eligible for introspection"); eventBuilder.detail(Details.REASON, "Realm revoke refresh token, token type is "+tokenType+ " and token is not eligible for introspection");
return false; return null;
}
if (updateTimestamps && valid) {
int currentTime = Time.currentTime();
userSession.setLastSessionRefresh(currentTime);
if (clientSession != null) {
clientSession.setTimestamp(currentTime);
}
}
} }
return userSession;
return valid;
} }
private boolean validateTokenReuseForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token) { private boolean validateTokenReuseForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token) {
UserSessionModel userSession = null; UserSessionModel userSession = null;
if (token.getType().equals(TokenUtil.TOKEN_TYPE_REFRESH)) { if (token.getType().equals(TokenUtil.TOKEN_TYPE_REFRESH)) {
userSession = session.sessions().getUserSession(realm, token.getSessionState()); userSession = session.sessions().getUserSession(realm, token.getSessionId());
} else { } else {
UserSessionManager sessionManager = new UserSessionManager(session); UserSessionManager sessionManager = new UserSessionManager(session);
userSession = sessionManager.findOfflineUserSession(realm, token.getSessionState()); userSession = sessionManager.findOfflineUserSession(realm, token.getSessionId());
} }
ClientModel client = realm.getClientByClientId(token.getIssuedFor()); ClientModel client = realm.getClientByClientId(token.getIssuedFor());
@ -362,13 +332,13 @@ public class TokenManager {
} }
} }
private boolean isUserValid(KeycloakSession session, RealmModel realm, AccessToken token, UserModel user) { public static boolean isUserValid(KeycloakSession session, RealmModel realm, AccessToken token, UserModel user) {
if (user == null) { if (user == null) {
logger.debugf("User does not exist for token introspection"); logger.debugf("User does not exists");
return false; return false;
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
logger.debugf("User is disable for token introspection"); logger.debugf("User '%s' is disabled", user.getUsername());
return false; return false;
} }
try { try {

View file

@ -1410,14 +1410,14 @@ public class AuthenticationManager {
UserModel user = null; UserModel user = null;
if (token.getSessionState() == null) { if (token.getSessionState() == null) {
user = TokenManager.lookupUserFromStatelessToken(session, realm, token); user = TokenManager.lookupUserFromStatelessToken(session, realm, token);
if (!isUserValid(session, realm, user, token)) { if (!TokenManager.isUserValid(session, realm, token, user)) {
return null; return null;
} }
} else { } else {
userSession = session.sessions().getUserSession(realm, token.getSessionState()); userSession = session.sessions().getUserSession(realm, token.getSessionState());
if (userSession != null) { if (userSession != null) {
user = userSession.getUser(); user = userSession.getUser();
if (!isUserValid(session, realm, user, token)) { if (!TokenManager.isUserValid(session, realm, token, user)) {
return null; return null;
} }
} }
@ -1483,23 +1483,6 @@ public class AuthenticationManager {
return true; return true;
} }
private static boolean isUserValid(KeycloakSession session, RealmModel realm, UserModel user, AccessToken token) {
if (user == null || !user.isEnabled()) {
logger.debug("Unknown user in identity token");
return false;
}
if (! isLightweightUser(user)) {
int userNotBefore = session.users().getNotBeforeOfUser(realm, user);
if (token.getIssuedAt() < userNotBefore) {
logger.debug("User notBefore newer than token");
return false;
}
}
return true;
}
public enum AuthenticationStatus { public enum AuthenticationStatus {
SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
} }

View file

@ -22,10 +22,11 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ImpersonationSessionNote;
import static org.keycloak.services.managers.AuthenticationManager.authenticateIdentityCookie; import static org.keycloak.services.managers.AuthenticationManager.authenticateIdentityCookie;
@ -48,6 +49,9 @@ public class UserSessionCrossDCManager {
return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessionByClient(clientUUID) != null); return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessionByClient(clientUUID) != null);
} }
public UserSessionModel getUserSessionWithImpersonatorClient(RealmModel realm, String id, boolean offline, String clientUUID) {
return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> clientUUID.equals(userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_CLIENT.toString())));
}
// get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache
// TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead // TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead

View file

@ -34,6 +34,7 @@ import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resource.AccountResourceProvider; import org.keycloak.services.resource.AccountResourceProvider;
import org.keycloak.services.util.UserSessionUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.HttpMethod;
@ -120,11 +121,14 @@ public class AccountLoader {
} }
AccessToken accessToken = authResult.getToken(); AccessToken accessToken = authResult.getToken();
UserSessionUtil.checkTokenIssuedAt(client.getRealm(), accessToken, authResult.getSession(), event, authResult.getClient());
if (accessToken.getAudience() == null || accessToken.getResourceAccess(client.getClientId()) == null) { if (accessToken.getAudience() == null || accessToken.getResourceAccess(client.getClientId()) == null) {
// transform for introspection to get the required claims // transform for introspection to get the required claims
AccessTokenIntrospectionProvider provider = (AccessTokenIntrospectionProvider) session.getProvider(TokenIntrospectionProvider.class, AccessTokenIntrospectionProvider provider = (AccessTokenIntrospectionProvider) session.getProvider(TokenIntrospectionProvider.class,
AccessTokenIntrospectionProviderFactory.ACCESS_TOKEN_TYPE); AccessTokenIntrospectionProviderFactory.ACCESS_TOKEN_TYPE);
accessToken = provider.transformAccessToken(accessToken); accessToken = provider.transformAccessToken(accessToken, authResult.getSession());
} }
if (!accessToken.hasAudience(client.getClientId())) { if (!accessToken.hasAudience(client.getClientId())) {

View file

@ -12,7 +12,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -28,6 +27,9 @@ public class UserSessionUtil {
private static final Logger logger = Logger.getLogger(UserSessionUtil.class); private static final Logger logger = Logger.getLogger(UserSessionUtil.class);
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) { public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) {
OAuth2Error error = new OAuth2Error().json(false).realm(realm); OAuth2Error error = new OAuth2Error().json(false).realm(realm);
return findValidSession(session, realm, token, event, client, error); return findValidSession(session, realm, token, event, client, error);
@ -35,18 +37,23 @@ public class UserSessionUtil {
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm,
AccessToken token, EventBuilder event, ClientModel client, OAuth2Error error) { AccessToken token, EventBuilder event, ClientModel client, OAuth2Error error) {
if (token.getSessionState() == null) { if (token.getSessionId() == null) {
return createTransientSessionForClient(session, realm, token, client, event); return createTransientSessionForClient(session, realm, token, client, event);
} }
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId()); UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), false, client.getId());
if (userSession == null) {
// also try to resolve sessions created during token exchange when the user is impersonated
userSession = new UserSessionCrossDCManager(session).getUserSessionWithImpersonatorClient(realm, token.getSessionId(), false, client.getId());
}
UserSessionModel offlineUserSession = null; UserSessionModel offlineUserSession = null;
if (AuthenticationManager.isSessionValid(realm, userSession)) { if (AuthenticationManager.isSessionValid(realm, userSession)) {
checkTokenIssuedAt(realm, token, userSession, event, client); checkTokenIssuedAt(realm, token, userSession, event, client);
event.session(userSession); event.session(userSession);
return userSession; return userSession;
} else { } else {
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId()); offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) { if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
checkTokenIssuedAt(realm, token, offlineUserSession, event, client); checkTokenIssuedAt(realm, token, offlineUserSession, event, client);
event.session(offlineUserSession); event.session(offlineUserSession);
@ -94,7 +101,7 @@ public class UserSessionUtil {
return userSession; return userSession;
} }
private static void checkTokenIssuedAt(RealmModel realm, AccessToken token, UserSessionModel userSession, EventBuilder event, ClientModel client) { public static void checkTokenIssuedAt(RealmModel realm, AccessToken token, UserSessionModel userSession, EventBuilder event, ClientModel client) {
OAuth2Error error = new OAuth2Error().json(false).realm(realm); OAuth2Error error = new OAuth2Error().json(false).realm(realm);
if (token.isIssuedBeforeSessionStart(userSession.getStarted())) { if (token.isIssuedBeforeSessionStart(userSession.getStarted())) {
logger.debug("Stale token for user session"); logger.debug("Stale token for user session");
@ -103,7 +110,7 @@ public class UserSessionUtil {
} }
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if (token.isIssuedBeforeSessionStart(clientSession.getStarted())) { if (clientSession != null && token.isIssuedBeforeSessionStart(clientSession.getStarted())) {
logger.debug("Stale token for client session"); logger.debug("Stale token for client session");
event.error(Errors.INVALID_TOKEN); event.error(Errors.INVALID_TOKEN);
throw error.invalidToken("Stale token"); throw error.invalidToken("Stale token");

View file

@ -2783,6 +2783,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
} }
private String doIntrospectAccessTokenWithClientCredential(OAuthClient.AccessTokenResponse tokenRes, String username) throws IOException { private String doIntrospectAccessTokenWithClientCredential(OAuthClient.AccessTokenResponse tokenRes, String username) throws IOException {
AccessToken accessToken = oauth.verifyToken(tokenRes.getAccessToken());
String tokenResponse = oauth.introspectAccessTokenWithClientCredential(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, tokenRes.getAccessToken()); String tokenResponse = oauth.introspectAccessTokenWithClientCredential(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, tokenRes.getAccessToken());
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(tokenResponse); JsonNode jsonNode = objectMapper.readTree(tokenResponse);
@ -2793,7 +2794,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
assertThat(rep.isActive(), is(equalTo(true))); assertThat(rep.isActive(), is(equalTo(true)));
assertThat(rep.getClientId(), is(equalTo(TEST_CLIENT_NAME))); assertThat(rep.getClientId(), is(equalTo(TEST_CLIENT_NAME)));
assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME))); assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME)));
events.expect(EventType.INTROSPECT_TOKEN).user((String) null).clearDetails().assertEvent(); events.expect(EventType.INTROSPECT_TOKEN).user((String) null).session(accessToken.getSessionId()).clearDetails().assertEvent();
tokenResponse = oauth.introspectAccessTokenWithClientCredential(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, tokenRes.getRefreshToken()); tokenResponse = oauth.introspectAccessTokenWithClientCredential(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, tokenRes.getRefreshToken());
jsonNode = objectMapper.readTree(tokenResponse); jsonNode = objectMapper.readTree(tokenResponse);
@ -2804,7 +2805,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
assertThat(rep.getClientId(), is(equalTo(TEST_CLIENT_NAME))); assertThat(rep.getClientId(), is(equalTo(TEST_CLIENT_NAME)));
assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME))); assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME)));
assertThat(rep.getAudience()[0], is(equalTo(rep.getIssuer()))); assertThat(rep.getAudience()[0], is(equalTo(rep.getIssuer())));
events.expect(EventType.INTROSPECT_TOKEN).user((String) null).clearDetails().assertEvent(); events.expect(EventType.INTROSPECT_TOKEN).user((String) null).session(accessToken.getSessionId()).clearDetails().assertEvent();
tokenResponse = oauth.introspectAccessTokenWithClientCredential(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, tokenRes.getIdToken()); tokenResponse = oauth.introspectAccessTokenWithClientCredential(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, tokenRes.getIdToken());
jsonNode = objectMapper.readTree(tokenResponse); jsonNode = objectMapper.readTree(tokenResponse);
@ -2817,7 +2818,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME))); assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME)));
assertThat(rep.getPreferredUsername(), is(equalTo(username))); assertThat(rep.getPreferredUsername(), is(equalTo(username)));
assertThat(rep.getAudience()[0], is(equalTo(rep.getIssuedFor()))); assertThat(rep.getAudience()[0], is(equalTo(rep.getIssuedFor())));
events.expect(EventType.INTROSPECT_TOKEN).user((String) null).clearDetails().assertEvent(); events.expect(EventType.INTROSPECT_TOKEN).user((String) null).session(accessToken.getSessionId()).clearDetails().assertEvent();
return tokenResponse; return tokenResponse;
} }

View file

@ -648,7 +648,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
// OAuth2 protocol operation // OAuth2 protocol operation
protected void doIntrospectAccessToken(OAuthClient.AccessTokenResponse tokenRes, String username, String clientId, String clientSecret) throws IOException { protected void doIntrospectAccessToken(OAuthClient.AccessTokenResponse tokenRes, String username, String clientId, String sessionId, String clientSecret) throws IOException {
String tokenResponse = oauth.introspectAccessTokenWithClientCredential(clientId, clientSecret, tokenRes.getAccessToken()); String tokenResponse = oauth.introspectAccessTokenWithClientCredential(clientId, clientSecret, tokenRes.getAccessToken());
JsonNode jsonNode = objectMapper.readTree(tokenResponse); JsonNode jsonNode = objectMapper.readTree(tokenResponse);
assertEquals(true, jsonNode.get("active").asBoolean()); assertEquals(true, jsonNode.get("active").asBoolean());
@ -658,7 +658,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
assertEquals(true, rep.isActive()); assertEquals(true, rep.isActive());
assertEquals(clientId, rep.getClientId()); assertEquals(clientId, rep.getClientId());
assertEquals(clientId, rep.getIssuedFor()); assertEquals(clientId, rep.getIssuedFor());
events.expect(EventType.INTROSPECT_TOKEN).client(clientId).user((String)null).clearDetails().assertEvent(); events.expect(EventType.INTROSPECT_TOKEN).client(clientId).session(sessionId).user((String)null).clearDetails().assertEvent();
} }
protected void doTokenRevoke(String refreshToken, String clientId, String clientSecret, String userId, boolean isOfflineAccess) throws IOException { protected void doTokenRevoke(String refreshToken, String clientId, String clientSecret, String userId, boolean isOfflineAccess) throws IOException {
@ -1530,7 +1530,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
assertEquals(sessionId, refreshedRefreshToken.getSessionState()); assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertEquals(findUserByUsername(adminClient.realm(REALM_NAME), userName).getId(), refreshedToken.getSubject()); assertEquals(findUserByUsername(adminClient.realm(REALM_NAME), userName).getId(), refreshedToken.getSubject());
doIntrospectAccessToken(refreshResponse, userName, clientId, clientSecret); doIntrospectAccessToken(refreshResponse, userName, clientId, sessionId, clientSecret);
doTokenRevoke(refreshResponse.getRefreshToken(), clientId, clientSecret, userId, false); doTokenRevoke(refreshResponse.getRefreshToken(), clientId, clientSecret, userId, false);
} }