Identity & authentication
Authentication answers one question: who is this? It produces a principal, a set of methods that were used to prove it, and a session. It produces nothing else. Every question of the form “may they do it” belongs to the three authorization planes and is answered after identity is already settled.
That separation is the load-bearing decision on this page, and it is stated as a rule the kernel enforces rather than a convention: how a user signed in has no effect on what they may do. A user who typed a password and a user who arrived through a corporate SAML assertion resolve to the same principal with the same permission sets and the same record access. Nothing in the identity plane can add a grant, and nothing in the authorization plane can be reached without one.
The single legitimate coupling runs the other way. A permission set may declare a minimum assurance — “this grant only applies inside a session that proved multiple factors” — which is a condition on a grant already held, never a grant conferred by a login. Identity gates; it does not give.
The model
Section titled “The model”Four things are distinct and are stored in different places, because collapsing any two of them is how identity systems leak.
| Concept | What it is | Where it lives |
|---|---|---|
| Principal | The identity the platform reasons about — a stable id, a tenant, and a kind (person or service) |
Control plane |
| User record | The business object: name, email, title, manager, locale, and any custom fields the org adds | Tenant schema, as an ordinary object |
| Credential | A password hash, an enrolled WebAuthn key, a TOTP secret, an IdP subject binding, a token hash | Control plane, keyed by principal, never a column on any tenant object |
| Session | A minted, expiring assertion that a principal authenticated, carrying which methods were used and under which policy generation | Control plane |
The user record is an ordinary object. It has real columns, page layouts, list views, field-level security, and custom fields, and it is edited through the same Setup surface machinery as any other object. What it does not have is a credential column. Schema-per-tenant means a user holding metadata.author can author fields on any tenant object and read them through ordinary tooling; if a password hash or a TOTP seed were a column on that object, metadata authoring would be a path to credential material. It is not, because the credential is not in the tenant’s schema at all.
A principal outlives a user record’s contents and is never reused. Renaming a person, changing their email, moving them between identity providers, deactivating and reactivating them — none of it mints a new principal, because the principal is what every audit entry, every owner_id, and every record-history row points at. Deleting a user would orphan all of that, which is the mechanical reason deletion is not offered.
Assurance is a property of the session, not of the user. Three levels, ordered:
| Level | Proven by |
|---|---|
single_factor |
One factor — a password, or an IdP assertion that reported no second factor |
multi_factor |
Two factors, or an IdP assertion reporting multi-factor authentication |
phishing_resistant |
A FIDO2/WebAuthn credential — a security key, a platform authenticator, or a passkey — either directly or asserted by the IdP |
A permission set names the level it requires. If the session does not reach it, the grants that set carries are simply absent from the effective-access resolver for that session — the same bool_or union, with the set’s rows filtered out. Nothing errors; access resolves lower. Stepping up re-runs the union and the grants appear.
Lifecycle: invite, activate, deactivate
Section titled “Lifecycle: invite, activate, deactivate”A user has exactly four states and one forbidden transition.
invited ──accept──▶ active ──deactivate──▶ deactivated │ ▲ │ │ │ │ └──lock (policy)──▶ locked expire └──────reactivate──────────┘ │ or unlock ▼ (invite void, no seat consumed)| State | Can sign in | Holds a seat | Owns records |
|---|---|---|---|
invited |
No — the invite is a one-time credential, not a session | No | No |
active |
Yes | Yes | Yes |
locked |
No — set by failed-login policy, self-clearing | Yes | Yes |
deactivated |
No | No | Yes, still |
Invitation. Creating a user mints a principal in invited and sends a single-use, expiring invitation. The invite is bound to the principal and to the email address it was sent to; it cannot be forwarded into a different account. Accepting it runs whichever login method the org’s policy allows — set a password, enrol a passkey, or bounce to the IdP — and flips the user to active. An expired invite is re-issuable; it is never extendable in place, because an invitation that can be silently prolonged is an unbounded credential.
Deactivation, never deletion. Deactivating a user ends every live session, revokes every token issued to them, releases their seat, and leaves every record they own, every audit entry they wrote, and every history row naming them exactly as it was. There is no delete. A user is the referent of owner_id, of created_by, and of every entry in all three audit streams; destroying the row would either break those references or force them to be rewritten, and rewriting audit history to make a foreign key work is not a trade the platform offers.
Ownership reassignment is a separate, explicit step offered at deactivation, not a side effect of it. A deactivated user continues to own their records until someone decides who should own them instead — which is correct, because “this person left” and “this person’s pipeline moves to Dana” are two decisions and the second one has a wrong answer.
Reactivation restores the same principal. Sessions are not restored, tokens are not restored, and MFA enrolments are — the factors a person registered are still theirs. A seat must be available; if the subscription is full, reactivation is blocked rather than silently over-billing.
Seat accounting. Seats are counted as active users of kind person, checked against the subscription the control plane holds. Service identities are counted separately, on their own entitlement, because a machine is not a person and pricing that pretends otherwise pushes orgs toward credential sharing. Crossing the seat limit blocks activation — of a new invite acceptance or of a reactivation — and says so at the moment it blocks, naming the plan and the count.
Authentication methods
Section titled “Authentication methods”An org declares one or more login methods. There is no single-SSO assumption and no requirement to pick one: a tenant may run corporate SAML for staff, OIDC against a second directory for a subsidiary, and passwords for a handful of contractors, simultaneously.
| Method | Kind | Notes |
|---|---|---|
password |
Local | Subject to a password policy; disableable org-wide |
passkey |
Local, phishing-resistant | A WebAuthn credential used as the primary method — not merely a second factor |
saml |
Federated | SP-initiated and IdP-initiated; signed assertions required |
oidc |
Federated | Authorization-code flow with PKCE |
Passkeys are a login method, not only a factor. A passkey proves possession and user presence in one step and is phishing-resistant by construction, so a passkey login reaches phishing_resistant assurance without a password in front of it. Treating WebAuthn strictly as a “second factor” bolted behind a password preserves the password as the weakest link in a flow whose whole purpose was removing it.
The sign-in card offers the org’s configured methods directly — it never routes on a typed identifier. All of an org’s login methods are presented together in one step: password entry alongside a button for each configured provider. The card does not ask for an identifier first and then decide what to show, because a card that behaves differently once a username is typed is an account-existence oracle no matter how carefully its wording is policed — the same “not-found beats forbidden” invariant that governs every authentication surface. Which methods appear is an org-level fact, not a per-account one, so it discloses nothing about any individual. Registered email domains still matter, but as a backend concern only: an org declares the domains it owns (the domains list below) so the platform can resolve an inbound federated assertion — or a deep link that names no org — to the right org/tenant server-side. That resolution is invisible to the card; it never becomes identifier-first routing on the sign-in surface.
Disabling password login is a first-class setting, not a per-user chore. allowPasswordLogin: false on the login policy means no principal in the tenant can authenticate with a password, including one created before the setting flipped — existing password credentials are marked unusable rather than deleted, so re-enabling does not silently restore a set of stale hashes. Break-glass is a named list of principals exempted by the policy itself, which is auditable, rather than an org-wide setting nobody remembers turning back on.
Self-service password reset is part of the login contract, not an app feature. Because a password is stored only as a one-way hash, a forgotten or rotated one is otherwise unrecoverable — so the “Forgot password?” affordance and the two screens behind it (request a link → set a new password) are guaranteed present on every login surface. A brand may restyle them through the --brand-*/--caos-* token layer; it may never remove them. The engine exposes the capability as two unauthenticated system operations alongside auth/login, with the same posture as the bootstrap login (no session yet, org resolved from the host):
- request — an identifier comes in; if it resolves to an active principal in the host’s org, the engine mints a high-entropy reset token, stores only its hash, and hands the raw link to a pluggable out-of-band delivery channel. It always returns the same uniform acknowledgement whether or not the account exists — the same “not-found beats forbidden” non-disclosure the login failure obeys — and is rate-limited on both the identifier and the source, so it enumerates nothing and cannot be used to spray. The confirmation screen says “if that account exists, a link is on its way,” never that it does.
- complete — the token and a new password come in; the engine claims the token atomically (a single guarded update, so it is provably single-use), sets the credential through the same scrypt path a seeded admin uses, and — because a reset is a trust event — revokes every live session for the principal. A malformed, expired, superseded, or already-used token yields one uniform failure that discloses no reason.
The token is single-use and time-limited (45 minutes by default); a new request supersedes any outstanding one. The delivery channel is a seam: until the transactional email service lands it uses a development/manual sink so the flow is exercisable end-to-end, and the real email sender swaps in behind the same interface with no change to the engine or the login surface. This is recovery for a forgotten password; recovery for lost MFA factors is the administrator bypass above, and the two are deliberately separate mechanisms.
Provider health is monitored, and failure is visible. An identity provider that stops responding, or whose signing certificate is within thirty days of expiry, raises a banner on the Setup home and an entry in the metadata audit stream. Certificate expiry is the single most common cause of a total SSO outage and it is entirely predictable, so it is treated as a scheduled event rather than an incident.
Just-in-time provisioning and identity linking
Section titled “Just-in-time provisioning and identity linking”A person authenticates successfully at the IdP and no user record exists for them. Three outcomes are possible and the policy chooses which:
- Link — an existing user matches; bind the IdP subject to that principal and sign in.
- Provision — no user matches; create one from the assertion’s attributes and sign in.
- Refuse — the match is ambiguous, or provisioning is off; deny the login and raise a
conflict-class error into the audit stream.
Matching is explicit and ordered, and ambiguity always refuses. The policy names an ordered list of match keys — typically the IdP subject binding, then a federationId field, then verified email. The first key that matches exactly one active or invited user wins. A key matching more than one user does not fall through to the next key and does not create a new user: it refuses, because a silent duplicate account is how one person ends up owning records under two identities that no report will ever join.
JIT is declarative. Provisioning maps assertion attributes to fields and IdP group claims to permission-set groups:
{ "key": "idp_corp_entra", "label": "Corporate directory (Entra)", "type": "identity_provider", "body": { "protocol": "oidc", "issuer": "https://login.microsoftonline.com/{tenant}/v2.0", "clientId": "cafe3383-…", "domains": ["dragonproducts.com", "moderngroup.com"], "assurance": { "trustAmr": true, "minimum": "multi_factor" }, "jit": { "create": true, "matchOn": ["subject", "user.federation_id", "user.email"], "caseInsensitiveMatch": true, "attributeMap": { "user.first_name": "given_name", "user.last_name": "family_name", "user.email": "email", "user.department": "department" }, "groupMap": { "SG-Billing": "psg_lead_billing", "SG-Sales-Admin": "psg_security_admin" }, "ownedFields": ["user.first_name", "user.last_name", "user.email", "user.department"], "onUnmapped": "ignore" } }}onUnmapped decides what happens to a group claim with no mapping: ignore (default) or error. It never means “assign something plausible.”
Group claims are synchronized, not merely applied. A user whose IdP group is removed loses the mapped permission-set group at their next login. Assignments made inside the platform are untouched — the two sources are distinguishable, and the Users surface shows which is which. An org that wants the directory to be authoritative gets that; an org that wants the directory to seed and the platform to refine gets that too, without either silently undoing the other.
One escape hatch, and it is not the entry price. Where attribute mapping genuinely cannot express the rule, an effectful automation controller may subscribe to the identity.link and identity.provision events and run in the one typed language with the incoming claims bound as assertion. It runs inside the login transaction and is budgeted; a controller that throws refuses the login rather than provisioning a half-built user. Salesforce requires an Apex class before JIT does anything at all; here code is available for the last five percent and required for none of it.
Identity-owned fields
Section titled “Identity-owned fields”A field listed in ownedFields is owned upstream. That is a property of the field, not a permission, and it behaves accordingly:
- It is read-only on every surface — record page, list view inline edit, API write, and automation — and no permission set can grant edit on it.
platform.rootdoes not override it either, because root is an authorization bypass and this is not an authorization rule. - A write attempt fails with a distinct error code naming the owning provider, rather than succeeding and being reverted at the next login.
- The field renders with a provenance marker stating which provider owns it, so an administrator looking at a stale surname knows where to go and fix it.
The alternative — allow the edit, then overwrite it on the next sync — is what makes IdP-backed fields infuriating in practice, because the change appears to work and un-happens hours later with no event anyone can find.
Multi-factor authentication
Section titled “Multi-factor authentication”MFA is on from the moment a tenant is provisioned and cannot be switched off. There is no rollout, no auto-enablement wave, and no deadline, because there was never a period in which it was optional. Every enforcement problem described in How Salesforce does it is a retrofit problem, and a platform that starts with the requirement does not have one.
What is configurable is which factors are acceptable and what the minimum assurance is for whom.
| Factor | Counts toward | Notes |
|---|---|---|
| Security key (FIDO2/WebAuthn, roaming) | phishing_resistant |
|
| Platform authenticator / passkey | phishing_resistant |
Touch ID, Windows Hello, synced passkeys |
| TOTP authenticator app | multi_factor |
|
| Push approval (platform app) | multi_factor |
Number-matching required |
| Email or SMS one-time code | Nothing | Available for device verification only — see below |
Email and SMS codes do not satisfy MFA. They are phishable and SIM-swappable, and treating them as a factor produces the compliance-shaped illusion of MFA. They remain useful for a different job: verifying a login from an unrecognized device, where the question is “is this the same person as last time” rather than “prove a second factor.” Keeping the two jobs separate is why the table has a row that counts toward nothing.
Enrolment happens at the point it is needed. A user whose assurance is below what their session requires is walked through enrolment inline — not sent an email, not blocked at a wall with a support link. Two factors are required before the first can be considered enrolled; a single registered key is a lockout waiting for a lost laptop.
Federated logins delegate MFA and it is verified, not assumed. With trustAmr: true, an OIDC amr claim of mfa/hwk, or the equivalent SAML authentication-context class, raises the session’s assurance. With it false — or when the assertion is silent — a federated login lands at single_factor and the platform challenges for its own factor. The default is to trust nothing the IdP did not explicitly assert, which is the opposite of accepting an SSO login as self-evidently strong.
Recovery is bounded and audited. Losing every factor is recovered by an administrator holding users.manage issuing a temporary bypass: a one-time code, valid for a stated window, that permits exactly one login at single_factor and requires enrolment before anything else happens. There is no permanent exemption, no “MFA exempt” flag, and no way to leave a user in a bypassed state by forgetting to undo something — the bypass expires whether or not it is used. Issuing one is a flagged entry in the metadata audit stream, on the same footing as a root assignment.
Session policy
Section titled “Session policy”One session_policy component holds everything about a session’s lifetime and the network conditions under which it may exist. This is where login hours and IP ranges live, since there are no profiles to carry them — and it is the correct home regardless, because neither is a permission: they are conditions evaluated at authentication and at each request, not grants unioned into anyone’s access.
{ "key": "session_default", "label": "Default session policy", "type": "session_policy", "body": { "idleTimeout": "PT2H", "absoluteLifetime": "PT12H", "concurrentSessions": 3, "onConcurrentExceeded": "evict_oldest", "networkBinding": "prefix", "ipRanges": [{ "cidr": "203.0.113.0/24", "label": "HQ" }], "outOfRangeAction": "challenge", "loginHours": { "timezone": "America/Chicago", "mon_fri": "06:00-20:00", "sat_sun": null }, "deviceTrust": { "rememberFor": "P30D", "requireVerification": true }, "stepUp": { "freshness": "PT5M", "requiredFor": ["platform.root", "metadata.purge", "deploy.promote", "data.export", "client_app.rotate_secret"] } }}A policy is assigned to a permission-set group and resolves most restrictive wins — the inverse of the permission union, and deliberately so. Permissions are additive because a grant should never be cancelled invisibly; session constraints are intersective because a constraint should never be escaped invisibly. A user who is in one group with a two-hour idle timeout and another with eight hours gets two hours.
Concurrent sessions are a number, with a stated behavior on exceeding it. evict_oldest (the default) signs out the least recently used session; refuse blocks the new login. Both are legitimate — a shared workstation environment wants eviction, a compliance regime that must not have an unattended session open wants refusal — and neither should require building an authentication flow by hand to obtain.
Network binding defaults to the prefix, not the address. Binding a session to the exact source IP breaks every user on mobile data, every corporate network with more than one egress, and a good deal of mobile client software. Binding to the announced prefix keeps the property that matters — a stolen session cookie cannot be replayed from another network — without the failure mode. networkBinding: "exact" remains available and the Setup surface states the consequence next to the toggle rather than in a knowledge article discovered afterwards.
Login hours carry a timezone, always. A schedule without one is interpreted against something — an org default, a server clock, a user preference — and every such interpretation produces a support ticket at a daylight-saving boundary. The field is required.
Out-of-range does not have to mean refuse. outOfRangeAction is challenge by default: a login from outside the declared ranges is permitted after device verification and a step-up to multi_factor. refuse is available for orgs that need a hard perimeter. Making the strict option deliberate rather than the only option is what keeps IP ranges from being abandoned the first time a director logs in from an airport.
Step-up is enumerated, not implied. Sensitive operations require a re-authentication newer than freshness, and the list is explicit metadata. Assuming platform.root and rotating a client secret are on it by default. A step-up challenge is the strongest factor the principal has enrolled — a root holder with a security key is challenged for the key, not offered a TOTP fallback.
Policy changes apply to live sessions. A session records the policy generation it was minted under; when the policy changes, the next request re-evaluates against the new generation, and a session that no longer satisfies it is ended. Tightening a policy that leaves existing sessions running is a control that does not take effect until everyone happens to log out.
Login-as: impersonation
Section titled “Login-as: impersonation”Support work needs it and it is the single most dangerous feature in an administration surface, so every property below is a constraint rather than an affordance.
A dedicated permission. users.impersonate — not implied by data.modify_all, not implied by users.manage, and not included in platform.root’s short-circuit. Reading every record and becoming another person are different trust decisions, and binding them to one grant means every org that needs a data-recovery admin also has an impersonator.
Impersonation never escalates. The impersonating session resolves to the intersection of the target’s access and the impersonator’s own. A support engineer who cannot promote a deploy does not gain that by impersonating a release manager. Systems where impersonation adopts the target’s permissions wholesale contain a privilege-escalation path whose entry condition is “impersonate the most privileged user in the org.”
Further, the session is denied a fixed set of operations regardless of what the intersection allows:
- No Setup, no security configuration, no permission-set or session-policy change.
- No credential operation — no password reset, no MFA enrolment or bypass, no token or secret creation, no grant of login access to anyone else.
- No bulk export.
- No nested impersonation.
Read-only by default, time-boxed, and reasoned. Write-enabled impersonation is a separate permission (users.impersonate_write) and a shorter session. Every impersonation is capped — 30 minutes by default, one hour hard — and requires a reason string at the moment it starts, which lands in the audit entry. A reason typed under a support ticket number is worth more than a checkbox.
The actor is always the human. Every audit entry, every data-history row, and every execution trace produced during an impersonated session names the impersonator as actor and carries the impersonated principal as the effective subject. “Who changed this?” never answers with the person who was being impersonated. This is the whole reason the feature is safe to have, and it is why the attribution is not configurable.
It is impossible not to notice. A persistent banner across the top of every page names both identities and counts down the remaining time; the application chrome shifts to a distinct colour; the exit control is always present. The impersonated user sees a notification on their own record — impersonation is not a secret kept from its subject.
Impersonation of a service identity is refused outright. A machine identity has no interactive session to inhabit, and its access is reachable by ordinary means (users.manage can inspect its permission sets). Allowing it would be a way to borrow an integration’s permissions with a human’s convenience.
API and machine identity
Section titled “API and machine identity”A machine identity is a user. type: "service" on the user record, holding permission sets, subject to the same object and field permissions, the same record-access predicate, and the same FLS column masking as anybody else. It is not a bypass and there is no system mode for it to run in — consistent with the platform having no system mode at all.
Its differences from a person are constraints, not exemptions:
| Property | Service identity |
|---|---|
| Interactive login | Never |
| Seat | Counted on the service entitlement, not the interactive one |
platform.root |
Cannot hold it |
| Impersonation | Cannot be impersonated |
| Role | None — a machine is not in a management hierarchy |
| Credentials | Client secrets and tokens only; no password, no MFA enrolment |
Giving integrations a real, permissioned identity is the point. The alternative — a privileged bypass path for machines — makes “what can this integration see” unanswerable by the same query that answers it for a person, and makes an integration’s blast radius invisible in the security surface.
Client apps
Section titled “Client apps”A client_app component declares an OAuth client: its grant types, its redirect URIs, its scopes, and for the client-credentials grant, the service identity it runs as.
{ "key": "app_erp_sync", "label": "ERP inventory sync", "type": "client_app", "body": { "grantTypes": ["client_credentials"], "runAs": "svc_erp_sync", "scopes": ["object:item:read", "object:price_book:read", "object:inventory_snapshot:create"], "accessTokenLifetime": "PT1H", "networkRanges": [{ "cidr": "198.51.100.0/28", "label": "ERP egress" }], "requireDpop": true }}Scopes are drawn from the platform’s own permission vocabulary and are strictly subtractive. A scope names an object and an operation, or a system permission — the same vocabulary the permission model already uses. A token’s effective access is runAs identity's permissions ∩ app scopes ∩ (for user-delegated grants) the user's consent. A scope therefore cannot grant anything; it can only narrow. There is no full scope and no wildcard — an unnarrowed token is a decision an integration owner has to make explicitly, object by object, and the deploy diff shows exactly which objects an app’s blast radius grew to cover.
Secrets are shown once and rotate without an outage. A secret is displayed at creation and stored only as a hash. Rotation is dual-key: generating a new secret leaves the previous one valid for an overlap window the app owner sets, both work simultaneously, and existing access and refresh tokens are not revoked. The old secret expires at the end of the window or is retired early on demand. An emergency revoke that kills the old secret and every token issued under it exists as a separate, clearly-labelled action for the case where a secret leaked — which is the only case in which “revoke everything now” is the behavior anyone wants.
Every token expires. Access tokens default to one hour. Refresh tokens carry both an inactivity expiry (90 days by default) and an absolute maximum (one year); there is no valid until revoked option, because a refresh token with no expiry is a permanent credential whose existence nobody re-examines. Sender-constrained tokens (DPoP) are available per app and are the recommendation for anything running outside a fixed network range.
Personal access tokens cover the case a client app is too heavy for — a script, a notebook, a CI job acting as a named person:
- Always bounded by the issuing user’s own access; a PAT can never exceed what its owner can do, and it loses access the moment the owner does.
- Mandatory expiry, defaulting to 90 days, capped at one year. No never-expiring option.
- Named, individually revocable, and listed on the owner’s user record with last-used timestamps, so an unused token is visible as unused.
- Minted only by the owner. An administrator cannot create a token on someone else’s behalf — that would be impersonation with none of impersonation’s constraints.
- Every use appears in login history with the token’s name, so “which of my six tokens is hitting this endpoint” is answerable.
Login history and failed logins
Section titled “Login history and failed logins”Every authentication attempt is an event — success and failure alike — carried on the always-on execution stream with the same envelope and correlation id as everything else. There is no separate login-history product, no add-on, and no shorter retention for login events than for anything else in that stream: the default is 90 days hot plus archive, and archives are not destroyed.
An event records the principal, the outcome and its reason code, the method and resulting assurance, the identity provider if federated, the source address and its network, the device, the client app or token, and — where an org enables it — a coarse geo derived from the source. A login event links by correlation id to the session it created and therefore to every transaction that session ran, which makes “what did this compromised login do” one query rather than a reconstruction.
One message to the user, precise reasons to the audit stream. A failed login says the same thing whatever went wrong: the username and password combination is not valid. Not “no such user,” not “outside your login hours,” not “your account is locked,” not “your IP is not allowed.” Each of those is an oracle — the first enumerates valid usernames, the rest let an attacker map the org’s security policy by probing it. The audit stream records the actual reason code (no_such_principal, bad_credential, outside_login_hours, network_not_permitted, principal_deactivated, assurance_insufficient, locked), and the surfaces that show it are the ones a user has already authenticated to reach.
The one exception is a deliberate one: a user who is already authenticated and fails a step-up or an assurance check is told exactly what is required, because at that point the person’s identity is established and withholding the reason only prevents them from fixing it.
Throttling before lockout, and lockout that always ends.
- Failed attempts are throttled with an exponential backoff keyed on
(principal, source prefix)— the first few failures cost nothing, the tenth costs seconds. Most credential-guessing dies here without anybody being locked out. - A separate throttle keyed on source prefix alone catches a spray across many accounts, which per-principal counting misses entirely by design: one attempt against each of 500 users trips no user’s counter.
- Lockout after a configured count is time-bounded and self-clearing, maximum 24 hours. There is no permanent lockout. A lockout that never expires converts a credential-stuffing attempt into a guaranteed denial of service against the victim, executed by the platform on the attacker’s behalf. An administrator may clear a lockout early; nobody can make one last forever.
- Lockout ends sessions but does not deactivate the user, and it is a distinct state on the record with its remaining time shown, so “why can’t Dana log in” is answered by looking rather than by guessing.
Limits, and the reasons behind them
Section titled “Limits, and the reasons behind them”| Concern | CAOS | Salesforce | Why their limit exists |
|---|---|---|---|
| Idle session timeout | Any duration; 5 minutes minimum | Fixed list, 15 minutes shortest, 24 hours longest | A closed enum in SecuritySettings.sessionSettings.sessionTimeout |
| Absolute session lifetime | First-class, default 12 hours | Not offered — only idle timeout | Session lifetime is idle-based only |
| Concurrent sessions | A number on the session policy | No setting; documented as something you build with a Login Flow, or a Transaction Security Policy on the paid Event Monitoring / Shield add-on | Concurrency was never modelled as policy |
| Password minimum length | 12, floor of 8 | 5–50, default 8 | minimumPasswordLength in SecuritySettings |
| Failed-attempt lockout | Bounded, max 24 hours, always self-clearing | ThreeAttempts / FiveAttempts / TenAttempts / NoLimit; interval FifteenMinutes / ThirtyMinutes / SixtyMinutes / Forever |
A permanent-lockout option predates thinking of lockout as a DoS surface |
| Password expiry | Off by default | Never / 30 / 60 / 90 days / 6 months / 1 year, default NinetyDays |
Rotation-era default that modern guidance has moved away from |
| Refresh-token lifetime | Inactivity expiry + absolute cap; never unbounded | Default Valid until revoked | Long-lived tokens were the compatibility-safe default |
| OAuth scopes | Platform permission vocabulary; no wildcard | A fixed product-shaped list including full — “all data accessible by the logged-in user” |
Scopes were added per product, not derived from the permission model |
| Secret rotation | Dual-key with an owner-set overlap; tokens survive | Stage → apply; all tokens revoked, “up to 10 minutes of unstable behavior” | Rotation is a replacement, not an overlap |
| Impersonation duration | Capped, 30 minutes default | Not time-boxed | Login As predates session-scoped controls |
| Login history retention | 90 days hot + permanent archive, on the same stream as everything else | ~6 months in LoginHistory; extended retention via Login Forensics on the paid add-on (reported) |
Retention is a licensing tier |
How Salesforce does it
Section titled “How Salesforce does it”The login endpoint. Salesforce requires My Domain — a per-org login hostname — for setting a custom login policy, configuring SSO with external providers, and using several newer services, and orgs can turn on a setting preventing login through the generic login.salesforce.com entirely. Enhanced domains were subsequently enforced across sandboxes and production through the Winter ’23–Spring ’23 window (the enforcement dates are from secondary summaries).
SSO. SAML is configured as one or more SAML SSO configurations under Security Settings; the SecuritySettings.singleSignOnSettings metadata carries enableSamlLogin, enableMultipleSamlConfigs, enableSamlJitProvisioning, enableCaseInsensitiveFederationID, and isLoginWithSalesforceCredentialsDisabled. OIDC and social providers are configured as Auth Providers, a separate surface with a separate model.
JIT provisioning requires Apex. SAML JIT runs a Just-in-Time handler class; Auth Providers require a registration handler implementing Auth.RegistrationHandler, whose createUser / updateUser methods are invoked once the provider returns user information. Salesforce generates a template class, but the entry price for provisioning a user from an assertion is a deployed, tested Apex class.
Login Flows run after authentication succeeds and before the user reaches the app, built as a Flow and assigned per profile. Their documented gaps are the interesting part: they do not run for API logins or sessions entering through frontdoor.jsp; an administrator using Log in as another user is not subject to them at all; in Lightning runtime users may reach some functionality before the flow completes; and a flow that throws leaves the affected users stopped at an error screen. A control with that many exits is not a control.
Session settings. SecuritySettings.sessionSettings offers sessionTimeout from a closed enum (15 minutes to 24 hours), plus booleans including lockSessionsToIp, lockSessionsToDomain, and forceRelogin. ProfileSessionSetting overrides the timeout per profile and sets requiredSessionLevel to STANDARD or HIGH_ASSURANCE — Salesforce’s version of assurance-gated access, and a good idea. There is no absolute session lifetime and no concurrent-session setting; limiting concurrency is documented as something an admin builds, with a Login Flow.
lockSessionsToIp is the canonical foot-gun. Salesforce publishes a Known Issue that enabling it crashes the Salesforce iOS app on Hyperforce instances, third-party integrations return Invalid Session ID when it is on, and there is a standing IdeaExchange request to relax it. The setting is a single checkbox whose consequences arrive later, elsewhere, in someone else’s product.
Network and time restrictions live on profiles. Login IP ranges and login hours are profile settings, plus an org-wide trusted-IP list in SecuritySettings.networkAccess. Login hours are set per profile with no timezone field surfaced next to them, and a user denied by login hours sees the same error as a wrong password — which is the right behavior, and worth copying.
MFA has been a five-year rollout. Customers have been contractually required to use MFA since February 1, 2022. Auto-enablement rolled across releases from Spring ’23 through Spring ’24, switching the org-wide setting on while leaving admins able to switch it back off. Enforcement — where users without a compliant method are blocked from logging in — has been repeatedly rescheduled, most recently to June 22, 2026 for sandboxes and July 20, 2026 for production for standard MFA, with phishing-resistant MFA required for privileged users (System Administrator profile, or holders of Modify All Data, View All Data, Customize Application, or Author Apex) from July 1, 2026 in production. Standard-MFA enforcement was then paused again after a bug in the phishing-resistant enrolment flow prompted users with existing security keys to register new ones. Email, SMS, and phone one-time codes do not satisfy the requirement; for privileged users, neither do TOTP apps or push approvals. (The 2026 dates and the pause are from community and partner reporting; Salesforce’s own release note confirms the auto-enablement-then-enforcement structure.)
Connected apps, and their replacement. OAuth clients are Connected Apps with a fixed scope list — api, web, full, refresh_token/offline_access, openid, visualforce, custom_permissions, chatter_api, wave_api, eclair_api, cdp_*, pardot_api, sfap_api, and more — a list shaped by Salesforce’s product portfolio rather than by the permission model. Access policies cover permitted users (self-authorize vs admin-approved), four IP-relaxation modes, and a refresh-token policy defaulting to Valid until revoked. The client-credentials flow requires nominating an integration user to run as, and Salesforce’s own warning is blunt: “any person or app that has access to your external client app’s consumer key and consumer secret can get an access token.”
Secret rotation is staged then applied, and applying it means “all existing access and refresh tokens associated with the connected app are revoked,” followed by “up to 10 minutes of unstable behavior … during this time, the old and new consumer details work intermittently.” Rotating a credential is therefore an outage with a ten-minute nondeterministic tail — which is why secrets do not get rotated.
As of Spring ’26, creating new Connected Apps is restricted by default in both the UI and the Metadata API, with External Client Apps as the replacement and re-enabling creation requiring a support request (reported by multiple partner and ISV announcements; existing apps continue to work).
Login As requires Modify All Data and is switched on org-wide through Login Access Policies. Salesforce documents one consequential property: “While logging in as another user, you aren’t subject to any login flows, whether they’re associated with the user’s profile or your own.” Community threads asking how to find who logged in as whom are a recurring genre.
Identity is a licence. Salesforce Identity and Identity Plus are separate SKUs for users who need SSO and access management without CRM access, with External Identity for customers and partners (pricing and packaging from secondary licensing guides).
Where CAOS is genuinely better:
- Dual-key secret rotation. Two secrets valid simultaneously across an owner-set overlap window, with live tokens untouched. Salesforce’s rotation revokes every token and documents a ten-minute window of intermittent behavior, which converts routine hygiene into a change-window event.
- Scopes are the permission vocabulary, and strictly subtractive. A scope names an object and an operation from the same list the permission model uses, cannot grant anything the run-as identity lacks, and has no wildcard.
fullcannot exist in a model where scope is an intersection. - Impersonation is an intersection and never escalates. Binding it to a dedicated permission rather than Modify All Data, capping its duration, denying it Setup and every credential operation, and attributing every resulting entry to the impersonator rather than the impersonated user.
- Declarative JIT. Attribute and group mapping as component body, with an automation hook for the residue. Salesforce requires an Apex handler class before JIT provisions anything.
- Identity ownership is a field property, not a permission. An IdP-owned field is read-only to everyone including root, and a write fails with a named error instead of succeeding and being silently overwritten at the next login.
- MFA never had a rollout. On from tenant creation, undisableable, with federated MFA verified from the assertion rather than assumed. Every enforcement date, exemption list, and pause in the incumbent’s timeline is an artifact of retrofitting.
- Session policy is one component with an absolute lifetime, a concurrency number, timezone-carrying login hours, and prefix binding by default. Salesforce splits these across
SecuritySettings,ProfileSessionSetting, and profile pages; offers no absolute lifetime; makes concurrency something you build; and ships exact-IP binding as the only binding, with a published Known Issue attached. - Login history is not a product. Authentication events ride the always-on execution stream at full retention, with the correlation id that joins a login to everything the resulting session did. No add-on tier, no separate retention.
- Lockout always ends. No
Foreveroption, and a source-prefix throttle alongside the per-principal one so a low-and-slow spray is caught without locking out its targets.
Parity: federated SSO over SAML and OIDC, per-org login hostnames, JIT provisioning, assurance-gated access (Salesforce’s HIGH_ASSURANCE session level is the right idea and is copied), IP ranges and login hours as authentication-time conditions, OAuth 2.0 with authorization-code, refresh, and client-credentials grants, a nominated run-as identity for machine-to-machine, administrator impersonation, and returning an indistinguishable error for every login failure. None of this is reinvented.
Costs and risks:
- Undisableable MFA has no escape valve. An org whose IdP breaks, or whose sole administrator loses every factor, depends entirely on the bypass path being operable. That path is a support-operated procedure with its own identity-verification burden, and it must be built, staffed, and rehearsed — not documented and forgotten.
- Assurance-gated permission sets make access time-dependent. A user’s effective permissions differ between a passkey session and a password session, which is powerful and confusing. The effective-access resolver must report for a given session, and every “why can’t I see this” investigation now has an extra dimension.
- Most-restrictive session-policy resolution surprises admins trained on additive permissions. Two planes with opposite composition rules is a genuine cognitive cost, mitigated by the Setup surface showing which policy contributed each resolved value, and not by expecting people to remember.
- Splitting credentials into the control plane complicates tenant-level operations. Export, environment refresh, and per-tenant restore all now span two stores, and the boundary has to be honoured by every one of them. A tenant’s sandbox refresh must never carry production credentials into a lower environment.
- Dual-key rotation widens the window in which a leaked secret works. Two valid secrets is, briefly, twice the attack surface. The overlap is owner-set and bounded, and emergency revoke exists precisely because the safe default for a leak is different from the safe default for a rotation — but an org that sets a 90-day overlap has quietly opted out of the benefit.
- No
fullscope means integration authors do more work. Every client app enumerates its objects, and every new object an integration needs is a metadata change. That is the intent, and it is friction that will be complained about. - Prefix-based session binding is weaker than exact binding. An attacker on the same network prefix as the victim can replay a stolen session. The default trades a narrow attack for a broad availability failure; orgs with a genuine hard perimeter should set
exactand accept the mobile consequences.
Metadata & deploy representation
Section titled “Metadata & deploy representation”| Component | type |
Body |
|---|---|---|
| Identity provider | identity_provider |
protocol, issuer/metadata, domains, assurance, jit |
| Login policy | login_policy |
methods[], allowPasswordLogin, breakGlass[], passwordPolicy |
| Session policy | session_policy |
timeouts, concurrentSessions, networkBinding, ipRanges, loginHours, deviceTrust, stepUp |
| MFA policy | mfa_policy |
acceptedFactors[], minimum assurance per permission-set group, enrolment rules |
| Client app | client_app |
grantTypes, runAs, scopes, token lifetimes, networkRanges, requireDpop |
Everything about a person is record data and never a component: user records, permission-set assignments, MFA enrolments, IdP subject bindings, sessions, personal access tokens, and client-app secrets. This is the same split permissions and sharing already draw, and here it also carries a security property: a metadata deploy moves policy between environments and can never move a credential, so a production secret cannot arrive in a sandbox by way of a retrieve.
Client-app secrets deploy as references, not values. A client_app component names its secret; the secret itself is created per environment and lives in the control plane. Retrieving the component from production and applying it to a sandbox produces an app with no secret rather than an app with production’s.
Deploying a tightened session or MFA policy takes effect at the generation flip like any other metadata change, and — unlike a permission change, which affects the next query — it also re-evaluates live sessions on their next request.
Salesforce Metadata API analogs, for migration mapping: SamlSsoConfig and AuthProvider (identity providers), SecuritySettings with its passwordPolicies, sessionSettings, networkAccess, and singleSignOnSettings blocks, ProfileSessionSetting (per-profile timeout and requiredSessionLevel), Profile (loginIpRanges, loginHours), LoginFlow, ConnectedApp and its successor ExternalClientApplication, and the User, LoginHistory, and LoginEvent objects for the record-data side.
Sources
Section titled “Sources”- SecuritySettings — Metadata API Developer Guide — exact enums for
passwordPolicies(complexity, expiration,historyRestriction0–24,lockoutIntervalincludingForever,maxLoginAttempts,minimumPasswordLength5–50 default 8),sessionSettings.sessionTimeout(15 minutes – 24 hours),networkAccess.ipRanges, andsingleSignOnSettings. - ProfileSessionSetting — Metadata API Developer Guide — per-profile
sessionTimeoutvalues and theSessionSecurityLevelenum (STANDARD,HIGH_ASSURANCE,LOW). - OAuth Tokens and Scopes — Salesforce Help — the full scope list including
full(“all data accessible by the logged-in user”),refresh_token/offline_access, and token-lifetime characterizations. - Manage OAuth Access Policies for a Connected App — Salesforce Help — permitted-users options, the four IP-relaxation modes, and the refresh-token policy defaulting to Valid until revoked.
- View and Rotate the Consumer Key and Consumer Secret of a Connected App — Salesforce Help — staged values, “all existing access and refresh tokens … are revoked,” and “up to 10 minutes of unstable behavior.”
- OAuth 2.0 Client Credentials Flow — Salesforce Help — the required run-as integration user, no refresh tokens, and the warning that anyone holding the key and secret can obtain a token.
- Log In as Another User — Salesforce Help — Modify All Data requirement, and “while logging in as another user, you aren’t subject to any login flows.”
- Custom Login Flows — Salesforce Help — login flows run after authentication and before the landing page; the API /
frontdoor.jsp/ Log-in-as gaps and the Lightning-runtime caveat. - Just-in-Time Provisioning for SAML — Salesforce Help — the JIT handler class, assertion-field requirements, and JIT provisioning errors.
Auth.RegistrationHandler— Apex Reference Guide —createUser/updateUserare invoked once the provider returns user information.- Configure an Authentication Provider Using OpenID Connect — Salesforce Developers — Auth Providers as the OIDC surface and the registration-handler requirement.
- MFA Auto-Enablement Continues and MFA Enforcement Begins with Summer ’24 — Salesforce Release Notes — the auto-enablement-then-enforcement structure.
- Everything You Need to Know About Multi-Factor Authentication for Salesforce Orgs — Salesforce Help — the February 1, 2022 contractual requirement and acceptable factors.
- How to Prepare for Salesforce’s Mandatory MFA Changes in 2026 — Salesforce Ben (secondary) — scope split between standard and phishing-resistant MFA, excluded factors, and the admin cost complaints.
- Salesforce MFA Enforcement Update: What Was Paused, What Still Applies, and the Revised 2026 Dates (secondary) — the June 22 / July 20, 2026 dates, the July 1 privileged-user date, and the enrolment-flow bug that paused standard enforcement.
- Salesforce is enforcing phishing-resistant MFA: what you need to know — Bitwarden (secondary) — the privileged-user definition (System Administrator profile, Modify All Data, View All Data, Customize Application, Author Apex) and the FIDO2/WebAuthn-only requirement.
- “Lock sessions to the IP address from which they originated” causes the Salesforce iOS app to crash on Hyperforce instances — Salesforce Known Issues — the published defect.
- Relax “Lock sessions to the IP address from which they originated” — IdeaExchange — the standing request to soften exact-IP binding.
- Limit the Number of Concurrent Sessions with Login Flows — Salesforce Help — concurrency limiting is documented as a flow to build, not a setting.
- My Domain — Salesforce Help — the per-org login hostname and what requires it.
- Restrict Login Hours in Salesforce — Roycon (secondary) — login hours are a profile-level setting, and a denied user sees the same error as a wrong password.
- Salesforce Security Auditing: Login History — Onapsis (secondary) —
LoginHistoryfields and its approximately six-month retention. - Salesforce Forensics: Telemetry for Detection & Response — Abstract Security (secondary) — Login Forensics extends login retention and rides the Event Monitoring add-on.
- Salesforce Is Phasing Out Connected Apps — What the Spring ’26 Change Really Means (secondary) — creation restricted by default in UI and Metadata API, External Client Apps as the replacement, existing apps unaffected.
- Salesforce Identity vs Identity Plus licences (secondary) — Identity, Identity Plus, and External Identity as separate SKUs for users who need SSO without CRM access.