We have been trying to test changing our login from SAML to OIDC with the new AEM support for OIDC (https://experienceleaguecommunities.adobe.com/t5/forums/postpage/board-id/adobe-experience-manager-q...).
We are using Salesforce as the IDP so the additional attributes we need to write to the user profile live in the ID Token. This isn't supported out of the box so we have to create a custom UserInfoProcessor. We've tried copying the Sling version https://github.com/apache/sling-org-apache-sling-auth-oauth-client/blob/master/src/main/java/org/apa... but on deploy we are having issues with the dependencies resolving in the bundle.
Has anyone had any luck with their own custom implementation of UserInfoProcessor?
Topics help categorize Community content and increase your ability to discover relevant content.
Views
Replies
Total Likes
Hi @Psythe ,
Try this:
For ID Token attributes (which is your Salesforce case), you don't need a custom UserInfoProcessor. Here's what to do:
1. Use the Built-in Configuration:
AEM's native OIDC support can map attributes from both ID tokens and UserInfo endpoints. Configure the property mapping in your SlingUserInfoProcessor config
{
"user.propertyMapping": [
"profile/email=email",
"profile/givenName=given_name",
"profile/familyName=family_name"
]
}
The dependency resolution issue you're facing is common. Here's the fix:
In your core/pom.xml:
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.auth.oauth-client</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
In your bundle plugin configuration:
<Import-Package>
org.apache.sling.auth.oauth_client.*;version="[1.0,2)",
*
</Import-Package>
Implementation Pattern:
@Component(service = UserInfoProcessor.class)
public class CustomUserInfoProcessor implements UserInfoProcessor {
@Override
public void process(UserInfo userInfo, Map<String, Object> claims) {
// Extract from ID token claims
String customAttr = (String) claims.get("salesforce_custom_field");
userInfo.getProfile().put("customProperty", customAttr);
}
}
The default Sling implementation handles most cases - only customize if absolutely necessary Adobe Experience Manager
Use scope=provided to avoid embedding conflicting versions
Make sure the bundle is in "Active" state in Felix Console after deployment
Test locally on SDK before deploying to Cloud
Check /system/console/bundles for "Resolved" vs "Active" state
Use -conditionalpackage or embed only if it's not available as OSGi bundle
Views
Replies
Total Likes
Hello @Psythe
OIDC Authentication Handler on Publish is documented here :
https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/open-id...
Based on the Documentation, the recommended pattern is:
1. Use the built‑in OIDC components only:
org.apache.sling.auth.oauth_client.impl.OidcConnectionImpl~<id>.cfg.json
Defines the connection to your IdP (Salesforce, Azure, etc.): baseUrl or explicit endpoints, clientId, clientSecret/PKCE, scopes (must include openid).
org.apache.sling.auth.oauth_client.impl.OidcAuthenticationHandler~<id>.cfg.json
Protects specific paths and wires them to the OIDC connection: path, callbackUri (…/j_security_check), defaultConnectionName, idp.
org.apache.sling.auth.oauth_client.impl.SlingUserInfoProcessor~<id>.cfg.json
Standard processor for ID Token / UserInfo claims (groups, basic profile, token storage).
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncHandler~<id>.cfg.json
Controls how users/groups are created & updated in AEM (property mapping, expiry, dynamic membership).
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalLoginModuleFactory~<id>.cfg.json
Binds the sync handler to the IdP in Oak.
2. For custom profile/group logic, implement a custom UserInfoProcessor, not a custom handler:
The doc explicitly says you can:
“Optional: Implement a Custom UserInfoProcessor… If additional non-standard operations must be performed, a custom implementation of the UserInfoProcessor…”
So to write Salesforce‑specific attributes into the AEM user:
- Keep using the official OidcConnectionImpl + OidcAuthenticationHandler.
- Create your own OSGi component that implements the UserInfoProcessor SPI available in your runtime.
- Give it a higher service.ranking so it overrides the default SlingUserInfoProcessorImpl.
- In that class, read the Salesforce claims (ID Token and/or UserInfo JSON) and map them to AEM user properties or groups.
3. Avoid copying the Sling OIDC client bundle into your project.
The article does not ask you to deploy the full org-apache-sling-auth-oauth-client bundle yourself, and doing so causes the dependency/API‑region issues you’re seeing. The supported extension point is the UserInfoProcessor interface, not embedding upstream Sling internals.
Hi @Psythe
SlingUserInfoProcessor has a lot of dependencies that are embedded in the oidc granite bundle. If you want to modify it, you need to include all of them. You can find the list in the sling project.
I suggest instead to implement the org.apache.sling.auth.oauth_client.spi.UserInfoProcessor interface. In this way you need only to include following dependencies:
<!-- Optional: Gson for JSON parsing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>provided</scope>
</dependency>
<!-- Required: JetBrains Annotations -->
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Required: Sling OAuth Client - provides UserInfoProcessor SPI -->
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.auth.oauth-client</artifactId>
<version>0.1.6</version>
<scope>provided</scope>
</dependency>
A sample code for a UserInfoProcessor:
package com.wintergw2025.core.services;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.apache.sling.auth.oauth_client.spi.OidcAuthCredentials;
import org.apache.sling.auth.oauth_client.spi.UserInfoProcessor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* Sample UserInfoProcessor showing how to:
* - Extract access_token and id_token from the token response
* - Return hardcoded user attributes
* - Return hardcoded group memberships
*/
@Component(service = UserInfoProcessor.class, property = {"service.ranking:Integer=50"})
@Designate(ocd = SampleUserInfoProcessor.Config.class, factory = true)
public class SampleUserInfoProcessor implements UserInfoProcessor {
private static final Logger logger = LoggerFactory.getLogger(SampleUserInfoProcessor.class);
@ObjectClassDefinition(name = "Sample UserInfo Processor")
@interface Config {
@AttributeDefinition(name = "Connection Name", description = "OIDC Connection Name")
String connection();
}
private final String connection;
@Activate
public SampleUserInfoProcessor(Config config) {
this.connection = config.connection();
logger.info("SampleUserInfoProcessor activated for connection: {}", connection);
}
@Override
public @NotNull OidcAuthCredentials process(
@Nullable String userInfo,
@NotNull String tokenResponse,
@NotNull String oidcSubject,
@NotNull String idp) {
// === Extract tokens from token response ===
JsonObject tokenJson = JsonParser.parseString(tokenResponse).getAsJsonObject();
String accessToken = tokenJson.has("access_token") ? tokenJson.get("access_token").getAsString() : null;
String idToken = tokenJson.has("id_token") ? tokenJson.get("id_token").getAsString() : null;
logger.info("Access Token: {}", accessToken != null ? accessToken.substring(0, Math.min(30, accessToken.length())) + "..." : "null");
logger.info("ID Token: {}", idToken != null ? idToken.substring(0, Math.min(30, idToken.length())) + "..." : "null");
// === Decode ID Token to see claims (optional) ===
if (idToken != null) {
JsonObject claims = decodeJwtPayload(idToken);
logger.info("ID Token claims: {}", claims);
}
// === Create credentials ===
OidcAuthCredentials credentials = new OidcAuthCredentials(oidcSubject, idp);
credentials.setAttribute(".token", "");
// Implement your logic to retrieve the attributes...
// === Set hardcoded user attributes ===
// These will be synced to user node based on DefaultSyncHandler propertyMapping
credentials.setAttribute("profile/given_name", "John");
credentials.setAttribute("profile/family_name", "Doe");
credentials.setAttribute("profile/email", "john.doe@example.com");
credentials.setAttribute("profile/department", "Engineering");
// === Add hardcoded group memberships ===
// These groups will be created/synced based on DefaultSyncHandler configuration
credentials.addGroup("authenticated-users");
credentials.addGroup("premium-members");
credentials.addGroup("content-authors");
return credentials;
}
@Override
public @NotNull String connection() {
return connection;
}
/** Decode JWT payload (middle part) */
private JsonObject decodeJwtPayload(String jwt) {
try {
String[] parts = jwt.split("\\.");
if (parts.length != 3) return null;
String payload = parts[1];
payload = payload + "====".substring(0, (4 - payload.length() % 4) % 4);
payload = payload.replace('-', '+').replace('_', '/');
return JsonParser.parseString(new String(Base64.getDecoder().decode(payload), StandardCharsets.UTF_8)).getAsJsonObject();
} catch (Exception e) {
return null;
}
}
}
Views
Replies
Total Likes
Views
Like
Replies
Views
Likes
Replies