Managing User Context and (Error) Logging in Salesforce Auth Providers and Named/External Credentials

An Auth Provider Utility to support the “Per User Principal” Identity type and basic (error) logging

Justus van den Berg
13 min readJun 7, 2023

In my story OAuth 2.0 JWT Client Credentials Auth Provider using Salesforce Named and External Credentials I explained how to leverage a custom Auth Provider to create a secure, reusable and configurable JWT Client Credentials integration using Named and External Credentials.

This integration was purely focused on a “Named Principal” Identity type. That means that every Salesforce user using this named credential, is actually using the same external integration user, technically called the “subject”.
This is great for system integrations that need to have access to all data or integrations where you need to see data as a certain group, for example aggregated marketing data at a team level. But when it comes to user specific data it comes with a little additional work.

In this article I’ll explain what is needed for scalable integrations that needs to be in a user context and run through an Apex utility that I built, that supports my approach.

It’s not a definitive answer but hopefully it will give you enough inspiration to get the idea and extend or create something that fits your requirements.

TL;DR The Github repository can be found here: https://github.com/jfwberg/lightweight-auth-provider-util

Important

Security is no easy subject: Before implementing this (or any) solution, always validate what you’re doing with a certified security expert and your certified implementation partner.

User Mapping Options

The core requirement for user context is to create and store a mapping between the logged in Salesforce user and the Subject Identifier that same user has in the external system (i.e. a username).
There are two main solutions:

  1. Create a custom field on the user record for each external system identifier, this mimics the FederationIdentifier field functionality
  2. Create a custom object to store your tailored mapping records

The custom field approach works well for smaller implementations with a hand full of different external systems that do not require any business or additional security.

When you get to larger, more complex implementations I personally prefer to move to a custom sObject approach for a number of reasons:

  • Separation of concerns: It’s not the User object’s job to hold mapping information for multiple systems.
    What you want to prevent is the need to deploy User Object related metadata if you’re building an integration that is completely separate.
  • Separation of Logic: If there is any logic that needs to be applied on a mapping record, either in Apex or Flow, this should not be the concern of the User object, this should be managed in it’s own place.
    Some logic examples you can think about are things like allowed office hours, break glass accounts or simply checking that an Auth Provider exists in the metadata.
  • Extensibility: If you want to capture any related data to external logins, storing this data in the same or a related sObject as the mapping makes it easier to extend. If you need a field to store the date a user last logged in or had an error, you simply put it with the mapping instead of adding another field to aUser record.
  • Multi Team Development: If you work with multiple independent development teams that build integrations, you can create a reusable asset that can be implemented by all teams. This creates a standardised way of user mapping in a single place. By doing that you improve the security and manageability of external credentials.
    A single team team can own the logic for mapping and other teams don’t have to worry about creating multiple solutions for integration user management.
  • User management: External user provisioning can now be done in a single place. It can be reported on, it can be integrated in user provisioning automation with simply the access to the integration object.
  • One reusable solution: Having a single solution for all your integrations prevents that different teams go of on their own. From a security team perspective it’s preferable to have a single approved solution that can be monitored instead of multiple potential dangerous solutions that each require it’s own overhead to maintain and monitor.

As with every solution there are a number of downsides to consider:

  • You create overhead: it’s an extra object or objects you need to manage and educate your user provisioning team on.
  • You need to govern security around this object; when configured wrongly you can give users without setup access the power to create mappings. Triggers with CRUD checks on access to auth providers can validate all this, but you need to build this validation.
  • You might work with with registration handlers and require creation of user mappings at the time of registration. This is another bit of overhead (or flexibility) to take into consideration.
  • You create a single point of failure for all your user based integrations. If somebody deletes all the mappings all your integrations stop working. Of course the same can happen if you put everything on the User object. It is something to keep in mind though and arrange your monitoring and backup and restore process accordingly.

The architecture for this approach can be different for each org and can have more pros and cons than I mentioned. It can seem a little overkill for a “simple” user mapping.
If you start to scale up the management and tracking to say 50 different integrations with 25,000 users split over 10 individual development teams and a dedicated user management team, you can see where the separation begins to make sense. Although you should probably move to a middleware solution, but that is out of scope for this one.

As always it is a trade-off between manageability, flexibility, extensibility and security versus the extra work required.

Lightweight - Auth Provider Util v2

I created a lightweight utility that allows you to create a custom mapping between a Salesforce user and a user from an external system. This can be used when you are connecting through a custom Auth Provider.

This utility also provides basic logging functions that can be used for debugging or monitoring any customization that you have in your custom Auth Provider.

The utility consists of an App called “Auth Provider Util” and two sObjects: “Auth Provider Log” for storing the logs and “Auth Provider User Mapping” for storing the user mappings records.

There are two platform event objects. Auth Provider classes cannot use DML, so we can use a platform event with a trigger to create logging records and update the login information.

00 - Assign permissions to Automated Process User

Beginning the Spring ‘24 release, platform events are now running as the Automated Process User where they were in teh running user context before. This made the platform events fail due to access issues. The proper way to solve this issue is by creating a “PlatformEventSubscriberConfig” metadata type for each platform event where you specify the user. But that is not easily automated. You can use the code snippet below to assign a special permission set to the Automated Process User to solve the issue.

insert new PermissionSetAssignment(
AssigneeId = [SELECT Id FROM User WHERE alias = 'autoproc']?.Id,
PermissionSetId = [SELECT Id FROM PermissionSet WHERE Name = 'Lightweight_Auth_Provider_Util_AutoProc']?.Id
);

01 - User Mapping

User mappings allow you to create a “Per User Principal” integration with a custom Auth Provider in combination with Named/External credentials.

1) Open the “Auth Provider Util” app through the app launcher

2) Go to the “Auth Provider User Mapping” tab
3) Click “New
4) Select the Source Org User

  • Give the API Name for the AuthProvider (This will be validated against the Metadata
  • Give the name that is used in the external system to identify the user/subject
  • Other fields can be ignored

5) Press “Save

You now have successfully created a mapping that you can use in your custom Auth Provider logic.

1.1 - User Mapping methods

// A method to check if a user mapping exists, your code can add custom validations using this method
Boolean userMappingExists = lwt.AuthProviderUtil.checkUserMappingExists(String authProviderName, String userId);

// Method to get the target subject name from the user mapping
String subject = lwt.AuthProviderUtil.getSubjectFromUserMapping(String authProviderName, String userId);

2 - Login Statistics

The auth provider mapping records contain two fields regarding the login:

  • Last Login Date, This is the last time a user has retrieved a new or refresh token through the auth provider
  • Number of logins, This is the total times a token/refresh token has been requested by the user

Using these fields are completely optional. They can be handy to track usage after the implementation. They do take up platform events so keep that in mind before hitting any limits.

2.1 - Login Statistics methods

// Method to update the login detail fields for a specific user (void)
lwt.AuthProviderUtil.updateMappingLoginDetails(String authProviderName, String userId);

3 - Logging

In order to create a record from an Auth Provider a platform event is required due to the lack of DML support. There is only one type of logging event, but you can implement this how you want, i.e. success, error warning. Errors only will be the most common.

Keep in mind creating logs will affect your Platform Event limits so use accordingly.

If a mapping record with the Salesforce user Id and Auth Provider Name exists, the field “Last Auth Provider Log” record will be updated with a related record sot it can easily be traced when debugging a specific user.

Logs have a logId field that needs to be unique, use this with a GUID to keep your logs traceable.

3.1 - Logging methods

// Method to insert a log entry
lwt.AuthProviderUtil.insertLog(String authProviderName, String userId, String logId, String message);

4 - View Logs

  1. Open the “Auth Provider Util” app through the app launcher
  2. Go to the “Auth Provider Logs” tab
  3. Open any of the logs you’ll need you can reference them by the log Id

5 - Security notes

There are two permission sets included:

  1. Auth Provider Util Admin: Use this permission set for users that manage users mappings and are allowed to see the logs. This Permission Set gives full access.
  2. Auth Provider Util User: Use this for users who only need to be able to use an Auth Provider. This permission set gives users only access to read their own mapping record, access to create a log event and update the login details for their own mapping record.

The Sharing model is set to private so users only have access to records they own.

The OwnerId field is set to the Salesforce user by a trigger to manage this.

!! As with every user mapping, whether its custom or the Federation Identifier fields, make sure the correct Admin access is supplied so users cannot impersonate other users by updating fields. Users should only have read access to the mapping object to prevent impersonation. Again, speak to your security expert to validate all settings.!!

6 - Callable Implementation

To keep packages loosely coupled the callable interface has been implemented. Not every package Auth Provider you use will need this package, to allow for loose coupling the Callable Interface has been implemented.

/**
* Implementation example where the class is dymically instanciated
* and each method is resolved at run time.
* Use this if you want to loosely couple the package
*/
Callable authProviderInstance = (Callable) Type.forName('lwt.AuthProviderUtil').newInstance();

Object insertLogResult;
insertLogResult = authProviderInstance.call('insertLog', new Map<String, Object> {
'authProviderName' => authProviderName,
'userId' => userId,
'logId' => logId,
'message' => message
});
Boolean checkUserMappingExistsResult;
checkUserMappingExistsResult = (Boolean) authProviderInstance.call('checkUserMappingExists', new Map<String, Object> {
'authProviderName' => authProviderName,
'userId' => userId
});
Object updateMappingLoginDetailsResult;
updateMappingLoginDetailsResult = authProviderInstance.call('updateMappingLoginDetails', new Map<String, Object> {
'authProviderName' => authProviderName,
'userId' => userId
});
String getSubjectFromUserMappingResult;
getSubjectFromUserMappingResult = (String) authProviderInstance.call('getSubjectFromUserMapping', new Map<String, Object> {
'authProviderName' => authProviderName,
'userId' => userId
});
try{
authProviderInstance.call('invalid method',null);
throw new StringException('This part should not be reached');
}catch(Exception e){
System.debug(e.getMessage());
}

Per User Principle Setup Notes

Credential Storage

A difference between system wide Named Principals and and Per User Principals is that the user will need access to the sObject where the tokens are stored: "User External Credentials". Users need CRUD access. This can be assigned using a profile or as part of a permission set. (See the links below for full details)

Per User Principal Permission Set Mapping

The Identity type “Per User Principal” or “Named Principal” is set on the permission set mapping related list located on the external credential.

  1. Go to setup >> Named Credentials > External Credentials Tab and Open the External Credential you want configure as a Per User Principal.
  2. Scroll down to the Permission Set Mapping related list and press the "New" button.
  3. Select the permission set associated with the external credential and select "Per User Principal" as the Identity type.
  4. Note that the "Authenticate" option is not available from the options dropdown next to the permission set mapping you created. Authentication is set up through user settings.

Grant Access to an External Credential

All users will have to grand explicit access to the external application through their settings. As far as I know this cannot be done in bulk, feel free to comment if this is not the case, I’d love to know.

  1. Make sure the logged in user has a permission set assigned that is mapped to an external credential
  2. Click on your user avatar in the top right and click Settings
  3. In the left menu click on External Credentials
  4. In the list of credentials click on the button “Allow Access”, this will start the authentication flow for this user.
  5. That is it, you’re ready to use named and external credentials in user context :-)

Example 01 - Custom Field - External Credential - JWT Bearer flow

The out of the box external credentials come with the option for a JWT Bearer flow. This allows you to create a JWT where we can set the subject parameter to a custom value using a formula.

I will be using the example as described in my article: "Declaratively Connect Salesforce to Salesforce with the “new” Named and External Credentials JWT Bearer Flow".
In this article in step 6.4, the subject parameter of the JWT claims set has been defined with a hard coded system user. We are going to change this to a formula that points to a field on the user record.

  1. Go to setup >> Object Manager >> User >> Fields >> Click New
  2. Set type to "text", set the label to "External Org Username" the API name should automatically populate. You can choose your own name here.
  3. Make sure to set this field as an "External Id" and make the field unique. We don't want multiple users to be able to login with the same name, that would be a mistake.
  4. Update your user record through settings and set the newly created field with a username like "justus@pimoria.com", where pimoria is my external org.
  5. Create an External Credential (follow the article) but now in the JWT Claims, update the "sub" claim to the following formula:
    "{!$User.External_Org_User_Name__c}" Instead of a hard coded value, now each user will be able to connect to the external system using their own credential instead of the system account.
  6. !! Don't forget to give the users read access only to the field through the permission set related to to the named credential !!

Example 02 - Mapping Object - Custom Auth Provider

I won’t go into the full details here, even though this is the main show. An example implementation can be found in the core class of the OAuth 2.0 JWT Client Credentials Auth Provider. This class implements all the functionalities from the utility and is loosely coupled.

The first step this class uses is the logic to create a method that instantiates the Callable interface of the AuthProviderUtil class.

/**
* @description Method to get an instance of the AuthProviderUtil class.
* This option requires the "Lightweight - Auth Provider Util v2" (04t4K000002Jv1tQAC)
* package to be installed
* @return Instance of the AuthProviderUtil class
* @throws GenericException The lwt.AuthProviderUtil class does not exist.
*/
@TestVisible
private static Callable getAuthProviderUtil(){

// Lazy loading
if(authProviderUtil == null){

// Dymaically instatiate class
authProviderUtil = (Callable) Type.forName('lwt.AuthProviderUtil')?.newInstance();

// Throw an error if the package is not installed
// Add Test check here so the test does not fail in case the package is installed
if(authProviderUtil == null || Test.isRunningTest()){
throw new GenericException(MISSING_UTIL_PACKAGE_MSG);
}
}
return authProviderUtil;
}

We now have a reusable instance that we can use to execute the utility methods without a hard reference.

// Inserting a log
getAuthProviderUtil().call('insertLog', new Map<String, Object> {
'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(),
'userId' => UserInfo.getUserId(),
'logId' => GUID,
'message' => exceptionMessage
});


// Return the subject from the user mapping record related to this user and auth provider
return (String) getAuthProviderUtil().call('getSubjectFromUserMapping', new Map<String, Object> {
'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(),
'userId' => UserInfo.getUserId()
});


// Throw an exception if the user mapping does not exist
if(! (Boolean) getAuthProviderUtil().call('checkUserMappingExists', new Map<String, Object> {
'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(),
'userId' => UserInfo.getUserId()
})){
throw new SubjectException('Mapped subject does not exist');
}


// Update the mapping record
getAuthProviderUtil().call('updateMappingLoginDetails', new Map<String, Object> {
'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(),
'userId' => UserInfo.getUserId()
});

The most important part is you make sure you select the subject from the mapping table when you create your JWT. The snippet below shows how the subject is mapped based on a method “getSubject”. This method switches between a configuration setting and

 // Create the JWT payload
String payload = JSON.serialize(new Map<String,Object>{
'iss' => authProviderConfiguration.get(JWT_ISSUER_FIELD_NAME)?.trim(),
'aud' => authProviderConfiguration.get(JWT_AUDIENCE_FIELD_NAME)?.trim(),
'sub' => getSubject(authProviderConfiguration),
'exp' => (DateTime.now().addSeconds(300).getTime() / 1000),
'jti' => GUID
});

Final note

At the time of writing, I work for Salesforce. The views / solutions presented here are strictly MY OWN and NOT per definition the views or solutions Salesforce would recommend. Again; always consult with your certified implementation partner before implementing anything.

Useful Links

--

--