Microsoft 365 / Azure AD SSO Setup Guide

January 17, 2026 · View on GitHub

This guide provides detailed, step-by-step instructions for configuring Microsoft 365 or Azure Active Directory (Azure AD) authentication with Winter CMS.

Overview

Microsoft's identity platform supports multiple authentication scenarios:

  • Multi-tenant (Public): Users from any Microsoft 365 organization can authenticate
  • Single-tenant (Private): Only users from your specific Azure AD tenant can authenticate
  • Account Types: Work/school accounts, personal Microsoft accounts, or both

This guide focuses on multi-tenant setup as the primary use case, with single-tenant configuration covered in the Advanced section.

Prerequisites

Winter CMS Requirements

  • Winter CMS v1.2+ installed and running
  • Winter.SSO plugin installed
  • Winter.SSOProviderMicrosoft plugin installed
  • Your site accessible via HTTPS (required for production)

Azure Account Setup

Do I Need an Azure Account?

Yes. To create OAuth applications for Microsoft authentication, you need access to the Azure Portal. This is true even if you're only authenticating personal Microsoft accounts.

Azure Active Directory (now also called Microsoft Entra ID) is Microsoft's cloud-based identity and access management service. You'll use it to register your Winter CMS application and generate OAuth credentials.

Types of Access

Option A: Microsoft 365 Organization (Recommended for businesses)

  • If your organization uses Microsoft 365, you likely already have Azure AD
  • Contact your IT administrator for access to Azure Portal
  • You'll need permission to register applications in Azure AD
  • No Azure subscription needed - Azure AD is included with Microsoft 365

Option B: Personal Azure Account (For individual developers)

  • Sign up for a free Azure account
  • Perfect for development, testing, and small projects
  • Includes free Azure AD capabilities
  • No cost for OAuth application registrations

Creating a Free Azure Account

If you don't have Azure access yet:

  1. Visit Azure Free Account Page

  2. Sign In or Create Microsoft Account

    • Use an existing Microsoft account (Outlook, Hotmail, Live, Xbox, etc.)
    • Or create a new one during signup
  3. Complete Registration

    • Identity verification: Verify via phone (SMS or call)
    • Credit card: Required for identity verification
      • Important: You won't be charged for free services
      • Card is only used to prevent abuse
      • No automatic charges when free credit expires
    • Agreement: Review and accept the terms
  4. Wait for Provisioning

    • Usually completes instantly
    • You'll see the Azure Portal dashboard when ready

What's Included (Free):

  • Free for 12 months: Popular services with monthly limits
  • $200 credit: For first 30 days to explore paid services
  • Always free: 25+ services including Azure AD
  • No charge: OAuth application registrations are completely free
  • No auto-upgrade: Must explicitly upgrade to pay-as-you-go

Cost Clarification: Creating Azure AD applications for SSO authentication is free and doesn't consume your credit or trial period.

Verify Your Azure Access

Before proceeding, confirm you can access Azure:

  1. Navigate to Azure Portal
  2. Sign in with your Microsoft account
  3. You should see the Azure dashboard
  4. Search for "Azure Active Directory" or "Microsoft Entra ID" in the top search bar
  5. You should be able to access the Azure AD overview page

What if I see "No subscriptions found"?

  • This is normal for new accounts
  • Azure AD features don't require an active subscription
  • You can still create app registrations

Common Issues:

IssueSolution
"Access denied"Your organization may restrict access. Contact your IT administrator.
"No subscriptions"Normal for new accounts. Azure AD doesn't require a subscription.
"Invalid tenant"You may need to switch directories using the profile menu (top-right corner).
"Sign in error"Ensure you're using the correct Microsoft account. Try incognito mode.

For Microsoft 365 Administrators

If you're setting this up for your organization:

  1. You need at least Application Administrator or Cloud Application Administrator role
  2. Check your permissions: Azure Portal → Azure AD → Roles and administrators
  3. If you don't have access, contact your Global Administrator

Recommended approach: Use a dedicated service account for application registrations rather than personal accounts.

Part 1: Azure AD Application Registration

Step 1: Access Azure Portal

  1. Navigate to Azure Portal
  2. Sign in with your Microsoft 365 or Azure account
  3. From the left menu or search bar, find Azure Active Directory (also called Microsoft Entra ID)
  4. Click on App registrations in the left sidebar

Direct link: https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps

Step 2: Create New Application

  1. Click New registration button at the top
  2. You'll see the "Register an application" form

Step 3: Configure Application Details

Application Name

  • Name: Enter a descriptive name (e.g., "Winter CMS Backend" or "Your Company - Winter CMS")
  • This name will be shown to users during the consent screen

Supported Account Types

Choose based on your requirements:

For Multi-tenant (Public) - RECOMMENDED:

  • Select: "Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)"
  • This allows:
    • Users from any Microsoft 365 organization
    • Personal Microsoft accounts (@outlook.com, @live.com, @hotmail.com)
    • Most flexible option

Alternative: Work/School Only:

  • Select: "Accounts in any organizational directory (Any Azure AD directory - Multitenant)"
  • This allows:
    • Users from any Microsoft 365 organization
    • Excludes personal Microsoft accounts

For Single-tenant (Private):

  • Select: "Accounts in this organizational directory only (Single tenant)"
  • Only users in your Azure AD tenant can authenticate
  • See Advanced section below for configuration

Redirect URI

  1. Platform: Select Web

  2. Redirect URI: Enter your callback URL in this exact format:

    https://example.com/backend/winter/sso/handle/callback/microsoft
    

    Important:

    • Replace example.com with your actual domain
    • Must use https:// for production (http only for local development)
    • Path must be exactly: /backend/winter/sso/handle/callback/microsoft
    • No trailing slash

    Examples:

    • https://cms.mycompany.com/backend/winter/sso/handle/callback/microsoft
    • https://example.com/backend/winter/sso/handle/callback/microsoft
    • http://localhost/backend/winter/sso/handle/callback/microsoft (dev only)

Step 4: Register Application

  1. Click Register button at the bottom
  2. You'll be redirected to your application's overview page

Step 5: Copy Application (Client) ID

  1. On the application overview page, you'll see Application (client) ID
  2. Copy this value - you'll need it for your .env file
  3. It looks like: 12345678-1234-1234-1234-123456789012

Example:

Application (client) ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890

Save this as your MICROSOFT_CLIENT_ID.

Step 6: Copy Directory (Tenant) ID

  1. On the same page, copy the Directory (tenant) ID
  2. This is only needed if you're doing single-tenant setup (see Advanced section)
  3. For multi-tenant, you'll use 'common' instead

Step 7: Create Client Secret

  1. In the left sidebar under "Manage", click Certificates & secrets
  2. Click the Client secrets tab
  3. Click New client secret
  4. Configure:
    • Description: Enter a description (e.g., "Winter CMS Secret")
    • Expires: Choose an expiration period
      • Recommended: 24 months (easier to manage)
      • You'll need to regenerate before expiration
  5. Click Add
  6. IMPORTANT: Copy the Value immediately (not the Secret ID)
    • This value is only shown once!
    • If you miss it, you'll need to create a new secret
  7. Save this as your MICROSOFT_CLIENT_SECRET

The secret looks like:

abc123~def456GHI789jkl.MNO012pqr-STU345

Step 8: Configure API Permissions (Optional)

By default, your app has these permissions:

  • openid - Sign users in
  • profile - Read user profile
  • email - Read user email
  • User.Read - Read user's full profile

These are usually sufficient. If you need additional permissions:

  1. Click API permissions in the left sidebar
  2. Click Add a permission
  3. Choose Microsoft Graph
  4. Select Delegated permissions
  5. Common additions:
    • offline_access - Get refresh tokens (for long-lived sessions)
    • Group.Read.All - Read group memberships (for role-based access)
  6. Click Add permissions

Note: Some permissions require admin consent. If needed, click "Grant admin consent" button.

Part 2: Winter CMS Configuration

Step 1: Configure Environment Variables

Edit your .env file and add:

# Microsoft OAuth Credentials
MICROSOFT_CLIENT_ID=your_application_client_id
MICROSOFT_CLIENT_SECRET=your_client_secret_value

# Tenant Configuration (Multi-tenant)
MICROSOFT_TENANT=common

# Optional: Include tenant information
MICROSOFT_INCLUDE_TENANT_INFO=false

# Optional: Include profile photo
MICROSOFT_INCLUDE_AVATAR=true

# Optional: Additional scopes (comma-separated)
MICROSOFT_SCOPES=offline_access

Replace:

  • your_application_client_id with the Application ID from Step 5
  • your_client_secret_value with the secret value from Step 7

Step 2: Enable Microsoft Provider

Create or edit config/winter/sso/config.php:

<?php

return [
    /**
     * Enabled SSO providers
     */
    'enabled_providers' => [
        'microsoft',
    ],

    /**
     * Allow new user registration via SSO
     * If false, only existing backend users can login via SSO
     */
    'allow_registration' => false, // Change to true if you want auto-registration

    /**
     * Disable native username/password authentication (optional)
     * Forces users to authenticate via SSO only
     */
    'prevent_native_auth' => false,
];

Step 3: Test Authentication

  1. Visit your backend login page: https://example.com/backend/auth/signin
  2. You should see a "Sign in with Microsoft" button
  3. Click the button
  4. You'll be redirected to Microsoft's login page
  5. Sign in with a Microsoft account
  6. Grant consent when prompted (first time only)
  7. You'll be redirected back to Winter CMS
  8. If the user exists in your backend, they'll be logged in
  9. If allow_registration is true, a new user will be created

Success! You're now authenticated via Microsoft 365.

Advanced Configuration

Single-tenant (Private) Setup

To restrict authentication to your specific Azure AD tenant only:

1. Update Azure AD App Registration

When creating the app (Step 3 above), select:

  • "Accounts in this organizational directory only (Single tenant)"

2. Update Environment Variables

Replace common with your tenant ID:

MICROSOFT_TENANT=12345678-1234-1234-1234-123456789012

Get your tenant ID from Azure PortalAzure Active DirectoryOverviewTenant ID.

3. Restart Application

Clear cache and restart:

php artisan cache:clear
php artisan config:clear

Now only users from your Azure AD tenant can authenticate.

Restricting Account Types

Even with multi-tenant, you can restrict account types:

Work/School Accounts Only (no personal accounts):

MICROSOFT_TENANT=organizations

Personal Accounts Only (no work/school):

MICROSOFT_TENANT=consumers

Any Account Type (default):

MICROSOFT_TENANT=common

Requiring User Assignment

By default, any user in allowed tenants can authenticate. To require explicit assignment:

  1. Go to Azure PortalEnterprise applications
  2. Find your application
  3. Click Properties in the left sidebar
  4. Set "User assignment required?" to Yes
  5. Save
  6. Go to Users and groups
  7. Click Add user/group
  8. Select users or groups who should have access
  9. Click Assign

Now only assigned users can authenticate.

Multi-tenant with Tenant Filtering

Allow multiple tenants but filter in code:

// In your plugin or theme's boot() method
Event::listen('winter.sso.microsoft.authenticated', function ($ssoUser) {
    // Get tenant information (requires MICROSOFT_INCLUDE_TENANT_INFO=true)
    $tenantId = $ssoUser->tenant['id'] ?? null;

    $allowedTenants = [
        '12345678-1234-1234-1234-123456789012', // Tenant A
        '87654321-4321-4321-4321-210987654321', // Tenant B
    ];

    if (!in_array($tenantId, $allowedTenants)) {
        throw new \Winter\Storm\Auth\AuthenticationException(
            'Your organization is not authorized to access this system.'
        );
    }
});

Don't forget to enable tenant info:

MICROSOFT_INCLUDE_TENANT_INFO=true

Email Domain Filtering

Restrict to specific email domains:

Event::listen('winter.sso.microsoft.beforeRegister', function ($ssoUser) {
    $email = $ssoUser->getEmail();
    $allowedDomains = ['mycompany.com', 'partner.com'];

    $domain = explode('@', $email)[1] ?? '';

    if (!in_array($domain, $allowedDomains)) {
        throw new \Winter\Storm\Auth\AuthenticationException(
            'Only users with @mycompany.com or @partner.com email addresses are allowed.'
        );
    }
});

Accessing Group Memberships

To use Microsoft 365 groups for role assignment:

1. Add Permission in Azure AD

  1. Azure PortalApp registrations → Your app
  2. API permissionsAdd a permission
  3. Microsoft GraphDelegated permissions
  4. Select: Group.Read.All or Directory.Read.All
  5. Click Add permissions
  6. Click Grant admin consent (admin required)

2. Use Groups in Events

Event::listen('winter.sso.microsoft.afterLogin', function ($user, $ssoUser) {
    // Get group names
    $groups = $ssoUser->getRoles();

    // Assign role based on group membership
    if (in_array('CMS Administrators', $groups)) {
        $user->role = \Backend\Models\UserRole::where('code', 'developer')->first();
        $user->save();
    }
    elseif (in_array('CMS Editors', $groups)) {
        $user->role = \Backend\Models\UserRole::where('code', 'publisher')->first();
        $user->save();
    }
});

Populating User Profile

Automatically populate user details from Microsoft:

Event::listen('winter.sso.microsoft.registered', function ($user, $ssoUser) {
    $user->fill([
        'first_name' => $ssoUser->user['givenName'] ?? null,
        'last_name' => $ssoUser->user['surname'] ?? null,
    ]);
    $user->save();
});

Event::listen('winter.sso.microsoft.afterLogin', function ($user, $ssoUser) {
    // Update on every login
    $user->first_name = $ssoUser->user['givenName'] ?? $user->first_name;
    $user->last_name = $ssoUser->user['surname'] ?? $user->last_name;
    $user->save();
});

Using Refresh Tokens

For long-lived sessions, request refresh tokens:

MICROSOFT_SCOPES=offline_access

The refresh token will be stored in user metadata:

$refreshToken = $user->getSsoValue('microsoft', 'refreshToken');

You can use this to refresh access tokens without requiring re-authentication.

Troubleshooting

"AADSTS50011: The redirect URI specified in the request does not match"

Cause: Redirect URI in your Azure AD app doesn't match the actual callback URL.

Solution:

  1. Go to Azure PortalApp registrations → Your app → Authentication
  2. Under Redirect URIs, ensure you have:
    https://example.com/backend/winter/sso/handle/callback/microsoft
    
  3. Must match exactly:
    • Correct protocol (https vs http)
    • Exact domain
    • No trailing slash
    • Correct path

Add URI if missing:

  1. Click Add URI button
  2. Enter the correct URI
  3. Click Save at the bottom

"AADSTS700016: Application not found in the directory"

Cause: Your app is configured for single-tenant but you're using MICROSOFT_TENANT=common.

Solution:

  • For multi-tenant apps: Use MICROSOFT_TENANT=common or organizations
  • For single-tenant apps: Use your tenant ID: MICROSOFT_TENANT=your-tenant-id
  • Ensure "Supported account types" in Azure AD matches your configuration

"AADSTS50105: The signed in user is not assigned to a role"

Cause: Your app requires user assignment, but the user hasn't been assigned.

Solution (choose one):

Option A: Disable user assignment requirement

  1. Azure PortalEnterprise applications → Your app
  2. Properties → Set "User assignment required?" to No
  3. Save

Option B: Assign the user

  1. Azure PortalEnterprise applications → Your app
  2. Users and groupsAdd user/group
  3. Select users who should have access
  4. Assign

"AADSTS65001: The user or administrator has not consented"

Cause: User hasn't granted consent to the application.

Solution:

  1. Users will see a consent screen on first login
  2. They must click "Accept" to grant permissions
  3. For admin-required permissions, an admin must grant consent:
    • Azure PortalApp registrations → Your app → API permissions
    • Click Grant admin consent for [Your Organization]

"AADSTS90072: User account is disabled"

Cause: The Microsoft account is disabled in Azure AD.

Solution: Administrator must enable the account in Azure AD.

"Invalid state" error

Cause: Session lost between redirect and callback.

Solutions:

  • Ensure cookies are enabled in browser
  • Check Winter CMS session configuration
  • Winter.SSO automatically adjusts session.same_site to 'lax'
  • Try in incognito/private browsing mode
  • Clear browser cookies

"Email not found" error

Cause: User doesn't exist in Winter CMS backend and allow_registration is false.

Solutions:

  1. Create the user manually in backend first, or
  2. Enable registration: 'allow_registration' => true in config

Client Secret Expiration

Symptoms: Authentication suddenly stops working

Cause: Client secret has expired (they're typically valid for 6-24 months)

Solution:

  1. Azure PortalApp registrations → Your app → Certificates & secrets
  2. Check expiration dates
  3. Create new secret before old one expires
  4. Update MICROSOFT_CLIENT_SECRET in .env
  5. Delete expired secrets

Recommendation: Set a calendar reminder before expiration!

Testing in Local Development

For localhost testing:

  1. Use http://localhost (https not required locally)
  2. Add redirect URI in Azure AD:
    http://localhost/backend/winter/sso/handle/callback/microsoft
    
  3. Update .env:
    APP_URL=http://localhost
    

Note: Some enterprise policies may block localhost. Use a tool like ngrok to expose your local site via HTTPS for testing.

Security Best Practices

  1. Use HTTPS in production - Required for OAuth security
  2. Rotate secrets regularly - Create new client secrets before expiration
  3. Limit permissions - Only request scopes you actually need
  4. Monitor sign-in logs - Check Azure AD sign-in logs for suspicious activity
  5. Enable Conditional Access - Use Azure AD Conditional Access policies for additional security
  6. Multi-factor Authentication - Enforce MFA in Azure AD for all users
  7. Review app permissions - Periodically audit granted permissions

Additional Resources

Support

Feedback

Found an issue or have a suggestion? Please open an issue on GitHub.