Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,12 @@ protected boolean shouldIgnoreCredentialChange(Event event) {
}

protected boolean shouldIgnoreLogout(Event event) {
String reason = event.getDetails().get(Details.REASON);
// details can be null, e.g. for RP-initiated logouts without a post_logout_redirect_uri
Map<String, String> details = event.getDetails();
if (details == null) {
return false;
}
String reason = details.get(Details.REASON);
return Details.USER_SESSION_EXPIRED_REASON.equals(reason) || Details.INVALID_USER_SESSION_REMEMBER_ME_REASON.equals(reason);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,17 @@ protected SsfSecurityEventToken convertAdminEventToSecurityEventToken(AdminEvent
}

protected boolean isUserSessionExpiration(Event event) {
return EventType.USER_SESSION_DELETED.equals(event.getType()) &&
(Details.INVALID_USER_SESSION_REMEMBER_ME_REASON.equals(event.getDetails().get(Details.REASON))
|| Details.USER_SESSION_EXPIRED_REASON.equals(event.getDetails().get(Details.REASON)));
if (!EventType.USER_SESSION_DELETED.equals(event.getType())) {
return false;
}
// details can be null depending on how the event was constructed
Map<String, String> details = event.getDetails();
if (details == null) {
return false;
}
String reason = details.get(Details.REASON);
return Details.INVALID_USER_SESSION_REMEMBER_ME_REASON.equals(reason)
|| Details.USER_SESSION_EXPIRED_REASON.equals(reason);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.keycloak.ssf.transmitter.event;

import java.util.Map;

import org.keycloak.events.Details;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Unit tests for the logout-ignore predicate of {@link SecurityEventTokenMapper}.
*
* <p>Tests construct the mapper with a {@code null} session because the
* predicate only looks at the event.
*/
class SecurityEventTokenMapperTest {

private final SecurityEventTokenMapper mapper = new SecurityEventTokenMapper(null, null, s -> "https://issuer.example.com");

@Test
void logoutWithoutDetails_isNotIgnored() {
Event event = new Event();
event.setType(EventType.LOGOUT);
// details intentionally left null, which is the RP-initiated logout path
// without post_logout_redirect_uri never calls detail()

assertFalse(mapper.shouldIgnoreLogout(event),
"a real user logout without details must be propagated, not dropped or NPE");
assertTrue(mapper.canConvert(event),
"canConvert must survive a LOGOUT event with null details");
}

@Test
void logoutWithExpiredSessionReason_isIgnored() {
Event event = new Event();
event.setType(EventType.LOGOUT);
event.setDetails(Map.of(Details.REASON, Details.USER_SESSION_EXPIRED_REASON));

assertTrue(mapper.shouldIgnoreLogout(event),
"expired session cleanup is not a real logout and must not emit a SET");
}

@Test
void logoutWithUnrelatedDetails_isNotIgnored() {
Event event = new Event();
event.setType(EventType.LOGOUT);
event.setDetails(Map.of(Details.REDIRECT_URI, "https://rp.example.com/logged-out"));

assertFalse(mapper.shouldIgnoreLogout(event),
"details without a REASON entry must be treated like a real logout");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ private Object eventForType(String eventTypeUri) {
throw new IllegalArgumentException("Test fixture only covers CAEP session-revoked / credential-change. Add a branch when extending.");
}

@Test
void userSessionDeletedWithoutDetails_isNotExpiration() {
Event event = new Event();
event.setType(org.keycloak.events.EventType.USER_SESSION_DELETED);
// details intentionally left null — EventBuilder creates the map
// lazily, so events without any detail() call carry null here

assertFalse(listener.isUserSessionExpiration(event),
"a USER_SESSION_DELETED event without details must not NPE and not count as expiration");
}

// ----- auto-notify-on-login read-only handling -----

private static final String RECEIVER_CLIENT_ID = "receiver";
Expand Down
Loading