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.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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue