Skip to main content

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();
warning

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

KeyTypeExpected ValuesDescription
AzureAd:TenantIdstringYour Directory GUID (e.g. cbd15a9b-...)Your Entra ID tenant. Never use "common" for single-tenant apps.
AzureAd:ClientIdstringYour App registration GUIDThe Application (client) ID from Azure Portal.
AzureAd:ClientSecretstringSecret value from Certificates & secretsClient secret. Use user-secrets or Key Vault in production — never hardcode.
AzureAd:IsMultiTenantbooltrue / falsefalse = 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:TokenEncryptionKeystringAny random 32+ character stringEncrypts the session cookie (AES-256). Required. Use Azure Key Vault or dotnet user-secrets in production.
Auth:PostLoginRedirectstringAny route path, e.g. /, /dashboardWhere to redirect the user after successful login. Defaults to /.
How to get these values from Azure Portal

1. Register the application

  1. Go to Azure PortalMicrosoft Entra IDApp registrationsNew registration.
  2. Give it a name (e.g. MyApp).
  3. Under Supported account types, select based on your IsMultiTenant setting:
    • IsMultiTenant: falseAccounts in this organizational directory only (Single tenant)
    • IsMultiTenant: trueAccounts in any organizational directory (Multitenant)
    • To allow personal Microsoft accounts (outlook.com, live.com) → Accounts in any organizational directory + personal Microsoft accounts
  4. Click Register.

2. Add the redirect URI

  1. Go to AuthenticationAdd a platformWeb.
  2. Add your redirect URI:
    • Local dev: http://localhost:5000/api/auth/azure/callback
    • Production: https://your-api.com/api/auth/azure/callback
  3. Click Save.

If this URI is missing or wrong, Azure returns AADSTS500113: No reply address is registered after the consent screen.

3. Copy your credentials

From the Overview page:

  • Application (client) IDAzureAd:ClientId
  • Directory (tenant) IDAzureAd:TenantId

Go to Certificates & secretsNew 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:

  1. Go to Manifest in the left nav.
  2. Find "requestedAccessTokenVersion": null and change it to "requestedAccessTokenVersion": 2.
  3. Click Save.

Without this change, Azure returns: Property api.requestedAccessTokenVersion is invalid.

Step 4: Available endpoints

Endpoints are registered by app.MapPrimusAuthBroker().

EndpointDescription
GET /api/auth/providersReturns the list of configured providers
GET /api/auth/azureInitiates the Azure AD login flow
GET /api/auth/meReturns the current authenticated user (401 if not logged in)
POST /api/auth/logoutClears 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.