From 9d5a6e7035369009e2e7924a90b103c3e2ee9c8e Mon Sep 17 00:00:00 2001 From: Thumimku Date: Thu, 6 Jun 2024 13:58:30 +0530 Subject: [PATCH] impersonation audit logs --- .../pom.xml | 9 + .../impl/OAuth2AccessTokenHandler.java | 175 ++++++++++++++++-- .../identity/auth/service/util/Constants.java | 26 +++ .../auth/valve/AuthenticationValve.java | 2 + pom.xml | 8 + 5 files changed, 208 insertions(+), 12 deletions(-) diff --git a/components/org.wso2.carbon.identity.auth.service/pom.xml b/components/org.wso2.carbon.identity.auth.service/pom.xml index 46eeca47..0d30fde6 100644 --- a/components/org.wso2.carbon.identity.auth.service/pom.xml +++ b/components/org.wso2.carbon.identity.auth.service/pom.xml @@ -70,6 +70,14 @@ org.wso2.carbon.identity.event.handler.accountlock org.wso2.carbon.identity.handler.event.account.lock + + org.json.wso2 + json + + + org.wso2.orbit.com.nimbusds + nimbus-jose-jwt + org.testng testng @@ -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}", !org.wso2.carbon.identity.auth.service.internal, diff --git a/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/handler/impl/OAuth2AccessTokenHandler.java b/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/handler/impl/OAuth2AccessTokenHandler.java index 060d21be..5c59c94b 100644 --- a/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/handler/impl/OAuth2AccessTokenHandler.java +++ b/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/handler/impl/OAuth2AccessTokenHandler.java @@ -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; @@ -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; /** @@ -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"; @@ -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())) { @@ -138,6 +140,8 @@ protected AuthenticationResult doAuthenticate(MessageContext messageContext) { return authenticationResult; } + handleImpersonatedAccessToken(authenticationContext, accessToken, oAuth2IntrospectionResponseDTO); + authenticationResult.setAuthenticationStatus(AuthenticationStatus.SUCCESS); User authorizedUser = oAuth2IntrospectionResponseDTO.getAuthorizedUser(); @@ -145,22 +149,23 @@ protected AuthenticationResult doAuthenticate(MessageContext messageContext) { 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 { @@ -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) { @@ -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 mayActClaimSet = (Map) claimsSet.getClaim(Constants.ACT); + return mayActClaimSet.get(Constants.SUB); + } + return null; + } } diff --git a/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/util/Constants.java b/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/util/Constants.java index fa6f6d03..7c925f73 100644 --- a/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/util/Constants.java +++ b/components/org.wso2.carbon.identity.auth.service/src/main/java/org/wso2/carbon/identity/auth/service/util/Constants.java @@ -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"; } diff --git a/components/org.wso2.carbon.identity.auth.valve/src/main/java/org/wso2/carbon/identity/auth/valve/AuthenticationValve.java b/components/org.wso2.carbon.identity.auth.valve/src/main/java/org/wso2/carbon/identity/auth/valve/AuthenticationValve.java index db5ef1f3..85b308aa 100644 --- a/components/org.wso2.carbon.identity.auth.valve/src/main/java/org/wso2/carbon/identity/auth/valve/AuthenticationValve.java +++ b/components/org.wso2.carbon.identity.auth.valve/src/main/java/org/wso2/carbon/identity/auth/valve/AuthenticationValve.java @@ -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"; @@ -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) { diff --git a/pom.xml b/pom.xml index 2b07a1c6..715f7e79 100644 --- a/pom.xml +++ b/pom.xml @@ -162,6 +162,11 @@ json ${json.wso2.version} + + org.wso2.orbit.com.nimbusds + nimbus-jose-jwt + ${nimbusds.version} + org.wso2.carbon.identity.organization.management org.wso2.carbon.identity.organization.management.authz.service @@ -357,6 +362,9 @@ [6.2.18, 8.0.0) + 7.9.0.wso2v1 + [7.3.0,8.0.0) + 1.1.14 [1.0.0, 2.0.0)