Enhance error logs and error events during UserInfo endpoint and Token Introspection failure

Closes #24344

Signed-off-by: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr>
This commit is contained in:
cgeorgilakis-grnet 2023-10-27 13:46:20 +03:00 committed by Marek Posolda
parent 42b010ce0a
commit ccade62289
8 changed files with 70 additions and 16 deletions

View file

@ -67,6 +67,8 @@ public interface Errors {
String INVALID_INPUT = "invalid_input"; String INVALID_INPUT = "invalid_input";
String COOKIE_NOT_FOUND = "cookie_not_found"; String COOKIE_NOT_FOUND = "cookie_not_found";
String TOKEN_INTROSPECTION_FAILED = "token_introspection_failed";
String REGISTRATION_DISABLED = "registration_disabled"; String REGISTRATION_DISABLED = "registration_disabled";
String RESET_CREDENTIAL_DISABLED = "reset_credential_disabled"; String RESET_CREDENTIAL_DISABLED = "reset_credential_disabled";

View file

@ -17,6 +17,7 @@
*/ */
package org.keycloak.protocol.oidc; package org.keycloak.protocol.oidc;
import org.keycloak.events.EventBuilder;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@ -34,5 +35,5 @@ public interface TokenIntrospectionProvider extends Provider {
* @param token the token to introspect. * @param token the token to introspect.
* @return the response with the information about the token * @return the response with the information about the token
*/ */
Response introspect(String token); Response introspect(String token, EventBuilder event);
} }

View file

@ -28,6 +28,10 @@ import jakarta.ws.rs.core.Response;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.logging.Logger; 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.models.KeycloakSession;
import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider; import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -49,10 +53,10 @@ public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
} }
@Override @Override
public Response introspect(String token) { 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); AccessToken accessToken = verifyAccessToken(token, eventBuilder);
ObjectNode tokenMetadata; ObjectNode tokenMetadata;
@ -85,12 +89,15 @@ public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
} }
} else { } else {
tokenMetadata = JsonSerialization.createObjectNode(); tokenMetadata = JsonSerialization.createObjectNode();
eventBuilder.error(Errors.TOKEN_INTROSPECTION_FAILED);
} }
tokenMetadata.put("active", accessToken != null); tokenMetadata.put("active", accessToken != 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) {
eventBuilder.detail(Details.REASON, e.getMessage());
eventBuilder.error(Errors.TOKEN_INTROSPECTION_FAILED);
throw new RuntimeException("Error creating token introspection response.", e); throw new RuntimeException("Error creating token introspection response.", e);
} }
} }

View file

@ -25,6 +25,7 @@ import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
@ -60,9 +61,10 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
this.tokenManager = new TokenManager(); this.tokenManager = new TokenManager();
} }
public Response introspect(String token) { public Response introspect(String token, EventBuilder eventBuilder) {
AccessToken accessToken = null;
try { try {
AccessToken accessToken = verifyAccessToken(token); accessToken = verifyAccessToken(token, eventBuilder);
accessToken = transformAccessToken(accessToken); accessToken = transformAccessToken(accessToken);
ObjectNode tokenMetadata; ObjectNode tokenMetadata;
@ -105,12 +107,18 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
} else { } else {
tokenMetadata = JsonSerialization.createObjectNode(); tokenMetadata = JsonSerialization.createObjectNode();
logger.debug("Keycloak token introspection return false");
eventBuilder.error(Errors.TOKEN_INTROSPECTION_FAILED);
} }
tokenMetadata.put("active", accessToken != null); tokenMetadata.put("active", accessToken != 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) {
String clientId = accessToken != null ? accessToken.getIssuedFor() : "unknown";
logger.debugf(e, "Exception during Keycloak introspection for %s client in realm %s", clientId, realm.getName());
eventBuilder.detail(Details.REASON, e.getMessage());
eventBuilder.error(Errors.TOKEN_INTROSPECTION_FAILED);
throw new RuntimeException("Error creating token introspection response.", e); throw new RuntimeException("Error creating token introspection response.", e);
} }
} }
@ -166,7 +174,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
return newToken; return newToken;
} }
protected AccessToken verifyAccessToken(String token) { protected AccessToken verifyAccessToken(String token, EventBuilder eventBuilder) {
AccessToken accessToken; AccessToken accessToken;
try { try {
@ -178,13 +186,14 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
accessToken = verifier.verify().getToken(); accessToken = verifier.verify().getToken();
} catch (VerificationException e) { } catch (VerificationException e) {
logger.debugf("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");
return null; return null;
} }
RealmModel realm = this.session.getContext().getRealm(); RealmModel realm = this.session.getContext().getRealm();
return tokenManager.checkTokenValidForIntrospection(session, realm, accessToken, false) ? accessToken : null; return tokenManager.checkTokenValidForIntrospection(session, realm, accessToken, false, eventBuilder) ? accessToken : null;
} }
@Override @Override

View file

@ -247,9 +247,15 @@ public class TokenManager {
* @param updateTimestamps * @param updateTimestamps
* @return * @return
*/ */
public boolean checkTokenValidForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token, boolean updateTimestamps) { public boolean checkTokenValidForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token, boolean updateTimestamps, EventBuilder eventBuilder) {
ClientModel client = realm.getClientByClientId(token.getIssuedFor()); ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null || !client.isEnabled()) { 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;
} 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 false;
} }
@ -258,7 +264,8 @@ public class TokenManager {
.withChecks(NotBeforeCheck.forModel(client), TokenVerifier.IS_ACTIVE, new TokenRevocationCheck(session)) .withChecks(NotBeforeCheck.forModel(client), TokenVerifier.IS_ACTIVE, new TokenRevocationCheck(session))
.verify(); .verify();
} catch (VerificationException e) { } catch (VerificationException e) {
logger.debugf("JWT check failed: %s", 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");
return false; return false;
} }
@ -268,6 +275,8 @@ public class TokenManager {
if (token.getSessionState() == null) { if (token.getSessionState() == null) {
UserModel user = lookupUserFromStatelessToken(session, realm, token); UserModel user = lookupUserFromStatelessToken(session, realm, token);
valid = isUserValid(session, realm, token, user); valid = isUserValid(session, realm, token, user);
if (!valid)
eventBuilder.detail(Details.REASON, "Could not find valid transient user session");
} else { } else {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId()); UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
@ -288,14 +297,23 @@ public class TokenManager {
} }
} }
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()))) { if (valid && (token.isIssuedBeforeSessionStart(userSession.getStarted()))) {
valid = false; 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()); AuthenticatedClientSessionModel clientSession = userSession == null ? null : userSession.getAuthenticatedClientSessionByClient(client.getId());
if (clientSession != null) { if (clientSession != null) {
if (valid && (token.isIssuedBeforeSessionStart(clientSession.getStarted()))) { if (valid && (token.isIssuedBeforeSessionStart(clientSession.getStarted()))) {
valid = false; 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())));
} }
} }
@ -303,7 +321,9 @@ public class TokenManager {
if (realm.isRevokeRefreshToken() 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)) {
return false; 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) { if (updateTimestamps && valid) {
@ -313,6 +333,7 @@ public class TokenManager {
clientSession.setTimestamp(currentTime); clientSession.setTimestamp(currentTime);
} }
} }
} }
return valid; return valid;
@ -334,15 +355,18 @@ public class TokenManager {
validateTokenReuse(session, realm, token, clientSession, false); validateTokenReuse(session, realm, token, clientSession, false);
return true; return true;
} catch (OAuthErrorException e) { } catch (OAuthErrorException e) {
logger.debug("validateTokenReuseForIntrospection is false",e);
return false; return false;
} }
} }
private boolean isUserValid(KeycloakSession session, RealmModel realm, AccessToken token, UserModel user) { private 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");
return false; return false;
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
logger.debugf("User is disable for token introspection");
return false; return false;
} }
try { try {

View file

@ -107,7 +107,7 @@ public class TokenIntrospectionEndpoint {
try { try {
Response response = provider.introspect(token); Response response = provider.introspect(token, event);
this.event.success(); this.event.success();

View file

@ -1,5 +1,6 @@
package org.keycloak.services.util; package org.keycloak.services.util;
import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -11,6 +12,7 @@ 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;
@ -24,10 +26,12 @@ import org.keycloak.utils.OAuth2Error;
public class UserSessionUtil { 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) { 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);
if (token.getSessionState() == null) { if (token.getSessionState() == null) {
return createTransientSessionForClient(session, realm, token, client); 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.getSessionState(), false, client.getId());
@ -46,6 +50,7 @@ public class UserSessionUtil {
} }
if (userSession == null && offlineUserSession == null) { if (userSession == null && offlineUserSession == null) {
logger.debug("User session not found or doesn't have client attached on it");
event.error(Errors.USER_SESSION_NOT_FOUND); event.error(Errors.USER_SESSION_NOT_FOUND);
throw error.invalidToken("User session not found or doesn't have client attached on it"); throw error.invalidToken("User session not found or doesn't have client attached on it");
} }
@ -56,15 +61,18 @@ public class UserSessionUtil {
event.session(offlineUserSession); event.session(offlineUserSession);
} }
logger.debug("Session expired");
event.error(Errors.SESSION_EXPIRED); event.error(Errors.SESSION_EXPIRED);
throw error.invalidToken("Session expired"); throw error.invalidToken("Session expired");
} }
private static UserSessionModel createTransientSessionForClient(KeycloakSession session, RealmModel realm, AccessToken token, ClientModel client) { private static UserSessionModel createTransientSessionForClient(KeycloakSession session, RealmModel realm, AccessToken token, ClientModel client, EventBuilder event) {
OAuth2Error error = new OAuth2Error().json(false).realm(realm); OAuth2Error error = new OAuth2Error().json(false).realm(realm);
// create a transient session // create a transient session
UserModel user = TokenManager.lookupUserFromStatelessToken(session, realm, token); UserModel user = TokenManager.lookupUserFromStatelessToken(session, realm, token);
if (user == null) { if (user == null) {
logger.debug("Transient User not found");
event.error(Errors.USER_NOT_FOUND);
throw error.invalidToken("User not found"); throw error.invalidToken("User not found");
} }
ClientConnection clientConnection = session.getContext().getConnection(); ClientConnection clientConnection = session.getContext().getConnection();
@ -84,12 +92,14 @@ public class UserSessionUtil {
private static void checkTokenIssuedAt(RealmModel realm, AccessToken token, UserSessionModel userSession, EventBuilder event, ClientModel client) { private 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");
event.error(Errors.INVALID_TOKEN); event.error(Errors.INVALID_TOKEN);
throw error.invalidToken("Stale token"); throw error.invalidToken("Stale token");
} }
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if (token.isIssuedBeforeSessionStart(clientSession.getStarted())) { if (token.isIssuedBeforeSessionStart(clientSession.getStarted())) {
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

@ -379,10 +379,11 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
// Check that it is not possible to introspect token anymore // Check that it is not possible to introspect token anymore
Assert.assertFalse(getIntrospectionResponse("service-account-cl", "secret1", tokenString)); Assert.assertFalse(getIntrospectionResponse("service-account-cl", "secret1", tokenString));
// TODO: This would be better to be "INTROSPECT_TOKEN_ERROR" // TODO: This would be better to be "INTROSPECT_TOKEN_ERROR"
events.expect(EventType.INTROSPECT_TOKEN) events.expect(EventType.INTROSPECT_TOKEN_ERROR)
.client("service-account-cl") .client("service-account-cl")
.user(is(emptyOrNullString())) .user(is(emptyOrNullString()))
.session(is(emptyOrNullString())) .session(is(emptyOrNullString()))
.error(Errors.TOKEN_INTROSPECTION_FAILED)
.assertEvent(); .assertEvent();
} }