How to move an established platform from passwords to Azure (Microsoft Entra ID) login: the OpenID Connect flow, matching existing accounts, a staged rollout, and the failures you will meet in production.
On a platform with years of history, authentication is the one change where a mistake is immediately visible to every user. Nobody notices a faster query; everybody notices when they cannot log in. This is the approach I use for moving an existing Laravel application to Azure credential-based login (Microsoft Entra ID, formerly Azure AD), written so that the switch is boring for users.
On the platform I work on, moving staff to Azure credentials cut login-related issues by about 30%. Most of that came not from the protocol but from the rollout details below.
Why bother
Password login inside an application carries a long tail of work: resets, lockouts, weak passwords, people leaving the company who still have an account. With single sign-on the organisation's identity provider owns all of that. Joiners and leavers are handled in one place, multi-factor authentication comes for free, and the application stops storing secrets it does not need.
The flow: OpenID Connect with authorisation code
Use the authorisation code flow with PKCE. The browser never sees an access token, and PKCE protects the code if it is intercepted.
1.
The user clicks "Sign in with Microsoft". The app generates a random state, a nonce and a PKCE code_verifier, stores them in the session, and redirects to the tenant's authorise endpoint.
2.
The user signs in at Microsoft (with MFA if the tenant requires it) and is redirected back with a code and the state.
3.
The app checks that state matches the session, then exchanges the code (plus the verifier) for tokens on the back channel.
4.
The app validates the ID token and starts its own session.
In Laravel, Socialite with the community Microsoft/Azure provider covers steps 1 to 3. Step 4 is where you must be strict.
Validate the ID token properly
A token that parses is not a token you can trust. Check every one of these:
▸
Signature against the tenant's published keys (JWKS), cached and refreshed when a new kid appears.
▸
Issuer equals your tenant's issuer, and audience equals your app's client ID.
▸
Tenant ID (tid) is your organisation. A multi-tenant app registration will otherwise accept any Microsoft account in the world.
▸
Expiry and not-before, with a small clock-skew allowance (a minute or two), because servers drift.
▸
Nonce matches the one you put in the session.
Identify users by the immutable object ID (oid), not by email. Emails and UPNs change when people marry, change department or the company renames its domain; the object ID does not.
Matching existing accounts
The hard part is not the protocol; it is the thousands of accounts that already exist. I add a small table linking local users to identities:
sql
CREATE TABLE user_identities (
user_id BIGINT UNSIGNED NOT NULL,
provider VARCHAR(20) NOT NULL, -- 'azure'
subject VARCHAR(64) NOT NULL, -- the oid claim
linked_at DATETIME NOT NULL,
PRIMARY KEY (provider, subject),
UNIQUE KEY uq_user_provider (user_id, provider)
);
On the first Azure login, look up (azure, oid). If there is no link yet, find the local user by the verified email from the token, create the link, and from then on only the oid matters. If no local user matches, do not create one automatically; show a clear "ask your administrator for access" page and log it. Silent auto-provisioning is how test accounts and former contractors end up with access.
Roll out in stages
▸
Feature flag per group. Start with the IT team, then one business unit at a time.
▸
Keep password login as a fallback for a defined period, visible only to accounts that have not linked yet. Remove it on a published date.
▸
Map roles in one place. Application roles can come from Entra ID app roles or groups, but keep a single mapping function in the code so you can explain who gets which role.
▸
Log every failure with a reason (state_mismatch, no_local_account, wrong_tenant, token_expired). A dashboard of those reasons tells you exactly what to fix next.
The failures you will see
▸
Redirect URI mismatch after a domain or path change. Register every environment's callback URL explicitly.
▸
Lost session on the callback because the session cookie is SameSite=Strict. The return from Microsoft is a cross-site navigation, so the session cookie needs SameSite=Lax.
▸
Clock skew on a server whose NTP has stopped, which shows up as "token not yet valid".
▸
Users with two accounts (an old one under a personal email). Decide the merge rule before the rollout, not during it.
Logging out
Ending the application session is not enough if the user expects to be signed out of Microsoft too. Offer single sign-out via the end-session endpoint, and make the application's own session lifetime shorter than you think: re-authentication with SSO is a redirect, not a password prompt.
Takeaway
Treat SSO as a migration, not a feature. The protocol is a solved problem; account linking, staged rollout and good failure logging are what decide whether your support inbox gets quieter or louder.