Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audit log for impersonated resource access #273

Merged
merged 1 commit into from
Jun 12, 2024
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
9 changes: 9 additions & 0 deletions components/org.wso2.carbon.identity.auth.service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@
<groupId>org.wso2.carbon.identity.event.handler.accountlock</groupId>
<artifactId>org.wso2.carbon.identity.handler.event.account.lock</artifactId>
</dependency>
<dependency>
<groupId>org.json.wso2</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.orbit.com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
Expand Down Expand Up @@ -204,6 +212,7 @@
org.wso2.carbon.identity.organization.management.service.exception; version="${org.wso2.carbon.identity.organization.management.core.version.range}",
org.wso2.carbon.identity.handler.event.account.lock.exception;
version="${identity.event.handler.account.lock.version.range}",
com.nimbusds.jwt; version="${nimbusds.osgi.version.range}",
</Import-Package>
<Export-Package>
!org.wso2.carbon.identity.auth.service.internal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@

package org.wso2.carbon.identity.auth.service.handler.impl;

import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.apache.catalina.connector.Request;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHeaders;
import org.json.JSONObject;
import org.slf4j.MDC;
import org.wso2.carbon.CarbonConstants;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants;
import org.wso2.carbon.identity.application.common.model.ProvisioningServiceProviderType;
Expand Down Expand Up @@ -51,14 +55,11 @@
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
import org.wso2.carbon.identity.oauth2.validators.RefreshTokenValidator;

import java.text.ParseException;
import java.util.Map;
import java.util.Optional;

import static org.wso2.carbon.identity.auth.service.util.AuthConfigurationUtil.isAuthHeaderMatch;
import static org.wso2.carbon.identity.auth.service.util.Constants.AUTHENTICATION_TYPE;
import static org.wso2.carbon.identity.auth.service.util.Constants.IDP_NAME;
import static org.wso2.carbon.identity.auth.service.util.Constants.IS_FEDERATED_USER;
import static org.wso2.carbon.identity.auth.service.util.Constants.OAUTH2_ALLOWED_SCOPES;
import static org.wso2.carbon.identity.auth.service.util.Constants.OAUTH2_VALIDATE_SCOPE;
import static org.wso2.carbon.identity.oauth2.OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER;

/**
Expand All @@ -69,6 +70,7 @@
public class OAuth2AccessTokenHandler extends AuthenticationHandler {

private static final Log log = LogFactory.getLog(OAuth2AccessTokenHandler.class);
private static final Log AUDIT = CarbonConstants.AUDIT_LOG;
private final String OAUTH_HEADER = "Bearer";
private final String CONSUMER_KEY = "consumer-key";
private final String SERVICE_PROVIDER = "serviceProvider";
Expand Down Expand Up @@ -117,7 +119,7 @@ protected AuthenticationResult doAuthenticate(MessageContext messageContext) {
oAuth2TokenValidationService.buildIntrospectionResponse(requestDTO);

IdentityUtil.threadLocalProperties.get()
.put(AUTHENTICATION_TYPE, oAuth2IntrospectionResponseDTO.getAut());
.put(Constants.AUTHENTICATION_TYPE, oAuth2IntrospectionResponseDTO.getAut());

if (!oAuth2IntrospectionResponseDTO.isActive() ||
RefreshTokenValidator.TOKEN_TYPE_NAME.equals(oAuth2IntrospectionResponseDTO.getTokenType())) {
Expand All @@ -138,29 +140,32 @@ protected AuthenticationResult doAuthenticate(MessageContext messageContext) {
return authenticationResult;
}

handleImpersonatedAccessToken(authenticationContext, accessToken, oAuth2IntrospectionResponseDTO);

authenticationResult.setAuthenticationStatus(AuthenticationStatus.SUCCESS);

User authorizedUser = oAuth2IntrospectionResponseDTO.getAuthorizedUser();
if (authorizedUser != null) {
authenticationContext.setUser(authorizedUser);
if (authorizedUser instanceof AuthenticatedUser) {
IdentityUtil.threadLocalProperties.get()
.put(IS_FEDERATED_USER, ((AuthenticatedUser) authorizedUser).isFederatedUser());
.put(Constants.IS_FEDERATED_USER,
((AuthenticatedUser) authorizedUser).isFederatedUser());
IdentityUtil.threadLocalProperties.get()
.put(IDP_NAME, ((AuthenticatedUser) authorizedUser).getFederatedIdPName());
.put(Constants.IDP_NAME, ((AuthenticatedUser) authorizedUser).getFederatedIdPName());
} else {
AuthenticatedUser authenticatedUser = new AuthenticatedUser(authorizedUser);
IdentityUtil.threadLocalProperties.get()
.put(IS_FEDERATED_USER, authenticatedUser.isFederatedUser());
.put(Constants.IS_FEDERATED_USER, authenticatedUser.isFederatedUser());
IdentityUtil.threadLocalProperties.get()
.put(IDP_NAME, authenticatedUser.getFederatedIdPName());
.put(Constants.IDP_NAME, authenticatedUser.getFederatedIdPName());
}
}

authenticationContext.addParameter(CONSUMER_KEY, oAuth2IntrospectionResponseDTO.getClientId());
authenticationContext.addParameter(OAUTH2_ALLOWED_SCOPES,
authenticationContext.addParameter(Constants.OAUTH2_ALLOWED_SCOPES,
OAuth2Util.buildScopeArray(oAuth2IntrospectionResponseDTO.getScope()));
authenticationContext.addParameter(OAUTH2_VALIDATE_SCOPE,
authenticationContext.addParameter(Constants.OAUTH2_VALIDATE_SCOPE,
AuthConfigurationUtil.getInstance().isScopeValidationEnabled());
String serviceProvider = null;
try {
Expand Down Expand Up @@ -197,6 +202,89 @@ protected AuthenticationResult doAuthenticate(MessageContext messageContext) {
return authenticationResult;
}

/**
* Handles the logging of impersonated access tokens. This method extracts the claims from the given
* access token and checks if it represents an impersonation request. If impersonation is detected,
* it logs the impersonation event with relevant details such as subject, impersonator, resource path,
* HTTP method, client ID, and scope. The method ensures that only non-GET requests are logged for audit purposes.
*
* @param authenticationContext The authentication context containing the authentication request details.
* @param accessToken The access token to be inspected for impersonation.
* @param introspectionResponseDTO The introspection response containing token details.
*/
private void handleImpersonatedAccessToken(AuthenticationContext authenticationContext,
String accessToken,
OAuth2IntrospectionResponseDTO introspectionResponseDTO) {

try {
// Extract claims from the access token
SignedJWT signedJWT = getSignedJWT(accessToken);
JWTClaimsSet claimsSet = getClaimSet(signedJWT);
if (claimsSet != null) {
String subject = resolveSubject(claimsSet);
String impersonator = resolveImpersonator(claimsSet);
// Check if the token represents an impersonation request
if (impersonator != null) {
MDC.put(Constants.IMPERSONATOR, impersonator);
String scope = introspectionResponseDTO.getScope();
String clientId = introspectionResponseDTO.getClientId();
String requestUri = authenticationContext.getAuthenticationRequest().getRequestUri();
String httpMethod = authenticationContext.getAuthenticationRequest().getMethod();

// Ensure it's not a GET request before logging
if (!Constants.GET.equals(httpMethod)) {
// Prepare data for audit log
JSONObject data = new JSONObject();
data.put(Constants.SUBJECT, subject);
data.put(Constants.IMPERSONATOR, impersonator);
data.put(Constants.RESOURCE_PATH, requestUri);
data.put(Constants.HTTP_METHOD, httpMethod);
data.put(Constants.CLIENT_ID, clientId);
data.put(Constants.SCOPE, scope);

String action;

switch (httpMethod) {
case Constants.PATCH:
action = Constants.IMPERSONATION_RESOURCE_MODIFICATION;
break;
case Constants.POST:
action = Constants.IMPERSONATION_RESOURCE_CREATION;
break;
case Constants.DELETE:
action = Constants.IMPERSONATION_RESOURCE_DELETION;
break;
default:
action = Constants.IMPERSONATION_RESOURCE_ACCESS;
break;
}
// Log the audit event
AUDIT.info(createAuditMessage(impersonator, action, subject, data, Constants.AUTHORIZED));
}
}
}
} catch (IdentityOAuth2Exception e) {
// Ignore IdentityOAuth2Exception since this is an audit log section
}
}

/**
* To create an audit message based on provided parameters.
*
* @param action Activity
* @param target Target affected by this activity.
* @param data Information passed along with the request.
* @param resultField Result value.
* @return Relevant audit log in Json format.
*/
private String createAuditMessage(String subject, String action, String target, JSONObject data, String resultField) {

String auditMessage =
Constants.INITIATOR + "=%s " + Constants.ACTION + "=%s " + Constants.TARGET + "=%s "
+ Constants.DATA + "=%s " + Constants.OUTCOME + "=%s";
return String.format(auditMessage, subject, action, target, data, resultField);
}

@Override
public void init(InitConfig initConfig) {

Expand Down Expand Up @@ -385,4 +473,67 @@ private void setProvisioningServiceProviderThreadLocal(String oauthAppConsumerKe
IdentityApplicationManagementUtil.setThreadLocalProvisioningServiceProvider(provisioningServiceProvider);
}
}

/**
* Get the SignedJWT by parsing the subjectToken.
*
* @param token Token sent in the request
* @return SignedJWT
* @throws IdentityOAuth2Exception Error when parsing the subjectToken
*/
private SignedJWT getSignedJWT(String token) throws IdentityOAuth2Exception {

SignedJWT signedJWT;
if (StringUtils.isEmpty(token)) {
return null;
}
try {
signedJWT = SignedJWT.parse(token);
return signedJWT;
} catch (ParseException e) {
throw new IdentityOAuth2Exception("Error while parsing the JWT", e);
}
}

/**
* Retrieve the JWTClaimsSet from the SignedJWT.
*
* @param signedJWT SignedJWT object
* @return JWTClaimsSet
* @throws IdentityOAuth2Exception Error when retrieving the JWTClaimsSet
*/
public static JWTClaimsSet getClaimSet(SignedJWT signedJWT) throws IdentityOAuth2Exception {

try {
if (signedJWT != null){
return signedJWT.getJWTClaimsSet();
}
return null;
} catch (ParseException e) {
throw new IdentityOAuth2Exception("Error when retrieving claimsSet from the JWT", e);
}
}

/**
* The default implementation creates the subject from the Sub attribute.
* To translate between the federated and local user store, this may need some mapping.
* Override if needed
*
* @param claimsSet all the JWT claims
* @return The subject, to be used
*/
private String resolveSubject(JWTClaimsSet claimsSet) {

return claimsSet.getSubject();
}

private String resolveImpersonator(JWTClaimsSet claimsSet) {

if (claimsSet.getClaim(Constants.ACT) != null) {

Map<String, String> mayActClaimSet = (Map) claimsSet.getClaim(Constants.ACT);
return mayActClaimSet.get(Constants.SUB);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,30 @@ public class Constants {
public static final String RESOURCE_ACCESS_CONTROL_V2_FILE = "resource-access-control-v2.xml";
public static final String AUTHENTICATION_TYPE = "authenticationType";
public final static String VALIDATE_LEGACY_PERMISSIONS = "validateLegacyPermissions";

// Audit Log Constants.
public static final String INITIATOR = "Initiator";
public static final String ACTION = "Action";
public static final String TARGET = "Target";
public static final String DATA = "Data";
public static final String OUTCOME = "Outcome";

// Impersonation Constants.
public static final String ACT = "act";
public static final String SUB = "sub";
public static final String GET = "GET";
public static final String POST = "POST";
public static final String PATCH = "PATCH";
public static final String DELETE = "DELETE";
public static final String AUTHORIZED = "AUTHORIZED";
public static final String IMPERSONATION_RESOURCE_MODIFICATION = "resource-modification-via-impersonation";
public static final String IMPERSONATION_RESOURCE_ACCESS = "resource-access-via-impersonation";
public static final String IMPERSONATION_RESOURCE_DELETION = "resource-deletion-via-impersonation";
public static final String IMPERSONATION_RESOURCE_CREATION = "resource-creation-via-impersonation";
public static final String SUBJECT = "subject";
public static final String IMPERSONATOR = "impersonator";
public static final String RESOURCE_PATH = "ResourcePath";
public static final String HTTP_METHOD = "httpMethod";
public static final String CLIENT_ID = "clientId";
public static final String SCOPE = "scope";
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class AuthenticationValve extends ValveBase {
private static final String USER_AGENT = "User-Agent";
private static final String REMOTE_ADDRESS = "remoteAddress";
private static final String SERVICE_PROVIDER = "serviceProvider";
private static final String IMPERSONATOR = "impersonator";
private final String CLIENT_COMPONENT = "clientComponent";
private final String REST_API_CLIENT_COMPONENT = "REST API";
private static final String AUTH_USER_TENANT_DOMAIN = "authUserTenantDomain";
Expand Down Expand Up @@ -275,6 +276,7 @@ private void unsetMDCThreadLocals() {
MDC.remove(USER_AGENT);
MDC.remove(REMOTE_ADDRESS);
MDC.remove(SERVICE_PROVIDER);
MDC.remove(IMPERSONATOR);
}

private boolean isLoggableParam(String param) {
Expand Down
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@
<artifactId>json</artifactId>
<version>${json.wso2.version}</version>
</dependency>
<dependency>
<groupId>org.wso2.orbit.com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbusds.version}</version>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.organization.management</groupId>
<artifactId>org.wso2.carbon.identity.organization.management.authz.service</artifactId>
Expand Down Expand Up @@ -357,6 +362,9 @@
<org.wso2.carbon.identity.oauth.import.version.range>[6.2.18, 8.0.0)
</org.wso2.carbon.identity.oauth.import.version.range>

<nimbusds.version>7.9.0.wso2v1</nimbusds.version>
<nimbusds.osgi.version.range>[7.3.0,8.0.0)</nimbusds.osgi.version.range>

<org.wso2.carbon.identity.organization.management.version>1.1.14
</org.wso2.carbon.identity.organization.management.version>
<org.wso2.carbon.identity.organization.management.version.range>[1.0.0, 2.0.0)
Expand Down
Loading