ThirdPartyAccountLinks are records in Salesforce storing user details of an External AuthProvider (like Facebook, Google+ or a custom one for SSO) to the Salesforce user the user authenticated and identified his/herself with. On these records you find details like the related Auth. Provider, the Handle (username in External System) and RemoteIdentifier (unique user id in External System).
Example use case: Imagine Samantha to r2egister at your Salesforce Community with email salesforce@foxy-solutions.com; Afterwards she opts to authorise with Facebook as she likes to minimise her number of accounts and passwords. She logs in at Facebook with facebook@foxy-solutions.com and grants Salesforce access to her Facebook profile for the requested scope (e.g. profile details, friendlist and such). You’ve set up the Auth. Provider to allow matching even when emails mismatch ONLY as for fresh, new accounts, since that mitigates the risk of exposing data from other customers. By the end of this, Samantha can easily login to your Community with here Facebook credentials. She has a SF User with a related TPAL stating at least ‘facebook@foxy-solutions.com’ as Handle and ‘Facebook’ as Auth. Provider.
This article is to share some of my experiences and most importantly, some undocumented behaviour regarding those connecting objects.
Accessibility
Unfortunately, as of Spring ’20 Salesforce has revoked any read-access for users without “Manage Users” permissions. Only Community Users are allowed to see TPALs which are connected to their User record.
Due to this restriction, one is not able to query ThirdPartyAccountLinks for matching or identifying a user on login or before registration.
Manipulation
By default, ThirdPartyAccountLinks are set to read-only. To allow creation and deletion this standard object needs to be made writable via Salesforce Customer Support. However, even then, TPALs cannot be updated (so only inserted or deleted).
Insertion
Since ThirdPartyAccountLinks are no standard objects and connect details from an External System to Salesforce data, Salesforce made the decision to treat them differently as well.
For that reason, you can’t manipulate TPALs using standard DML actions:System.DmlException: Argument must be of internal sObject type. use insertAsync() or insertImmediate() instead
Using insertImmediate()
allows to simply create new ThirdPartyAccountLinks to the User of your chosing.
Deletion/revoking
To delete a TPAL, or in other words, revoke access of that External User for the corresponding Salesforce User, Salesforce provides you two options: Auth.AuthToken.revokeAccess()
or Database.deleteImmediate()
. Both come with advantages and disadvantages, but for sure with their own quirks.
Auth.AuthToken.revokeAccess( authProviderId, providerName, userId, remoteIdentifier )
This lovely Apex function allows to easily revoke access by deleting the ThirdPartyAccountlink after providing the Id and name of the AuthProvider and the IDs of the User in both Salesforce and the External System (RemoteIdentifier
), respectively.
While the method returns a Boolean to indicate success or failure, there is no description or hint why the revoke failed, in case false
is returned.
When using this method, it is crucial (and undocumented) to provide the ProviderName in lowercase. Else, the method will simply return false and no TPAL will be deleted. A code example can be found below.
Database.deleteImmediate( List<SObject> || SObject )
This Apex function might be perceived more intuitive, since the syntax is comparable with the standard delete
and Database.delete()
functions.
Significant benefit is the return of List<Database.DeleteResult>
, allowing a little better understanding of the cause of failure.
Unfortunately, at the time of writing (March ’21) there was a known bug at Salesforce, causing an Internal Server Error: UNKNOWN_EXCEPTION, An unexpected error occurred. Please include this ErrorId if you contact support: 1110349392-159649 (74215998)
.
A Salesforce support agent replied with: “I could see a bug W-5238232 for the delete operation. As a workaround, they requested to use Auth.AuthToken.revokeAccess() instead of deleteImmediate() .”
Hopefully, this will be resolved shortly, though we can luckily still continue using revokeAcces()
;
Re-parenting ThirdPartyAccountLinks
One of the use-cases where one might want to actively create and destruct of ThirdPartyAccountLinks is when in need of re-parenting, e.g. when merging two Accounts, both having ThirdPartyAccountLinks, but you need to delete one of the Accounts, while the User should not lose his/her connection with the External System.
To realise, one will need to delete the TPALs from the original Account (to be deleted) and insert them directly after to the Account (which will remain). Be aware it is crucial to set a SavePoint and rollback when any of the methods is returning failures on deletion and inserts, to prevent losing External System connections.
Note, you’ll receive an error when trying to insert the cloned TPAL before deleting the original, due to a duplicate ThirdPartyAccountLinkKey. The error however is rather unclear: Database.SaveResult[getErrors=(Database.Error[getFields=();getMessage=Unable to insert data for 00D3H0000000XXX:0SO3H0000008XXX:providername:remoteidentifier;getStatusCode=UNKNOWN_EXCEPTION;]);getId=null;isSuccess=false;]
Set<Id> obsoleteUserIds = new Set<Id>{ '005...' }; Id remainingUserId = '005'; // Retrieve the preferred TPAL to reparent List<ThirdPartyAccountLink> tpalsToReparent = [SELECT Handle, Id, IsNotSsoUsable, Provider, RemoteIdentifier, SsoProviderId, SsoProviderName, ThirdPartyAccountLinkKey, UserId FROM ThirdPartyAccountLink WHERE UserId IN :obsoleteUserIds]; List<ThirdPartyAccountLink> tpalsToCreate = new List<ThirdPartyAccountLink>() for( Integer i = 0, j = tpalsToReparent.size(); i < j; i++ ){ // (Deep)Clone TPAL without Id and set new UserId for reparenting ThirdPartyAccountLink tpalClone = tpalsToReparent[ i ].clone( false, true ); tpalClone.UserId = remainingUserId; tpalsToCreate.add( tpalClone ); } Savepoint sp = Database.setSavepoint(); try{ for( Integer i = 0, j = tpalsToReparent.size(); i < j; i++ ){ ThirdPartyAccountLink tpal = tpalsToReparent[ i ]; // Use the standard mechanism for deletion of TPALs // Crucial to set provide SSO ProviderName in lowercase if( !Auth.AuthToken.revokeAccess( tpal.SsoProviderId, tpal.SsoProviderName.toLowerCase(), tpal.UserId, tpal.RemoteIdentifier ) ){ throw new Exception( 'Something failed when revoking TPAL ' + tpal ); } } // After deletion insert new TPAL linked to the different User List<Database.SaveResult> saveResults = Database.insertImmediate( tpalsToCreate ); for( Integer i = 0, j = saveResults.size(); i < j; i++ ){ if( !saveResults[ i ].isSuccess() ){ throw new Exception( 'Failure when inserting tpal ' + saveResults[ i ] ); } } } catch( Exception ex ){ System.debug( 'Exception occurred' + ex.getMessage() ); Database.rollback( sp ); }
Conclusion
ThirdPartyAccountLinks are useful when implementing an External Auth. Provider, allowing to match the correct Salesforce user for the authorised user. However, when diving a bit deeper, there are some quirks which are not (extensively) documented.
Hopefully, this document could help you to know:
- a Salesforce case is required to make the TPAL object writable in your Salesforce org
- deleteImmediately() at moment of writing is not working due to a working bug
- revokeAccess() requires the provider name to be provided in lowercase, as the method will otherwise return false, without any specifications
Lastly, it might be good to know ThirdPartyAccountLinks are NOT deleted on user-deactivation.
In case you have any nice use cases or war stories to share, please do leave a comment!
Happy coding!
One thought to “Reparent ThirdPartyAccountLinks”
Fantastic