Azure AD
Configure Enterprise Single Sign-On (SSO) with Microsoft Entra ID.
Step 1: Install the package
dotnet add package PrimusSaaS.Identity.Broker
Step 2: Configure Program.cs
using PrimusSaaS.Identity.Broker;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment());
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication(); // REQUIRED — must come before UseAuthorization
app.UseAuthorization(); // REQUIRED — without this, all endpoints are unprotected
app.UsePrimusCsrfProtection();
app.MapControllers();
app.MapPrimusAuthBroker();
app.Run();
UseAuthentication() and UseAuthorization() are required in your middleware pipeline. Without them, protected endpoints such as /api/auth/me will return data to unauthenticated callers regardless of the .RequireAuthorization() annotation.
Step 3: Configure appsettings.json
{
"AzureAd": {
"TenantId": "YOUR_TENANT_ID",
"ClientId": "YOUR_CLIENT_ID",
"ClientSecret": "YOUR_CLIENT_SECRET",
"IsMultiTenant": false
},
"PrimusAuth": {
"Security": {
"TokenEncryptionKey": "change-this-to-a-random-32-char-secret!!"
}
},
"Auth": {
"PostLoginRedirect": "/"
}
}
Configuration keys
| Key | Type | Expected Values | Description |
|---|---|---|---|
AzureAd:TenantId | string | Your Directory GUID (e.g. cbd15a9b-...) | Your Entra ID tenant. Never use "common" for single-tenant apps. |
AzureAd:ClientId | string | Your App registration GUID | The Application (client) ID from Azure Portal. |
AzureAd:ClientSecret | string | Secret value from Certificates & secrets | Client secret. Use user-secrets or Key Vault in production — never hardcode. |
AzureAd:IsMultiTenant | bool | true / false | false = only users from your org. true = users from any Entra ID org. Must match the account type set in Azure Portal — see below. |
PrimusAuth:Security:TokenEncryptionKey | string | Any random 32+ character string | Encrypts the session cookie (AES-256). Required. Use Azure Key Vault or dotnet user-secrets in production. |
Auth:PostLoginRedirect | string | Any route path, e.g. /, /dashboard | Where to redirect the user after successful login. Defaults to /. |
How to get these values from Azure Portal
1. Register the application
- Go to Azure Portal → Microsoft Entra ID → App registrations → New registration.
- Give it a name (e.g.
MyApp). - Under Supported account types, select based on your
IsMultiTenantsetting:IsMultiTenant: false→ Accounts in this organizational directory only (Single tenant)IsMultiTenant: true→ Accounts in any organizational directory (Multitenant)- To allow personal Microsoft accounts (outlook.com, live.com) → Accounts in any organizational directory + personal Microsoft accounts
- Click Register.
2. Add the redirect URI
- Go to Authentication → Add a platform → Web.
- Add your redirect URI:
- Local dev:
http://localhost:5000/api/auth/azure/callback - Production:
https://your-api.com/api/auth/azure/callback
- Local dev:
- Click Save.
If this URI is missing or wrong, Azure returns
AADSTS500113: No reply address is registeredafter the consent screen.
3. Copy your credentials
From the Overview page:
- Application (client) ID →
AzureAd:ClientId - Directory (tenant) ID →
AzureAd:TenantId
Go to Certificates & secrets → New client secret → copy the Value immediately (shown only once) → AzureAd:ClientSecret.
4. Personal Microsoft accounts (outlook.com, live.com) only
If you selected "Any organizational directory + personal Microsoft accounts", you must also update the app manifest:
- Go to Manifest in the left nav.
- Find
"requestedAccessTokenVersion": nulland change it to"requestedAccessTokenVersion": 2. - Click Save.
Without this change, Azure returns: Property api.requestedAccessTokenVersion is invalid.
Step 4: Available endpoints
Endpoints are registered by app.MapPrimusAuthBroker().
| Endpoint | Description |
|---|---|
GET /api/auth/providers | Returns the list of configured providers |
GET /api/auth/azure | Initiates the Azure AD login flow |
GET /api/auth/me | Returns the current authenticated user (401 if not logged in) |
POST /api/auth/logout | Clears the session cookie |
Step 5: Test the flow
Run the app:
dotnet run --urls "http://localhost:5000"
1. Check available providers
# Linux/macOS
curl http://localhost:5000/api/auth/providers
# Windows PowerShell
Invoke-WebRequest http://localhost:5000/api/auth/providers | Select-Object -ExpandProperty Content
Expected response:
{"providers":["azure"]}
2. Sign in via browser
Open this URL in your browser — it will redirect to Microsoft login:
http://localhost:5000/api/auth/azure
Sign in with your Microsoft/Azure AD account. After login, Azure redirects back to your app and the Broker sets a session cookie automatically.
3. Confirm the session
In the same browser, open:
http://localhost:5000/api/auth/me
Expected response:
{
"email": "you@yourdomain.com",
"role": "User",
"provider": "azure"
}
4. Test a protected endpoint
Add [Authorize] to any controller action to restrict it to authenticated users only:
[HttpGet("profile")]
[Authorize]
public IActionResult Profile()
{
return Ok(new { email = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value });
}
With a valid session cookie → 200 OK with user data.
Without a session cookie → 401 Unauthorized.
5. Sign out
Run this in the browser DevTools console (F12) while on the app page:
const csrf = document.cookie.split('; ').find(c => c.startsWith('XSRF-TOKEN=')).split('=')[1];
fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
headers: { 'X-Primus-CSRF': csrf }
}).then(r => console.log('Logout:', r.status)); // Expected: 200
After logout, /api/auth/me returns 401 Unauthorized.