All insights

Platform

The OAuth username-password flow is retiring: migrate your integrations before they 401

Salesforce is turning off the OAuth 2.0 username-password flow for connected apps, and the integration that has signed in with a username, password, and security token for a decade will stop getting a token. Here's how to find every caller that still uses it, and the two flows to move them to.

The integration that has run for six years is the one about to break. It signs in with a stored username, a password, and a security token, gets a session, and syncs orders every ten minutes. Nobody has touched it. And that’s exactly why it’s at risk.

Salesforce is retiring the OAuth 2.0 username-password flow for connected apps, and when the retirement enforces, grant_type=password stops returning a token and starts returning an error. There is no UI warning, no banner on a user’s screen, no degraded mode. The next sync just fails to authenticate, the queue backs up, and someone pages you at 2 a.m. to ask why the ERP stopped talking to Salesforce.

If you own an integration user that logs in with a password, do one thing this week: move it to the client credentials flow or the JWT bearer flow. I’ll show you how to find every caller that still uses the old flow, and how to pick between the two replacements without guessing.

What is retiring, and what is not

The flow going away is narrow and specific. The OAuth 2.0 username-password flow is the one where a client posts grant_type=password with a consumer key, a consumer secret, a username, and a password (often with the security token appended) and gets back an access token.

It was always the least safe OAuth flow Salesforce offered, because it hands a long-lived password to a machine and asks the machine to keep it secret.

Salesforce ships the change as a release update, and the enforcement is per org on a published schedule. The date most sources are pointing at is February 20, 2027. Treat that as the deadline to plan against, and confirm the exact date for your instance in Setup under Release Updates, because Salesforce has already shuffled this timeline once and could again.

A few things are not affected, and conflating them causes needless panic:

  • Interactive user logins through the browser. A person typing a password into the Salesforce login page is unchanged.
  • SSO and the standard web server flow. Your Experience Cloud and Lightning logins keep working.
  • The JWT bearer and client credentials flows. These are the replacements, not casualties.

The blast radius is server-to-server integrations that authenticate with a password. Middleware, ETL jobs, a custom sync service, a vendor connector someone configured in 2021 and forgot. That’s where you look.

The username-password flow was never meant to be a permanent home. It was the quick way to get an integration talking, and the quick way became the way it shipped to production.

There’s a second deadline on the same horizon. The OAuth 2.0 device flow for connected apps is also ending, on a separate schedule around November 30, 2026. Fewer orgs use it, but if you have a connected app configured for device-flow login, it is on the same kind of countdown.

Find every caller before you fix anything

You cannot migrate what you cannot see, and the connected app list in Setup does not tell you which flow each caller uses at runtime. The authoritative record is the LoginHistory object. Every successful and failed login writes a row, and the LoginSubType field names the flow.

Query it directly from the Developer Console or any SOQL tool:

SELECT Application, UserId, LoginType, SourceIp, LoginTime
FROM LoginHistory
WHERE LoginSubType = 'OauthUsernamePassword'
AND LoginTime = LAST_N_DAYS:30
ORDER BY LoginTime DESC

The Application field holds the connected app the caller used, and UserId is the integration user it ran as. Group the results by Application and UserId and you have your migration backlog: one row per integration that still signs in the old way. Run it over 30 days to catch anything that syncs weekly or monthly, beyond the hourly jobs.

Only users with Manage Users or Monitor Login History can read LoginHistory for the whole org, so run this as an admin. If you have Event Monitoring, the LoginEventStore carries the same signal with more fields, and you can build a report or a real-time monitoring policy that flags any new username-password login so a freshly added integration can’t sneak back onto the dying flow.

Keep the output. It’s both your work queue and, later, your proof that nothing still calls the old flow before the enforcement date.

Pick the replacement: client credentials or JWT bearer

Both replacements are built for exactly this job, a server with no human sitting in front of it. The choice between them comes down to how you’d rather manage the secret.

Client credentials flow. The app exchanges its consumer key and consumer secret for an access token. No username, no password. You set it up by enabling the client credentials flow on the connected app and naming a run-as user: every call the integration makes runs as that user, with exactly that user’s object, field, and record access.

One nuance matters here. The Permitted Users policies (“All users may self-authorize”, “Admin approved users are pre-authorized”) do not apply to the client-credentials execution user. You pick the run-as user on the connected app itself, so getting that user’s permission set right is the whole security story.

A minimal token request looks like this:

POST https://MyDomainName.my.salesforce.com/services/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<consumer key>
&client_secret=<consumer secret>

Pair it with a Salesforce Integration user license. Those licenses are API-only and cheaper than a full license (orgs get a handful at no cost), and they exist precisely so an integration runs as a scoped, non-human identity instead of borrowing a salesperson’s full-access login.

Scope that user with a permission set that grants only the objects and fields the integration touches, and the client credentials flow becomes the least-privilege answer the username-password flow never was.

JWT bearer flow. The app signs a JWT with a private key and posts it to the token endpoint; Salesforce validates the signature against an uploaded certificate and issues a token. There is no secret in transit at all, because the private key never leaves your side, and the public certificate is the only thing Salesforce stores.

That makes it the stronger option for an ETL tool or middleware that already manages certificates, and it’s the one I reach for when the integration lives in an environment where rotating a shared secret is painful but rotating a key is routine.

The JWT request carries a signed assertion:

POST https://MyDomainName.my.salesforce.com/services/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<signed JWT>

The JWT bearer flow pre-authorizes the app for a specific user through the connected app’s policies, so like client credentials, it runs as an identity you choose and scope.

Here’s the decision I use. If the integration is a modern service you control and a shared secret in a vault is acceptable, client credentials is less setup. If it’s a vendor tool, a certificate-oriented middleware, or anything where you want no shared secret to leak, JWT bearer is worth the certificate work. Both end in the same good place: a scoped run-as user and no password sitting in a config file.

For a user-facing app where a person does log in, neither of these is right. Use the web server flow with PKCE instead. That’s a different migration, and a smaller population, but don’t accidentally move an interactive app onto client credentials and strand every user behind one service identity.

The migration, step by step

The order matters, because you want to prove the new flow works before you kill the old one.

  1. For each integration on your backlog, decide client credentials or JWT bearer by the rule above.
  2. Create or reuse a scoped integration user, and assign a permission set with only the access that integration needs. Do not reuse an admin.
  3. Configure the connected app for the new flow. For client credentials, set the run-as user. For JWT, upload the certificate and pre-authorize the app for the user.
  4. Update the caller to request a token the new way, and point it at your My Domain token endpoint.
  5. Run both flows in parallel in a sandbox, then in production, and watch LoginHistory to confirm the new LoginSubType appears and succeeds.
  6. Only then remove the password credential from the caller’s config and rotate it, so the old secret is dead even if a copy leaks.

Test the token lifetime and the session policies on the new connected app, beyond the first successful call. A flow that authenticates once and then can’t refresh will pass your smoke test and fail in production an hour later. The failure modes here are the same quiet kind that make most Salesforce integrations break eighteen months in: everything green on cutover day, a silent 401 the week the token policy bites.

One more honest note. Moving off the username-password flow is a security win on its own, but it is not the whole job. A scoped integration user with a leaked token is still a problem, which is why the connected-app token governance and the monitoring that catches an abused token are the other two legs of the same stool. The retirement forces the first step. Do the other two while you have the project open.

Understanding the basics

When is the OAuth username-password flow being retired in Salesforce?

Salesforce is retiring it through a release update, enforced per org, with the date most sources point to being February 20, 2027. It is already blocked by default for newly created orgs. Confirm the exact enforcement date for your instance in Setup under Release Updates, since Salesforce has adjusted the timeline before.

What breaks when the username-password flow is turned off?

Any server-to-server integration that authenticates with grant_type=password, a username, a password, and usually a security token. The token request returns an error instead of an access token, and the integration can’t sign in. Interactive logins, SSO, and the web server flow are not affected.

What should I migrate my integrations to?

For server-to-server integrations with no human involved, use the client credentials flow or the JWT bearer flow, each running as a scoped integration user. For apps where a person logs in, use the web server flow with PKCE. Don’t move an interactive app onto client credentials.

How do I find which integrations still use the old flow?

Query the LoginHistory object for LoginSubType = 'OauthUsernamePassword' over the last 30 days. The Application field tells you the connected app and UserId tells you the integration user. That list is your migration backlog.

Does the SOAP login() call stop working too?

The username-password OAuth flow is the one being retired as a release update. Legacy SOAP login() calls that pass a password and security token sit in the same at-risk category and are the right thing to migrate off at the same time, onto a token-based flow with a scoped user.


If you’ve got a wall of integrations authenticating with stored passwords and no inventory of which does what, that audit-and-migrate is exactly the kind of work we do. Talk to us before the enforcement date turns it into an incident.

Keep reading

All insights