Salesforce is enforcing stronger MFA requirements come June 2026 – act now!

Last week I presented together with a colleague at Salesforce World Tour in Oslo, Norway, on Salesforce security and among other things we covered the upcoming changes to the MFA (multi factor authentication) requirements for internal user logins to Salesforce Sales and Service Cloud. This sparked a lot of discussion after the session so it seems more focus on this is needed. There are a number of things you should make a note of and act on if you’re a Salesforce administrator:

  1. This applies both to production environments (orgs) as well as sandbox environments
  2. The employed MFA must be strong i.e. email and SMS is not enough
  3. Even if MFA is performed at an identity provider it will only be recognized by Salesforce if signaled using the industry standard amr / acr claims
  4. There are even stronger MFA requirements for admins / privileged users i.e. users with Modify All Data, View All Data, Author Apex or Customize Application permissions
  5. This applies only to logins through the user interface i.e. OAuth flows without any user interface component is not affected (i.e. client_credentials flow)
  6. Ensure your contact details are up to date with Salesforce and you monitor and act on communication from Salesforce

I’ve created a script to help you find the privileged users as the permission may be assigned using profile, permission sets or permission set groups. The script is in my salesforce-mfa-requirement Github repo.

The simple and easy solution for enterprise customers is to ensure the IdP sends the required claims to Salesforce and use passkeys for sandbox login.

Salesforce External Client App key and secret rotation via REST API

With the recent focus on security and especially API security I’ve spent a bit of time looking into how to further harden the security of orgs. One of the key areas to harden are the “apps” that API access is performed in the context of. In Salesforce this has traditionally been Connected Apps but these should really be consider legacy now. The “new kid of the block” is External Client Apps (ECA) – but they are not really new. They’ve been on the platform for a while but has really taken center stage with the most recent releases.

One of the things that ECA’s bring is a new architecture that allows for a host of things including much better ISV and packaging support. The architecture is also different and it allows Salesforce to bring better API support. One of the things that Salesforce engineering has been busy adding over the last few releases is a REST API for ECA’s. For the purpose of this post I’ll dive into the key and secret rotation capabilities afforded by this API.

For the purpose of these ECA’s we’re talking OAuth so the way to authenticate to Salesforce is with a client_id and a client_secret. Previously you’ve only been able to have a single set of these – now you can have two sets active at the same time which allows for key and secret rotation. Very nice.

The way this works is by having the main set as well as a staged set. The staged set may have a status of active or rotated. If active the set can be used like the main set of credentials. If rotated the set is automatically deleted after 30 days (this is not explicitly mentioned in the documentation but I found this value in internal documentation).

Step by step

Let me show you step by step how to do this… I’m gonna show this with working API requests using cURL. The steps also use jq to better format the JSON output and extract key pieces of info. Before I started on the steps below I configured an ECA in Salesforce Setup, configured it for the client_credentials flow and noted down the client_id and client_secret (this could be done with API’s as well).

For these examples it’s also important to enable obtaining secrets for ECA’s via the REST API. In Setup go to Apps/External Client Apps and ensure Allow access to External Client App consumer secrets via REST API is enabled.

Please note: I’ve only tested this below API statements with Winter 26 (API version 65.0) as this is where ECA key and secret rotation is slated to go GA (safe habour!)

# define params
export SF_APIVERSION=v65.0
export SF_MYDOMAIN=foo-xx1234xx.my.salesforce.com
export SF_CLIENT_ID=foo
export SF_CLIENT_SECRET=bar

# get an access token and save it in a param
export SF_ACCESS_TOKEN=`curl --silent -X POST -d "grant_type=client_credentials&client_id=$SF_CLIENT_ID&client_secret=$SF_CLIENT_SECRET" https://$SF_MYDOMAIN/services/oauth2/token | tee /dev/tty | jq -r .access_token`

{"access_token":"00DWt0...6Jz","signature":"mkLGHErOBL4QR77B7qs3N1w83r3agqE18OKIKpKHQ3g=","scope":"api","instance_url":"https://foo-xx1234xx.my.salesforce.com","id":"https://login.salesforce.com/id/00DWt00000B2V4bMAF/005Wt000004d0fNIAQ","token_type":"Bearer","issued_at":"1758701441120"}

# test the access token
curl --silent -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/oauth2/userinfo | jq -r .name

Mikkel Flindt Heisterberg

# list the apps we have and get the app identifier from the 
# first app. Change as appropriate. The identifier is the ID 
# of the ECA from Setup.
SF_APP_ID=`curl --silent -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/data/$SF_APIVERSION/apps/oauth/usage | tee /dev/tty | jq -r ".apps[0].identifier"`

{"apps":[{"accessTokenFormat":"opaque","availableActions":"disable, enable","description":null,"developerName":"ECA_Key_Secret_Rotation_Blog_Post","identifier":"0xIWt0000003lOz","isFromPackage":false,"usageDetailsUrl":"/services/data/v65.0/apps/oauth/usage/0xIWt0000003lOz/users"},{"accessTokenFormat":"opaque","availableActions":"disable, enable","description":null,"developerName":"Credentials_API_Poc","identifier":"0xIWt0000003kzB","isFromPackage":false,"usageDetailsUrl":"/services/data/v65.0/apps/oauth/usage/0xIWt0000003kzB/users"}],"currentPageUrl":"/services/data/v65.0/apps/oauth/usage?page=0&pageSize=100","nextPageUrl":"/services/data/v65.0/apps/oauth/usage?page=1&pageSize=100"}

# now we have the app id we can inspect the main set of credentials. Notice how the secret is not shown as it's specific to a consumer. Grab the consumer id and ask again
curl --silent -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/data/$SF_APIVERSION/apps/oauth/credentials/$SF_APP_ID | jq

{"consumers":[{"id":"888Wt000000O8CXIA0","key":"3MV...y1F.","name":"ECA Key Secret Rotation Blog Post","stagedCredentialsUrl":"/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000O8CXIA0/staged","url":"/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000O8CXIA0"}],"currentPageUrl":"/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI"}

# ask again and ask for secret as well
curl --silent -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/data/$SF_APIVERSION/apps/oauth/credentials/$SF_APP_ID/888Wt000000O8CXIA0?part=keyandsecret

{"id":"888Wt000000O8CXIA0","key":"3MV...y1F.","name":"ECA Key Secret Rotation Blog Post","secret":"BCD...D6E","stagedCredentialsUrl":"/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000O8CXIA0/staged","url":"/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000O8CXIA0?part=keyandsecret"}

# now ask for any staged credentials
curl --silent -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/data/$SF_APIVERSION/apps/oauth/credentials/$SF_APP_ID/888Wt000000Nn4rIAC/staged | jq

{
  "stagedCredentials": []
}

# as you can see there are none - let's create a set and save 
# the output and then extract the key and secret. 
curl --silent -X POST -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/data/$SF_APIVERSION/apps/oauth/credentials/$SF_APP_ID/888Wt000000Nn4rIAC/staged | tee new_credentials.json | jq

{
  "stagedCredentials": [
    {
      "createdBy": "foo.xx1234xx@salesforce.com",
      "createdDate": "2025-09-24T08:26:11.000Z",
      "expirationDate": "2025-10-24T08:26:11.000Z",
      "id": "0ugWt00000000w5IAA",
      "key": "3MV...g2J",
      "secret": "E1F...435",
      "state": "active",
      "url": "/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000Nn4rIAC/staged/0ugWt00000000w5"
    }
  ],
  "url": "/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000Nn4rIAC/staged"
}

export SF_CLIENT_ID=`cat new_credentials.json | jq -r ".stagedCredentials[].key"`
export SF_CLIENT_SECRET=`cat new_credentials.json | jq -r ".stagedCredentials[].secret"`

Wow that was a lot! But now we have two sets of active credentials for this consumer. As you can see the status of the staged credentials we just created is active and the consumer (you!?) should now make plans to switch to these credentials. We can list the staged credentials and we can delete them if we wanted to, by running a HTTP DELETE towards the credential url (curl -X DELETE -H "Authorization: Bearer $SF_ACCESS_TOKEN" https://$SF_MYDOMAIN/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000Nn4rIAC/staged/0ugWt00000000w5). Deleting them would of course make them invalid.

The next step is rotating the credentials. This essentially replaces the main set of credentials with the staged set marking the staged set as rotated and hence marked for deletion (as a staged set).

# rotate the staged credentials to be the main set
curl --silent -X PATCH -H "Authorization: Bearer $SF_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{ "command": "rotate" }' https://$SF_MYDOMAIN/services/data/$SF_APIVERSION/apps/oauth/credentials/$SF_APP_ID/888Wt000000Nn4rIAC/staged/0ugWt00000000w5IAA | jq

{
  "createdBy": "storm.ee4025cd74c549@salesforce.com",
  "createdDate": "2025-09-24T08:31:05.000Z",
  "expirationDate": "2025-10-24T08:31:05.000Z",
  "id": "0ugWt00000000xhIAA",
  "key": "3MV...Xy3",
  "secret": "054...51D",
  "state": "rotated",
  "url": "/services/data/v65.0/apps/oauth/credentials/0xIWt0000003lOzMAI/888Wt000000Nn4rIAC/staged/0ugWt00000000xhIAA"
}

As you can see the staged credentials now has the status rotated. If you repeat the steps to get the main credentials you’ll see that the main set and the rotated set has the same key and secret. If I attempt to use the prior set of credentials you’ll get an error like the one below.

{"error":"invalid_client_id","error_description":"client identifier invalid"}

You can now delete the staged set of credentials explicitly or let the platform delete them after a while. The act of creating a new set of staged credentials like we did above also deletes the staged rotated set before adding the new set.

YMMV.

Reference

Salesforce Headless Identity Login Example

This week I helped a colleague doing an example of how to use the Salesforce Headless Login flow from a single page app (SPA). The Github repo describes how to configure the Salesforce org (a scratch org) and how to use the included SPA – just a super simple HTML page really – to do a headless login against the Salesforce org. The login uses PKCE to further secure the exchange. Once the login has been performed the obtained access token is used to get information about the user from the /services/oauth2/userinfo endpoint.

YMMV

Salesforce Token Exchange Flow

I’ve spent some time with the new token exchange flow in Salesforce. The flow allows you to write and configure an Apex handler in Salesforce that can validate an incoming token (say an access_token, JWT or proprietary token) from another Identity Provider (think Okta, Auth0 or any custom implementation) in Salesforce and return a Salesforce access token that can be used towards Salesforce for API access. This can simplify the integration scenario for micro services or other apps as access can be granted based on an already proved identity. The returned Salesforce access token is then used for API access as the Apex handler also maps the incoming token to a user in Salesforce – the user may optionally be created on the fly.

I captured my findings, wrote instructions on how to implement wrote an example implementation in a Github repo.

As an interesting aside, the metadata type for the OAuth Token Exchange Handler is not yet supported with the “new” source format so it has to be deployed with the old school metadata API and format (still using the CLI though).

Video walkthru for the Salesforce Azure client_credentials Auth. Provider

Based on a number of comments I’ve added a video walkthru for the Azure Client Credentials Auth. Provider I have available on Github. The video is hosted on Youtube and besides a description of the elements used shows pushing the source to Salesforce, configuring the Auth. Provider in Salesforce as well as configuring the App Registration on Microsoft Azure.

https://www.youtube.com/watch?v=MYOtFuPe_R4

See the post for the Auth. Provider (Custom Salesforce Auth. Provider for Microsoft Azure client_credentials flow) for a link to Github.

Custom Salesforce Auth. Provider for Microsoft Azure client_credentials flow

When doing integrations from Salesforce you sometimes need to do this using single identity instead as in the context of the current user. Salesforce supports both through Named Credentials i.e. both working as the current user or as a Named Principal. Through the Named Credentials and Auth. Provider concepts from Salesforce you can setup a connection between Salesforce and an OAuth 2.0 enabled endpoint such as Microsoft Azure i.e. if needing the access the Microsoft Graph API. This can however be an issue as the target system (i.e. Microsoft Azure in this case) may not use a user that is able to login or you may not want to use a user license in the target system.

In this case you might want to use the client_credentials OAuth flow as that identifies the caller using a client_id and a client_secret instead of a user id. Unfortunately that OAuth flow type is not supported out-of-the-box with Salesforce. The reason is that the Auth. Providers in Salesforce usually deals with a user behind the keyboard. Due to this the OAuth 2.0 Auth. Provider flows was designed for a access_token / refresh_token World. This makes it impossible to use the built in capabilities for the client_credentials flow.

The solution is to write a custom Auth. Provider in Apex and use that from your Named Credential. To make this easier I’ve already implemented this for you. The code is available on Github in my salesforce-azure-clientcredentials-authprovider repo. This implementation just plays along with the access_token / refresh_token requirements and just requests a new access_token using the client_credentials flow whenever a (new) access_token is needed and hence do not need a user behind the keyboard.

The README.md in the repo has instructions for installing and configuring the code in the org. Once deployed you can create an Auth. Provider and a Named Credential. Please note you also need to create an App Registration in Azure Active Directory with the required Application Permissions.

2 legged vs 3 legged OAuth

Had a question come in from a customer that centered around understanding the difference between 2 legged and 3 legged OAuth so I thought I would write a little about it. I short 2 legged and 3 legged OAuth refers to the number of players involved in the OAuth dance but let’s explain each.

3 legged OAuth is used when you as a user wants to allow an application (i.e. Salesforce) to access a service (i.e. Azure) on your behalf without the application (Salesforce knowing your credentials for the service (Azure). This is accomplished by me (the user) being redirected by the application (Salesforce) to the service (Azure) where I log in directly, I obtain a code that the application (Salesforce) can use to obtain an access token for the service (Azure) out of band (i.e. the application contacts the service directly). Key is that I the user am involved because I need to authenticate to the service (Azure) and the application is afterwards able to impersonate me towards the service.

2 legged OAuth is used when an application needs to access a service using a service account (e.g. an App Registration as it’s called in Azure). The key here is that the application has all the information it needs to authenticate to the service. In 2 legged OAuth the application makes a single call to the service to basically exchange credentials (username/password, client_id/client_secret, Json Web Token (JWT)) for an access token. As an aside there is usually no way to get a refresh token issued in a 2 legged OAuth dance which is fair as the application could just perform the 2 legged OAuth dance again to get a new access token hence no need for a refresh token.

Looking back towards Salesforce and Named Credentials which is the way we recommend customers manage credentials for accessing services outside Salesforce. In Named Credentials you can use 3 legged OAuth if you selected “OAuth 2.0” for “Authentication Protocol” and 2 legged OAuth if you select “JWT” for “Authentication Protocol”.

Utility to generate JWT’s for use with Salesforce

Whenever you work with Json Web Tokens (JWT’s) generating them for testing is always a hassle as they usually are required to expire quite quickly (like in the order of minutes). To make that easier I wrote a small utility in node.js to generate JWT’s compatible with the Salesforce OAuth 2.0 JWT Bearer Flow.

Code is on Github at https://github.com/lekkimworld/salesforce-jwt-generator

YMMV!

Using the inbound OAuth 2.0 JWT Bearer Flow in Salesforce

If working with JWT’s for use with the inbound OAuth 2.0 JWT Bearer Token Flow you need to import a public key and a certificate to validate the signature of the JWT when calling into Salesforce. This is done on the Connected App and the import supports binary DER-format and the plain text PEM-format.

Once you’ve created your Connected App, check “Enable OAuth Settings” and under “Use digital signatures” import the certificate (PEM or DER format) to use to validate the signature of the JWT.

Using the actual flow requires you set the Consumer Key as the issuer (“iss”), the username of the user to act as, as the subject (“sub”) and the login-url as the audience (“aud”). The login-url will be https://login.salesforce.com, https://test.salesforce.com or a community url.

To make it easier to work with and test I’ve created a node.js console app that allows you to generate JWT’s that are compatible with Salesforce. The code is on Github at https://github.com/lekkimworld/salesforce-jwt-generator.

Exchanging the JWT for an access_token is as below setting the grant_type to urn:ietf:params:oauth:grant-type:jwt-bearer and specifying the signed JWT using the assertion-parameter:

POST /services/oauth2/token HTTP/1.1
 Host: login.salesforce.com
 Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYX...VtbyJ9.AjsbapI5XTeLpPLZJk2_a2PnpAV0iUOT6xxgUWZsjYBeH9FcWHjiS6DMw1xuNyOcHNxY6hTAp1_D6HPDY4i0hgOFzb0YUaaWf9MoplpNknsGhYZ0SOHX2OSIfFVZ7KdPx1_BudRSi3VDNt33EZhf3cm07rMSJu-DOzHP1BSJE4HXALusEV3WgdSyijUce4daF3PVANI8w-yGAhFkdO8RCrCAufaZVxtTI1ZmnXeDRxbULQZ9hnn0vtgYHaMcgTK41ZGay3UN7XVa-FERG4WcdnvylPAhnalgSFlCDX3UHvUdn-wxYX0pSPw41R2rjPUDCWBiEV8ULzEiWQrBpyqkww