socialiteproviders/microsoft-azure
Laravel Socialite provider for Microsoft Azure (Entra ID) OAuth login. Configure client ID, secret and tenant to sign users in, retrieve their id, name and email, support multiple tenants, and generate Azure logout URLs.
Install the provider alongside Socialite and the manager package it depends on:
composer require socialiteproviders/microsoft-azure
Then three things must all be true before Socialite::driver('azure') works — missing any one of them produces a confusing error rather than an obvious one:
https://app.test/auth/azure/callback) and create a client secret.config/services.php:'azure' => [
'client_id' => env('AZURE_CLIENT_ID'),
'client_secret' => env('AZURE_CLIENT_SECRET'),
'redirect' => env('AZURE_REDIRECT_URI'),
'tenant' => env('AZURE_TENANT_ID', 'common'),
],
SocialiteWasCalled listener. On Laravel 11+ this goes in AppServiceProvider::boot():use Illuminate\Support\Facades\Event;
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('azure', \SocialiteProviders\Azure\Provider::class);
});
On Laravel 10 and below, add the same listener class to $listen in app/Providers/EventServiceProvider.php.
Where to look first: the tenant value. It is not optional in practice — it is interpolated into both the authorize and token endpoints (login.microsoftonline.com/{tenant}/oauth2/v2.0/...). Use common for "any Microsoft account", organizations for work/school only, consumers for personal accounts only, or a tenant GUID for single-tenant apps.
First use case: an internal tool where you want to restrict sign-in to your own organisation. Point tenant at your directory GUID, add the routes, and you have SSO in an afternoon:
Route::get('/auth/azure/redirect', fn () => Socialite::driver('azure')->redirect());
Route::get('/auth/azure/callback', function () {
$azureUser = Socialite::driver('azure')->user();
// ['id' => oid, 'name' => displayName, 'email' => mail, 'nickname' => mailNickname]
});
Persist the provider identity, not the email. The package is a thin HTTP wrapper — it does not create users, store tokens, or link accounts. Your callback is responsible for all of it. Store a provider + provider_user_id pair (the Azure id claim is the immutable oid) with a unique index, and resolve users through it:
$azureUser = Socialite::driver('azure')->user();
$account = SocialAccount::firstOrCreate(
['provider' => 'azure', 'provider_user_id' => $azureUser->getId()],
['email' => $azureUser->getEmail(), 'name' => $azureUser->getName()]
);
Auth::login($account->user);
Match on provider_user_id first and only fall back to email-linking for accounts you have already verified. Email local-parts get recycled inside tenants.
Scope requests deliberately. The default scope set covers openid profile email plus User.Read for the Graph call that populates the user object. When you need more (groups, directory, SharePoint), request it per-flow:
return Socialite::driver('azure')->scopes(['User.Read', 'GroupMember.Read.All'])->redirect();
Any scope beyond the basics requires admin consent on the app registration, otherwise the callback fails with AADSTS65001.
Store refresh tokens if you need Graph later. Add offline_access to the scopes and read the raw token response — Socialite's user() object discards it:
$socialite = Socialite::driver('azure');
$user = $socialite->user();
$token = $socialite->getAccessTokenResponse($socialite->getAccessToken()); // has refresh_token
Encrypt these at rest and refresh proactively; the package will not do it for you.
Keep the tenant in sync with your data model. A user's oid is unique per tenant. If you later add a second tenant (e.g. after an acquisition), the same human arrives as a new provider_user_id. Treat tenant as part of the account key if you expect multi-tenant usage.
Testing. Point graph_url at a local mock server (WireMock, Http::fake-backed route, etc.) so the profile call is deterministic without touching Microsoft. Note the access token is sent to whatever URL is configured — see the gotchas below. Override per-test via config(['services.azure.graph_url' => 'http://127.0.0.1:8080/me']).
Driver [azure] is not supported means the event listener is missing or registered after Socialite resolves the driver. This is the single most common install failure, and it is silent in production if you only registered it in a service provider that is not loaded in console/queue contexts.invalid_state on callback almost always means the session was lost between redirect and return. Usual culprits: SESSION_DOMAIN not covering both hosts, APP_URL differing from the actual host, HTTPS behind a proxy without TrustProxies configured, or a redirect URI that goes from app.test to www.app.test. Fix the root cause; ->stateless() disables CSRF state verification and should only be used for genuinely stateless API flows.graph_url sends your access token there. The README warns about this for good reason — never point it at an untrusted host, and never let it be user-influenced (e.g. driven by a request header or a shared env var in a multi-tenant app). Pin it in config/services.php only.mail is frequently null. Personal Microsoft accounts and some guest accounts have no mail claim. Fall back to userPrincipalName from $user->getRaw(), and decide explicitly whether a null email is a login failure or an account-creation prompt.invalid_client on the token exchange, days after you have forgotten about it. Calendar it, or move to certificate credentials. Also remember secrets are only visible once — store them in your password manager, not a README.http://localhost:8000/auth/azure/callback and http://127.0.0.1:8000/... are different registrations. Also make sure you registered it under Web, not Single-page application — the SPA platform does not accept a client secret.php artisan config:cache in deploy means a changed AZURE_TENANT_ID has no effect until config:clear. This has burned many people mid-debugging.AADSTS65004), admins deny (AADSTS65001), and state validation throws Laravel\Socialite\Two\InvalidStateException. None of these should produce a 500 — redirect back to your login page with a flash message and log the exception.proxy config exists for locked-down networks where outbound HTTPS must go through a forward proxy. It only applies to this provider's Guzzle client, not the rest of your app.https://login.microsoftonline.com/{tenant}/oauth2/v2.0/logout?post_logout_redirect_uri=... yourself and register the post-logout URI in Entra ID.SocialiteProviders\Azure\Provider, override mapUserToObject(), and register your subclass in the extendSocialite listener instead of forking.How can I help you explore Laravel packages today?