socialiteproviders/microsoft
Microsoft provider for Laravel Socialite adds Microsoft (Azure AD) OAuth login to your Laravel app. Install via Composer, configure your client ID, secret, and tenant, then use Socialite::driver('microsoft') to authenticate users and retrieve profile data.
socialiteproviders/microsoft is a thin Socialite driver: it supplies the Entra ID / Azure AD v2.0 OAuth endpoints and maps the Graph me payload into a Laravel\Socialite\Two\User. It does nothing on its own — it plugs into laravel/socialite.
Install and wire it up:
composer require socialiteproviders/microsoft
// config/services.php
'microsoft' => [
'client_id' => env('MICROSOFT_CLIENT_ID'),
'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
'redirect' => env('MICROSOFT_REDIRECT_URI'),
'tenant' => env('MICROSOFT_TENANT_ID', 'common'), // common | organizations | consumers | <tenant-guid>
],
Register the event listener. On Laravel 10 and earlier, add it to app/Providers/EventServiceProvider.php:
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Microsoft\MicrosoftExtendSocialite::class.'@handle',
],
];
On Laravel 11+ event discovery usually picks this up, but if Socialite::driver('microsoft') throws InvalidArgumentException: Driver [microsoft] is not supported, register it explicitly — see Gotchas.
Routes:
Route::get('/auth/microsoft/redirect', fn () => Socialite::driver('microsoft')->redirect());
Route::get('/auth/microsoft/callback', function () {
$ms = Socialite::driver('microsoft')->user();
// $ms->getId() -> oid
// $ms->getName() -> displayName
// $ms->getEmail() -> mail ?? userPrincipalName
// $ms->getRaw() -> full Graph /me payload
});
Where to look first:
vendor/socialiteproviders/microsoft/MicrosoftProvider.php) — it's ~150 lines and tells you exactly which claims map to which User fields, and which scopes are requested by default.config/services.php and config/session.php — the two config files that cause most "it works locally" failures.First use case: add a "Sign in with Microsoft" button next to your existing Google/GitHub buttons, route the callback into the same account-linking logic you already have, and key users on oid (not email).
Minimal controller flow. The provider is stateless about your app; you own the user record. A typical callback:
public function callback()
{
try {
$ms = Socialite::driver('microsoft')->user();
} catch (\Laravel\Socialite\Two\InvalidStateException $e) {
return redirect('/login')->withErrors('Login session expired, please retry.');
}
$account = SocialAccount::firstOrCreate(
['provider' => 'microsoft', 'provider_user_id' => $ms->getId()],
[
'tenant_id' => $ms->getRaw()['tid'] ?? null,
'email' => $ms->getEmail(),
'name' => $ms->getName(),
]
);
Auth::login($account->user);
return redirect()->intended('/dashboard');
}
Request extra scopes. Pass an array to scopes(); it replaces the driver defaults, so include the basics you still need:
Socialite::driver('microsoft')
->scopes(['openid', 'profile', 'email', 'offline_access', 'User.Read'])
->redirect();
offline_access is what gets you a refresh_token in $ms->accessTokenResponseBody. Without it you have a ~1 hour access token and nothing to refresh with.
Per-request tenant override. For B2B apps that want a "sign in with your work account" flow separate from "personal account", you can override the tenant per redirect using with() (exact key depends on your pinned version — check getTenant() in the provider source):
Socialite::driver('microsoft')
->with(['tenant' => 'organizations'])
->redirect();
Store tokens if you'll call Graph later. Socialite hands you the token but does not manage its lifecycle:
$token = $ms->token; // access token
$refresh = $ms->refreshToken; // if offline_access requested
$expires = $ms->expiresIn; // seconds
Encrypt at rest (encrypted cast), store expires_at, and build a small refresh helper. This is when "authentication only" quietly becomes "Graph integration" — plan for it up front.
Testing. Mock Socialite so tests never hit Microsoft:
Socialite::shouldReceive('driver->user')->andReturn(
(new \Laravel\Socialite\Two\User)->map([
'id' => '00000000-0000-0000-0000-000000000000',
'name' => 'Test User',
'email' => '[email protected]',
])
);
Combine with an integration test against a dedicated non-production Entra tenant for the real OAuth round-trip.
Feature-flag the rollout. Wrap the routes behind a flag and pilot with an internal group in a dev tenant before enabling the production tenant. This buys you a clean rollback without touching code.
Keep the adapter thin. Put the provider-specific mapping (claim selection, tenant handling) in one small service or SocialAccount factory. That's the seam you swap if you ever move to microsoft-azure, MSAL, or an in-house AbstractProvider.
oid is per-tenant, not global. The same person as a guest in two tenants has two oid values. Store oid + tid together and treat the pair as the identity key. Never key on email or userPrincipalName — UPNs change, guests often have a #EXT# UPN, and personal accounts may have no mail at all.Driver [microsoft] is not supported, event discovery didn't find MicrosoftExtendSocialite. Either keep the listener in EventServiceProvider (works on all versions), or explicitly Event::listen(SocialiteWasCalled::class, MicrosoftExtendSocialite::class) in a service provider boot().config:cache silently bakes credentials. After changing MICROSOFT_TENANT_ID, client_secret, or the redirect, run php artisan config:cache (or config:clear) in every environment. "It works locally but 500s in staging" is almost always this.http vs https, localhost vs 127.0.0.1, and port all count. A mismatch surfaces as AADSTS50011 at the Microsoft login page — the user never reaches your callback.Mail.Read or a Graph scope requires re-consent; without admin consent you'll see AADSTS65001 for regular users. Grant admin consent in the app registration, or design a consent-prompt fallback.AADSTS700016 (app not in tenant). In single-tenant mode, a user from another tenant cannot sign in. If guests and B2B collaborators are in scope, use common or organizations and handle the tenant yourself.login.microsoftonline.com is the tell. Rotation is a config change plus config:cache.getEmail() can be null. The provider falls back to userPrincipalName, but personal accounts often only have email in the id_token, not me. For anything identity-critical, read $user->getRaw()['mail'] ?? $user->getRaw()['userPrincipalName'] ?? $user->getId()./me doesn't return a photo; the provider leaves avatar empty. If you need one, call https://graph.microsoft.com/v1.0/me/photo/$value with the stored access token — and handle 404 for users with no photo.InvalidStateException intermittently. Fix the session driver, not the provider.->scopes() replaces defaults. If you pass only ['User.Read'] you'll lose openid / profile / email and get a weird, sparse user object. Always list the full set you want.common = work/school + personal; organizations = work/school only; consumers = personal only; a GUID = that tenant only. Pick deliberately — it determines who can log in, not just where they land.microsoft vs microsoft-azure split. Both exist in the SocialiteProviders ecosystem with different config keys and claim mapping. Pick one and stay there; mixing config blocks is a common source of "why is my id null".socialiteproviders/providers monorepo. Pin the package and verify Composer constraints against your installed laravel/socialite and socialiteproviders/manager when you upgrade either. If the package stalls on a Socialite major, the driver is small enough to fork into an in-house AbstractProvider subclass — budget a day for that, not a week.AADSTS code from the query string (?error=...&error_description=...) so support can triage.How can I help you explore Laravel packages today?