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:
-
Visit Azure Free Account Page
- Go to https://azure.microsoft.com/free/
- Click "Start free" button
-
Sign In or Create Microsoft Account
- Use an existing Microsoft account (Outlook, Hotmail, Live, Xbox, etc.)
- Or create a new one during signup
-
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
-
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:
- Navigate to Azure Portal
- Sign in with your Microsoft account
- You should see the Azure dashboard
- Search for "Azure Active Directory" or "Microsoft Entra ID" in the top search bar
- 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:
| Issue | Solution |
|---|---|
| "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:
- You need at least Application Administrator or Cloud Application Administrator role
- Check your permissions: Azure Portal → Azure AD → Roles and administrators
- 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
- Navigate to Azure Portal
- Sign in with your Microsoft 365 or Azure account
- From the left menu or search bar, find Azure Active Directory (also called Microsoft Entra ID)
- 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
- Click New registration button at the top
- 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
-
Platform: Select Web
-
Redirect URI: Enter your callback URL in this exact format:
https://example.com/backend/winter/sso/handle/callback/microsoftImportant:
- Replace
example.comwith 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/microsofthttps://example.com/backend/winter/sso/handle/callback/microsofthttp://localhost/backend/winter/sso/handle/callback/microsoft(dev only)
- Replace
Step 4: Register Application
- Click Register button at the bottom
- You'll be redirected to your application's overview page
Step 5: Copy Application (Client) ID
- On the application overview page, you'll see Application (client) ID
- Copy this value - you'll need it for your
.envfile - 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
- On the same page, copy the Directory (tenant) ID
- This is only needed if you're doing single-tenant setup (see Advanced section)
- For multi-tenant, you'll use
'common'instead
Step 7: Create Client Secret
- In the left sidebar under "Manage", click Certificates & secrets
- Click the Client secrets tab
- Click New client secret
- 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
- Click Add
- 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
- 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 inprofile- Read user profileemail- Read user emailUser.Read- Read user's full profile
These are usually sufficient. If you need additional permissions:
- Click API permissions in the left sidebar
- Click Add a permission
- Choose Microsoft Graph
- Select Delegated permissions
- Common additions:
offline_access- Get refresh tokens (for long-lived sessions)Group.Read.All- Read group memberships (for role-based access)
- 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_idwith the Application ID from Step 5your_client_secret_valuewith 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
- Visit your backend login page:
https://example.com/backend/auth/signin - You should see a "Sign in with Microsoft" button
- Click the button
- You'll be redirected to Microsoft's login page
- Sign in with a Microsoft account
- Grant consent when prompted (first time only)
- You'll be redirected back to Winter CMS
- If the user exists in your backend, they'll be logged in
- If
allow_registrationis 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 Portal → Azure Active Directory → Overview → Tenant 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:
- Go to Azure Portal → Enterprise applications
- Find your application
- Click Properties in the left sidebar
- Set "User assignment required?" to Yes
- Save
- Go to Users and groups
- Click Add user/group
- Select users or groups who should have access
- 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
- Azure Portal → App registrations → Your app
- API permissions → Add a permission
- Microsoft Graph → Delegated permissions
- Select:
Group.Read.AllorDirectory.Read.All - Click Add permissions
- 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:
- Go to Azure Portal → App registrations → Your app → Authentication
- Under Redirect URIs, ensure you have:
https://example.com/backend/winter/sso/handle/callback/microsoft - Must match exactly:
- Correct protocol (https vs http)
- Exact domain
- No trailing slash
- Correct path
Add URI if missing:
- Click Add URI button
- Enter the correct URI
- 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=commonororganizations - 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
- Azure Portal → Enterprise applications → Your app
- Properties → Set "User assignment required?" to No
- Save
Option B: Assign the user
- Azure Portal → Enterprise applications → Your app
- Users and groups → Add user/group
- Select users who should have access
- Assign
"AADSTS65001: The user or administrator has not consented"
Cause: User hasn't granted consent to the application.
Solution:
- Users will see a consent screen on first login
- They must click "Accept" to grant permissions
- For admin-required permissions, an admin must grant consent:
- Azure Portal → App 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_siteto '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:
- Create the user manually in backend first, or
- Enable registration:
'allow_registration' => truein config
Client Secret Expiration
Symptoms: Authentication suddenly stops working
Cause: Client secret has expired (they're typically valid for 6-24 months)
Solution:
- Azure Portal → App registrations → Your app → Certificates & secrets
- Check expiration dates
- Create new secret before old one expires
- Update
MICROSOFT_CLIENT_SECRETin.env - Delete expired secrets
Recommendation: Set a calendar reminder before expiration!
Testing in Local Development
For localhost testing:
- Use
http://localhost(https not required locally) - Add redirect URI in Azure AD:
http://localhost/backend/winter/sso/handle/callback/microsoft - 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
- Use HTTPS in production - Required for OAuth security
- Rotate secrets regularly - Create new client secrets before expiration
- Limit permissions - Only request scopes you actually need
- Monitor sign-in logs - Check Azure AD sign-in logs for suspicious activity
- Enable Conditional Access - Use Azure AD Conditional Access policies for additional security
- Multi-factor Authentication - Enforce MFA in Azure AD for all users
- Review app permissions - Periodically audit granted permissions
Additional Resources
- Microsoft Identity Platform Documentation
- Azure AD OAuth 2.0 Flow
- Microsoft Graph API
- Conditional Access Policies
- Winter.SSO Documentation
- SocialiteProviders Microsoft
Support
- Winter CMS Discord - #plugins channel
- Winter CMS Forum
- GitHub Issues
Feedback
Found an issue or have a suggestion? Please open an issue on GitHub.