Microsoft Azure Login for Self-Hosted Supabase: Setup Guide

Configure Microsoft Entra ID (Azure) OAuth for self-hosted Supabase. App registration, GoTrue env vars, tenant restrictions, and the xms_edov claim.

Cover Image for Microsoft Azure Login for Self-Hosted Supabase: Setup Guide

If you're building anything B2B, "Sign in with Microsoft" isn't optional. Your customers live in Microsoft 365, their IT departments manage identities in Entra ID, and asking them to create yet another password is a great way to lose the deal. On Supabase Cloud, enabling Azure login is a dashboard toggle. On self-hosted Supabase, there's no toggle — you're editing GoTrue environment variables by hand, and the official self-hosted OAuth docs cover the mechanism but not the Azure-specific traps.

This guide walks through the full setup: registering the app in Microsoft Entra ID, wiring the credentials into your Docker Compose stack, locking sign-in to a single tenant, and configuring the xms_edov claim that protects you from a nasty class of email-spoofing attacks. It builds on our general OAuth providers overview for self-hosted Supabase — if you've already set up Google or GitHub, the GoTrue side will feel familiar. The Azure side has more sharp edges.

Before You Start: Know Your Callback URL

Every OAuth flow hinges on one URL: the callback that Microsoft redirects users to after they authenticate. For self-hosted Supabase, that's your API gateway's public URL plus the auth path:

https://api.yourdomain.com/auth/v1/callback

Two things to verify before touching the Azure portal:

  1. Your instance must be reachable over HTTPS on a real domain. Microsoft won't redirect to plain HTTP (except localhost), and your users' browsers need a valid certificate. If you haven't set this up yet, see our custom domains guide.
  2. Check what API_EXTERNAL_URL actually is in your .env. As of the mid-2026 stack updates, the default API_EXTERNAL_URL handling changed — if you upgraded recently, confirm your auth endpoints resolve where you think they do. Our June 2026 breaking changes prep guide covers what moved.

Curl the health endpoint to confirm:

curl https://api.yourdomain.com/auth/v1/health

If that returns JSON, you're ready.

Step 1: Register the Application in Microsoft Entra ID

  1. Sign in at portal.azure.com and open Microsoft Entra ID (the service formerly known as Azure Active Directory — you'll still see "Azure AD" in older docs and error codes).
  2. Go to App registrationsNew registration.
  3. Name it something users will recognize — this name appears on the Microsoft consent screen.
  4. Choose the supported account types. This decision matters more than it looks:
    • Single tenant — only users in your Entra directory. Right for internal tools.
    • Multitenant — any organization's work/school accounts. Right for B2B SaaS.
    • Multitenant + personal Microsoft accounts — adds outlook.com/Xbox/personal accounts. Right for consumer-facing apps.
  5. Under Redirect URI, select platform Web and enter your callback URL from above.
  6. Click Register, then copy the Application (client) ID from the overview page.

Create the Client Secret

Go to Certificates & secretsNew client secret. Here's the gotcha that generates half the Azure OAuth support threads: after creation, the table shows both a Value and a Secret ID. You want the Value. The Secret ID is a useless UUID that will produce AADSTS7000215: Invalid client secret errors if you paste it into GoTrue. Copy the Value immediately — it's only shown once.

Also note the expiry. Azure client secrets expire — the default is 6 months, the maximum is 24. Unlike Google or GitHub credentials that live forever, this one is a time bomb. When it expires, every Microsoft login on your instance fails with AADSTS7000222 until you rotate it. Put the expiry date in your calendar now, and treat rotation as part of your regular ops routine alongside API key rotation.

Configure the xms_edov Claim (Don't Skip This)

Entra ID has a documented weakness: it can issue tokens containing email addresses the tenant admin typed in but never verified. An attacker who controls their own Entra tenant can claim [email protected] as an email attribute — this is the basis of the "nOAuth" account-takeover technique. Supabase Auth defends against this using the optional xms_edov claim, which tells GoTrue whether Microsoft actually verified the email.

To enable it:

  1. In your app registration, open Token configurationAdd optional claim.
  2. Select token type ID, check xms_pdl if listed and look for xms_edov. If xms_edov isn't in the picker (it sometimes isn't), open Manifest and add it manually to optionalClaims.idToken:
"optionalClaims": {
  "idToken": [
    { "name": "xms_edov", "essential": false }
  ]
}

Without this claim, treat every Azure-provided email as unverified — which matters a lot if you use identity linking, because automatic account merging on an attacker-controlled email is exactly how accounts get hijacked.

Step 2: Configure GoTrue

Add the credentials to your Supabase .env file:

GOTRUE_EXTERNAL_AZURE_ENABLED=true
GOTRUE_EXTERNAL_AZURE_CLIENT_ID=your-application-client-id
GOTRUE_EXTERNAL_AZURE_SECRET=your-client-secret-value
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI=https://api.yourdomain.com/auth/v1/callback

Then map them into the auth service in docker-compose.yml (or an override file, so upstream updates don't clobber your changes):

auth:
  environment:
    GOTRUE_EXTERNAL_AZURE_ENABLED: ${GOTRUE_EXTERNAL_AZURE_ENABLED}
    GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${GOTRUE_EXTERNAL_AZURE_CLIENT_ID}
    GOTRUE_EXTERNAL_AZURE_SECRET: ${GOTRUE_EXTERNAL_AZURE_SECRET}
    GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${GOTRUE_EXTERNAL_AZURE_REDIRECT_URI}

Restricting Sign-In to Your Tenant

By default GoTrue uses Microsoft's common endpoint, which accepts any account matching your app registration's account-type setting. For internal tools, add a second layer of enforcement by pointing GoTrue at your tenant-specific endpoint:

GOTRUE_EXTERNAL_AZURE_URL=https://login.microsoftonline.com/your-tenant-id

Belt and suspenders: the app registration's "single tenant" setting enforces this on Microsoft's side, and GOTRUE_EXTERNAL_AZURE_URL enforces it on yours. Use both — misconfigured multitenant apps are a recurring finding in security audits.

Apply the changes:

docker compose up -d auth

GoTrue reads configuration only at startup, so a restart is mandatory. This is the core operational difference from Supabase Cloud: every provider change means an env edit and a container bounce. If you manage several instances, this is exactly the kind of toil Supascale removes — its OAuth configuration UI writes provider settings (Google, GitHub, Discord, Azure, and more) and handles the service restart for you, the same way it manages redirect URLs and Site URL configuration without hand-editing .env files.

Step 3: Trigger the Flow From Your App

With supabase-js:

const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'azure',
  options: {
    scopes: 'email',
    redirectTo: 'https://app.yourdomain.com/auth/callback',
  },
});

The provider name is azure (not microsoft, not entra). Request the email scope explicitly — without it, Microsoft may omit the email claim entirely and you'll get users with null emails in auth.users. If you need refresh tokens for calling Microsoft Graph later, add offline_access to the scopes.

Make sure redirectTo is on your redirect allow list (GOTRUE_URI_ALLOW_LIST), or users will land on your Site URL instead of where you sent them.

Common Failures and What They Mean

ErrorCauseFix
AADSTS50011: redirect URI mismatchCallback URL in Azure doesn't exactly match GoTrue'sCompare character-by-character — trailing slashes and http vs https count
AADSTS7000215: invalid client secretYou pasted the Secret ID, not the ValueCreate a new secret, copy the Value
AADSTS7000222: expired client secretSecret passed its expiry dateRotate the secret, update .env, restart auth
AADSTS50194: not configured as multi-tenantApp is single-tenant but GoTrue uses commonSet GOTRUE_EXTERNAL_AZURE_URL to your tenant endpoint
User created with no emailemail scope not requestedAdd scopes: 'email' to signInWithOAuth

One more honest caveat: Azure login gets you social/OAuth sign-in. If your enterprise customers ask for SSO with automatic user provisioning — SCIM, IdP-initiated flows, per-domain enforcement — that's SAML territory, which has its own setup on self-hosted instances. See our enterprise SSO and SAML guide for when OAuth stops being enough.

Conclusion

Azure OAuth on self-hosted Supabase comes down to four things done carefully: an app registration with the right account-type scope, the secret Value (not ID) in your GoTrue env, the xms_edov claim to keep unverified emails from becoming an attack vector, and a calendar reminder for secret expiry — the one failure mode that will bite you in production even if everything works today. The GoTrue configuration itself is five environment variables and a container restart. The operational discipline around it — rotation, tenant restrictions, redirect hygiene across environments — is where self-hosters actually earn their keep, and where tooling that manages provider config across instances pays for itself.

Further Reading