Expand my Community achievements bar.

Get ready! An upgraded Experience League Community experience is coming in January.

Implement a Custom UserInfoProcessor for OIDC

Avatar

Level 1

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

Topics help categorize Community content and increase your ability to discover relevant content.

3 Replies

Avatar

Community Advisor

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

 

Hrishikesh Kagane

Avatar

Employee

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.

Avatar

Employee

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;
        }
    }
}