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:
parent
a3b4b487d5
commit
4672366eb9
9 changed files with 105 additions and 157 deletions
|
@ -31,7 +31,6 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
@ -56,8 +55,7 @@ public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
|
|||
public Response introspect(String token, EventBuilder eventBuilder) {
|
||||
LOGGER.debug("Introspecting requesting party token");
|
||||
try {
|
||||
AccessToken accessToken = verifyAccessToken(token, eventBuilder);
|
||||
|
||||
AccessToken accessToken = verifyAccessToken(token, eventBuilder, true);
|
||||
ObjectNode tokenMetadata;
|
||||
|
||||
if (accessToken != null) {
|
||||
|
|
|
@ -65,12 +65,13 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
|
|||
public Response introspect(String token, EventBuilder eventBuilder) {
|
||||
AccessToken accessToken = null;
|
||||
try {
|
||||
accessToken = verifyAccessToken(token, eventBuilder);
|
||||
accessToken = transformAccessToken(accessToken);
|
||||
ObjectNode tokenMetadata;
|
||||
accessToken = verifyAccessToken(token, eventBuilder, false);
|
||||
UserSessionModel userSession = tokenManager.getValidUserSessionIfTokenIsValid(session, realm, accessToken, eventBuilder);
|
||||
|
||||
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.put("client_id", accessToken.getIssuedFor());
|
||||
|
||||
|
@ -83,22 +84,17 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
|
|||
if (accessToken.getPreferredUsername() != null) {
|
||||
tokenMetadata.put("username", accessToken.getPreferredUsername());
|
||||
} else {
|
||||
UserModel userModel = accessToken.getSubject() == null ? null : session.users().getUserById(realm, accessToken.getSubject());
|
||||
UserModel userModel = userSession.getUser();
|
||||
if (userModel != null) {
|
||||
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());
|
||||
|
||||
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
|
||||
tokenMetadata.putObject("act").put("sub", actor);
|
||||
}
|
||||
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
|
||||
tokenMetadata.putObject("act").put("sub", actor);
|
||||
}
|
||||
|
||||
tokenMetadata.put(OAuth2Constants.TOKEN_TYPE, accessToken.getType());
|
||||
|
@ -109,7 +105,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
|
|||
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();
|
||||
} catch (Exception e) {
|
||||
|
@ -121,35 +117,20 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
|
|||
}
|
||||
}
|
||||
|
||||
public AccessToken transformAccessToken(AccessToken token) {
|
||||
if (token == null) {
|
||||
return null;
|
||||
|
||||
public AccessToken transformAccessToken(AccessToken token, UserSessionModel userSession) {
|
||||
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);
|
||||
AccessToken smallToken = getAccessTokenFromStoredData(token, userSession);
|
||||
AccessToken smallToken = getAccessTokenFromStoredData(token);
|
||||
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)
|
||||
AccessToken newToken = new AccessToken();
|
||||
newToken.id(token.getId());
|
||||
|
@ -171,8 +152,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
|
|||
return newToken;
|
||||
}
|
||||
|
||||
protected AccessToken verifyAccessToken(String token, EventBuilder eventBuilder) {
|
||||
AccessToken accessToken;
|
||||
protected AccessToken verifyAccessToken(String token, EventBuilder eventBuilder, boolean validateSession) {
|
||||
|
||||
try {
|
||||
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());
|
||||
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) {
|
||||
logger.debugf("Introspection access token : JWT check failed: %s", e.getMessage());
|
||||
eventBuilder.detail(Details.REASON,"Access token JWT check failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
RealmModel realm = this.session.getContext().getRealm();
|
||||
|
||||
return tokenManager.checkTokenValidForIntrospection(session, realm, accessToken, false, eventBuilder) ? accessToken : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -90,6 +90,7 @@ import org.keycloak.services.util.AuthorizationContextUtil;
|
|||
import org.keycloak.services.util.DPoPUtil;
|
||||
import org.keycloak.services.util.DefaultClientSessionContext;
|
||||
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||
import org.keycloak.services.util.UserSessionUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
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
|
||||
* are updated if the token was valid. This is used to keep the session alive when long lived tokens are used.
|
||||
* Checks if the token is valid.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param token
|
||||
* @param updateTimestamps
|
||||
* @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());
|
||||
if (client == null) {
|
||||
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()));
|
||||
return false;
|
||||
return null;
|
||||
} else if (!client.isEnabled()) {
|
||||
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()));
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -268,86 +282,42 @@ public class TokenManager {
|
|||
} catch (VerificationException e) {
|
||||
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");
|
||||
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 (token.getSessionState() == null) {
|
||||
UserModel user = lookupUserFromStatelessToken(session, realm, token);
|
||||
valid = isUserValid(session, realm, token, user);
|
||||
if (!valid)
|
||||
eventBuilder.detail(Details.REASON, "Could not find valid transient user session");
|
||||
} else {
|
||||
if (!isUserValid(session, realm, token, userSession.getUser())) {
|
||||
logger.debugf("Could not find valid user from user");
|
||||
eventBuilder.detail(Details.REASON, "Could not find valid user from user");
|
||||
return null;
|
||||
}
|
||||
|
||||
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
|
||||
|
||||
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()
|
||||
String tokenType = token.getType();
|
||||
if (realm.isRevokeRefreshToken()
|
||||
&& (tokenType.equals(TokenUtil.TOKEN_TYPE_REFRESH) || tokenType.equals(TokenUtil.TOKEN_TYPE_OFFLINE))
|
||||
&& !validateTokenReuseForIntrospection(session, realm, token)) {
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updateTimestamps && valid) {
|
||||
int currentTime = Time.currentTime();
|
||||
userSession.setLastSessionRefresh(currentTime);
|
||||
if (clientSession != null) {
|
||||
clientSession.setTimestamp(currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
return null;
|
||||
}
|
||||
|
||||
return valid;
|
||||
return userSession;
|
||||
}
|
||||
|
||||
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());
|
||||
userSession = session.sessions().getUserSession(realm, token.getSessionId());
|
||||
} else {
|
||||
UserSessionManager sessionManager = new UserSessionManager(session);
|
||||
userSession = sessionManager.findOfflineUserSession(realm, token.getSessionState());
|
||||
userSession = sessionManager.findOfflineUserSession(realm, token.getSessionId());
|
||||
}
|
||||
|
||||
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) {
|
||||
logger.debugf("User does not exist for token introspection");
|
||||
logger.debugf("User does not exists");
|
||||
return false;
|
||||
}
|
||||
if (!user.isEnabled()) {
|
||||
logger.debugf("User is disable for token introspection");
|
||||
logger.debugf("User '%s' is disabled", user.getUsername());
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -1410,14 +1410,14 @@ public class AuthenticationManager {
|
|||
UserModel user = null;
|
||||
if (token.getSessionState() == null) {
|
||||
user = TokenManager.lookupUserFromStatelessToken(session, realm, token);
|
||||
if (!isUserValid(session, realm, user, token)) {
|
||||
if (!TokenManager.isUserValid(session, realm, token, user)) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
userSession = session.sessions().getUserSession(realm, token.getSessionState());
|
||||
if (userSession != null) {
|
||||
user = userSession.getUser();
|
||||
if (!isUserValid(session, realm, user, token)) {
|
||||
if (!TokenManager.isUserValid(session, realm, token, user)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1483,23 +1483,6 @@ public class AuthenticationManager {
|
|||
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 {
|
||||
SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
|
||||
}
|
||||
|
|
|
@ -22,10 +22,11 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ImpersonationSessionNote;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
// TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.keycloak.services.managers.AppAuthManager;
|
|||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resource.AccountResourceProvider;
|
||||
import org.keycloak.services.util.UserSessionUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
||||
import jakarta.ws.rs.HttpMethod;
|
||||
|
@ -120,11 +121,14 @@ public class AccountLoader {
|
|||
}
|
||||
|
||||
AccessToken accessToken = authResult.getToken();
|
||||
|
||||
UserSessionUtil.checkTokenIssuedAt(client.getRealm(), accessToken, authResult.getSession(), event, authResult.getClient());
|
||||
|
||||
if (accessToken.getAudience() == null || accessToken.getResourceAccess(client.getClientId()) == null) {
|
||||
// transform for introspection to get the required claims
|
||||
AccessTokenIntrospectionProvider provider = (AccessTokenIntrospectionProvider) session.getProvider(TokenIntrospectionProvider.class,
|
||||
AccessTokenIntrospectionProviderFactory.ACCESS_TOKEN_TYPE);
|
||||
accessToken = provider.transformAccessToken(accessToken);
|
||||
accessToken = provider.transformAccessToken(accessToken, authResult.getSession());
|
||||
}
|
||||
|
||||
if (!accessToken.hasAudience(client.getClientId())) {
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
@ -28,6 +27,9 @@ public class UserSessionUtil {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(UserSessionUtil.class);
|
||||
|
||||
|
||||
|
||||
|
||||
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) {
|
||||
OAuth2Error error = new OAuth2Error().json(false).realm(realm);
|
||||
return findValidSession(session, realm, token, event, client, error);
|
||||
|
@ -35,18 +37,23 @@ public class UserSessionUtil {
|
|||
|
||||
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm,
|
||||
AccessToken token, EventBuilder event, ClientModel client, OAuth2Error error) {
|
||||
if (token.getSessionState() == null) {
|
||||
if (token.getSessionId() == null) {
|
||||
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;
|
||||
if (AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
checkTokenIssuedAt(realm, token, userSession, event, client);
|
||||
event.session(userSession);
|
||||
return userSession;
|
||||
} 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)) {
|
||||
checkTokenIssuedAt(realm, token, offlineUserSession, event, client);
|
||||
event.session(offlineUserSession);
|
||||
|
@ -94,7 +101,7 @@ public class UserSessionUtil {
|
|||
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);
|
||||
if (token.isIssuedBeforeSessionStart(userSession.getStarted())) {
|
||||
logger.debug("Stale token for user session");
|
||||
|
@ -103,7 +110,7 @@ public class UserSessionUtil {
|
|||
}
|
||||
|
||||
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");
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
throw error.invalidToken("Stale token");
|
||||
|
|
|
@ -2783,6 +2783,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
|||
}
|
||||
|
||||
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());
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode jsonNode = objectMapper.readTree(tokenResponse);
|
||||
|
@ -2793,7 +2794,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
|||
assertThat(rep.isActive(), is(equalTo(true)));
|
||||
assertThat(rep.getClientId(), 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());
|
||||
jsonNode = objectMapper.readTree(tokenResponse);
|
||||
|
@ -2804,7 +2805,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
|||
assertThat(rep.getClientId(), is(equalTo(TEST_CLIENT_NAME)));
|
||||
assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME)));
|
||||
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());
|
||||
jsonNode = objectMapper.readTree(tokenResponse);
|
||||
|
@ -2817,7 +2818,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
|||
assertThat(rep.getIssuedFor(), is(equalTo(TEST_CLIENT_NAME)));
|
||||
assertThat(rep.getPreferredUsername(), is(equalTo(username)));
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -648,7 +648,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
|
|||
|
||||
// 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());
|
||||
JsonNode jsonNode = objectMapper.readTree(tokenResponse);
|
||||
assertEquals(true, jsonNode.get("active").asBoolean());
|
||||
|
@ -658,7 +658,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
|
|||
assertEquals(true, rep.isActive());
|
||||
assertEquals(clientId, rep.getClientId());
|
||||
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 {
|
||||
|
@ -1530,7 +1530,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
|
|||
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
|
||||
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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue