Skip to main content

Microsoft OAuth

WIP

Microsoft Entra ID / personal Microsoft accounts via OpenID Connect.

Setup

  1. Register an application in the Microsoft Entra admin center.
  2. Under Authentication, add a redirect URI of type Web: https://app.example.com/auth/microsoft/callback.
  3. Under Certificates & secrets, create a client secret.
  4. Note the Application (client) ID and the Directory (tenant) ID.
import { authMicrosoft } from '@gentleduck/auth/providers/oauth/microsoft'

export const auth = new AuthEngine({
  // ...
  providers: [
    authMicrosoft({
      clientId: process.env.MICROSOFT_CLIENT_ID!,
      clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
      tenant: process.env.MICROSOFT_TENANT_ID!,   // or 'common' for multi-tenant
      redirectUri: 'https://app.example.com/auth/microsoft/callback',
      stateSigningSecret: process.env.OAUTH_STATE_SECRET!,
      scopes: ['openid', 'email', 'profile'],
      onSignIn: async ({ profile, tenantId }) => {
        let identity = await auth.identities.findByProviderSub({
          providerId: 'microsoft',
          sub: profile.sub,
        })
        if (!identity) {
          identity = await auth.identities.create({
            email: profile.email,
            emailVerified: true,
            profile: {
              displayName: profile.name,
              tid: profile.tid,           // Microsoft tenant id
              upn: profile.preferred_username,
            },
          })
          await auth.identities.linkProvider(identity.id, {
            providerId: 'microsoft',
            sub: profile.sub,
          })
        }
        return { identityId: identity.id }
      },
    }),
  ],
})

Tenant scoping

The tenant option controls which directories can sign in:

ValueAllowed accounts
Your tenant ID (GUID)Only users in your Entra tenant.
commonAny Microsoft work, school, or personal account.
organizationsAny Microsoft work or school account (not personal).
consumersOnly personal Microsoft accounts.

For B2B SaaS apps that want to support any organization, use common or organizations. For internal apps, use your tenant ID for a hard gate.

Multi-tenant: restrict by tid

Even with tenant: 'common', you can post-filter by the tid claim inside onSignIn:

onSignIn: async ({ profile }) => {
  const allowedTenants = ['<contoso tid>', '<fabrikam tid>']
  if (!allowedTenants.includes(profile.tid)) {
    throw new AuthError('AUTH/PROVIDER_FAILED', {
      reason: 'tenant-not-whitelisted',
    })
  }
  // ...
}

What the profile contains

Microsoft's userinfo endpoint returns OIDC standard claims plus Microsoft extensions:

{
  sub: 'A1B2C3D4-...',                   // stable user id within the tenant
  oid: 'A1B2C3D4-...',                   // object id (same as sub for Entra)
  tid: 'E5F6G7H8-...',                   // tenant id
  email: 'ada@contoso.com',
  preferred_username: 'ada@contoso.com', // UPN
  name: 'Ada Lovelace',
  given_name: 'Ada',
  family_name: 'Lovelace',
}

Common errors

Errorduck-auth codeCause
AADSTS50011AUTH/PROVIDER_FAILEDRedirect URI mismatch.
AADSTS70008AUTH/PROVIDER_FAILEDAuth code expired.
AADSTS65001AUTH/PROVIDER_FAILEDAdmin consent required for app's scopes.
AADSTS500113AUTH/PROVIDER_FAILEDNo reply address registered.