Skip to content
Merged
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 @@ -13,30 +13,35 @@
*******************************************************************************/
package org.cloudfoundry.identity.uaa.audit.event;

import tools.jackson.core.type.TypeReference;
import org.cloudfoundry.identity.uaa.audit.AuditEvent;
import org.cloudfoundry.identity.uaa.audit.AuditEventType;
import org.cloudfoundry.identity.uaa.audit.UaaAuditService;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.oauth.UaaOauth2Authentication;
import org.cloudfoundry.identity.uaa.oauth.jwt.Jwt;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.cloudfoundry.identity.uaa.oauth.provider.OAuth2Authentication;
import org.cloudfoundry.identity.uaa.oauth.provider.authentication.OAuth2AuthenticationDetails;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.StringUtils;
import tools.jackson.core.type.TypeReference;

import java.io.Serial;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;

import static org.cloudfoundry.identity.uaa.util.UaaTokenUtils.isJwtToken;
import static org.springframework.util.StringUtils.hasText;

/**
* Base class for UAA events that want to publish audit records.
Expand All @@ -49,6 +54,9 @@ public abstract class AbstractUaaEvent extends ApplicationEvent {

@Serial
private static final long serialVersionUID = -7639844193401892160L;

private static final Logger logger = LoggerFactory.getLogger(AbstractUaaEvent.class);

private final transient String zoneId;

private Authentication authentication;
Expand Down Expand Up @@ -115,35 +123,50 @@ private String getAuthenticationString(Authentication caller) {
builder.append("caller=").append(caller.getName());
}

if (caller.getDetails() != null) {
Object details = caller.getDetails();
if (details != null) {
builder.append(", details=(");
try {
@SuppressWarnings("unchecked")
Map<String, Object> map =
JsonUtils.readValue((String) caller.getDetails(), new TypeReference<Map<String, Object>>(){
});
if (map.containsKey("remoteAddress")) {
builder.append("remoteAddress=").append(map.get("remoteAddress")).append(", ");
}
builder.append("type=").append(caller.getDetails().getClass().getSimpleName());
} catch (Exception _) {
// ignore
builder.append(caller.getDetails());
}
extractRemoteAddress(details).ifPresent(address -> builder.append("remoteAddress=").append(address).append(", "));
builder.append("type=").append(details.getClass().getSimpleName());
appendTokenDetails(caller, builder);
builder.append(")");
}

return builder.toString();
}

protected void appendTokenDetails(Authentication caller, StringBuilder builder) {
private Optional<String> extractRemoteAddress(Object details) {
return switch (details) {
case UaaAuthenticationDetails d -> Optional.ofNullable(d.getOrigin()).filter(StringUtils::hasText);
case OAuth2AuthenticationDetails d -> Optional.ofNullable(d.getRemoteAddress()).filter(StringUtils::hasText);
case WebAuthenticationDetails d -> Optional.ofNullable(d.getRemoteAddress()).filter(StringUtils::hasText);
case Map<?, ?> map -> Optional.ofNullable(map.get("remoteAddress")).map(Object::toString).filter(StringUtils::hasText);
case String jsonBlob -> extractRemoteAddressFromJson(jsonBlob);
default -> {
logger.warn("Unhandled Authentication.details type in audit origin: {}", details.getClass().getName());
yield Optional.empty();
}
};
}

private Optional<String> extractRemoteAddressFromJson(String jsonBlob) {
try {
Map<String, Object> map = JsonUtils.readValue(jsonBlob, new TypeReference<>() {
});
return map == null ? Optional.empty() : extractRemoteAddress(map);
} catch (JsonUtils.JsonUtilException _) {
return Optional.empty();
}
}

private void appendTokenDetails(Authentication caller, StringBuilder builder) {
String tokenValue = null;
if (caller instanceof UaaOauth2Authentication uaaOauth2Authentication) {
tokenValue = uaaOauth2Authentication.getTokenValue();
} else if (caller.getDetails() instanceof OAuth2AuthenticationDetails oAuth2AuthenticationDetails) {
tokenValue = oAuth2AuthenticationDetails.getTokenValue();
}
if (hasText(tokenValue)) {
if (StringUtils.hasText(tokenValue)) {
if (isJwtToken(tokenValue)) {
try {
Jwt token = JwtHelper.decode(tokenValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.springframework.security.core.Authentication;

import java.util.StringJoiner;

import static org.springframework.util.StringUtils.hasText;

/**
* @author Luke Taylor
*/
Expand All @@ -27,7 +31,21 @@ public abstract class AbstractUaaAuthenticationEvent extends AbstractUaaEvent {
}

protected String getOrigin(UaaAuthenticationDetails details) {
return details == null ? "unknown" : details.toString();
if (details == null) {
return "unknown";
}

StringJoiner joiner = new StringJoiner(", ");

if (hasText(details.getOrigin())) {
joiner.add("remoteAddress=" + details.getOrigin());
}

if (hasText(details.getClientId())) {
joiner.add("clientId=" + details.getClientId());
}

return joiner.length() == 0 ? "unknown" : joiner.toString();
}

UaaAuthenticationDetails getAuthenticationDetails() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.cloudfoundry.identity.uaa.audit.AuditEventType;
import org.cloudfoundry.identity.uaa.audit.JdbcAuditService;
import org.cloudfoundry.identity.uaa.audit.UaaAuditService;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.oauth.UaaOauth2Authentication;
import org.cloudfoundry.identity.uaa.oauth.provider.OAuth2Authentication;
import org.cloudfoundry.identity.uaa.oauth.provider.OAuth2Request;
Expand All @@ -16,6 +17,7 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import java.util.Map;

Expand Down Expand Up @@ -69,19 +71,34 @@ void getContextAuthentication() {
}

@Test
void getOrigin() {
void getOrigin_whenMapDetailsHasNoKnownKeys_doesNotLeakMapContents() {
UaaOauth2Authentication authentication = mock(UaaOauth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(authentication.getName()).thenReturn("marissa");
when(authentication.getDetails()).thenReturn(Map.of("misc", "somedetails", "remoteAddress", "external"));
when(authentication.getDetails()).thenReturn(Map.of("grant_type", "password", "username", "marissa", "client_id", "clientid"));
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("marissa")
.contains("client=null")
.contains("misc=somedetails")
.contains("remoteAddress=external")
.contains("details=({");
.doesNotContain("remoteAddress=")
.doesNotContain("grant_type=")
.doesNotContain("username=")
.doesNotContain("client_id=");
}

@Test
void getOrigin_whenMapDetailsContainsRemoteAddress_extractsRemoteAddress() {
UaaOauth2Authentication authentication = mock(UaaOauth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(authentication.getName()).thenReturn("marissa");
when(authentication.getDetails()).thenReturn(Map.of("grant_type", "password", "remoteAddress", "10.0.0.1"));
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("marissa")
.contains("remoteAddress=10.0.0.1")
.doesNotContain("grant_type=");
}

@Test
Expand All @@ -102,9 +119,97 @@ void getOriginDetailsParsed() {
.contains("client=null")
.doesNotContain("misc=somedetails")
.contains("remoteAddress=external")
.contains("type=String")
.doesNotContain("{");
}

@Test
void getOrigin_whenOAuth2AuthenticationWithUser_emitsClientAndUser() {
OAuth2Authentication authentication = mock(OAuth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(oAuth2Request.getClientId()).thenReturn("clientid");
when(authentication.isClientOnly()).thenReturn(false);
when(authentication.getName()).thenReturn("marissa");
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("client=clientid")
.contains("user=marissa");
}

@Test
void getOrigin_whenStringDetailsIsNotJson_omitsRemoteAddress() {
UaaOauth2Authentication authentication = mock(UaaOauth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(authentication.getName()).thenReturn("marissa");
when(authentication.getDetails()).thenReturn("session-id-abc");
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("marissa")
.contains("type=String")
.doesNotContain("remoteAddress=");
}

@Test
void getOrigin_whenJsonDetailsMissingRemoteAddress_omitsRemoteAddress() {
UaaOauth2Authentication authentication = mock(UaaOauth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(authentication.getName()).thenReturn("marissa");
when(authentication.getDetails()).thenReturn("{\"remoteAddress\":null,\"sessionId\":\"abc\"}");
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("marissa")
.contains("type=String")
.doesNotContain("remoteAddress=");
}

@Test
void getOrigin_whenUaaAuthenticationDetails_extractsRemoteAddressFromAccessor() {
UaaOauth2Authentication authentication = mock(UaaOauth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(authentication.getName()).thenReturn("marissa");
UaaAuthenticationDetails uaaDetails = mock(UaaAuthenticationDetails.class);
when(uaaDetails.getOrigin()).thenReturn("10.0.0.1");
when(authentication.getDetails()).thenReturn(uaaDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("details=(remoteAddress=10.0.0.1, type=UaaAuthenticationDetails");
}

@Test
void getOrigin_whenOAuth2AuthenticationDetails_extractsRemoteAddressFromAccessor() {
OAuth2Authentication authentication = mock(OAuth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(oAuth2Request.getClientId()).thenReturn("clientid");
when(authentication.isClientOnly()).thenReturn(true);
OAuth2AuthenticationDetails oauthDetails = mock(OAuth2AuthenticationDetails.class);
when(oauthDetails.getRemoteAddress()).thenReturn("172.18.0.1");
when(authentication.getDetails()).thenReturn(oauthDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("client=clientid")
.contains("details=(remoteAddress=172.18.0.1, type=OAuth2AuthenticationDetails");
}

@Test
void getOrigin_whenWebAuthenticationDetails_extractsRemoteAddressFromAccessor() {
UaaOauth2Authentication authentication = mock(UaaOauth2Authentication.class);
OAuth2Request oAuth2Request = mock(OAuth2Request.class);
when(authentication.getOAuth2Request()).thenReturn(oAuth2Request);
when(authentication.getName()).thenReturn("marissa");
WebAuthenticationDetails webDetails = mock(WebAuthenticationDetails.class);
when(webDetails.getRemoteAddress()).thenReturn("192.168.1.5");
when(authentication.getDetails()).thenReturn(webDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
String originString = event.getOrigin(authentication);
assertThat(originString).contains("marissa")
.contains("details=(remoteAddress=192.168.1.5, type=WebAuthenticationDetails");
}

@Test
void getAuthenticationJsonWebTokenValue() {
String originTokenString = event.getOrigin(mockAuthenticationWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYXJpc3NhIiwiaXNzIjoidWFhIn0.omitted"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ void getOriginFromRequest() {

assertThat(origin).contains("remoteAddress=127.10.10.10")
.contains("clientId=client-id")
.contains("sessionId=<SESSION>");
.doesNotContain("sessionId");
}
}
Loading
Loading