Azure AD
Sign in with Microsoft Entra ID (Azure AD) — works for single-tenant org accounts, multi-tenant apps, and personal Microsoft accounts.
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();
// CSRF token cookie must be issued before authentication runs
app.UsePrimusCsrfProtection();
app.UseAuthentication(); // REQUIRED — must come before UseAuthorization
app.UseAuthorization(); // REQUIRED — without this, all endpoints are unprotected
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": "http://localhost:5173/dashboard"
}
}
If your frontend runs on a different port or domain from the API, PostLoginRedirect must be an absolute URL pointing to the frontend (e.g. http://localhost:5173/dashboard). Using / redirects to the API server root, not your app. See the Integration Guide for full cross-origin SPA setup.
Configuration keys
| Key | Type | Required | Expected values | Description |
|---|---|---|---|---|
AzureAd:TenantId | string | Yes | Your Directory GUID (e.g. cbd15a9b-...) | Your Entra ID tenant. Never use "common" for single-tenant apps. |
AzureAd:ClientId | string | Yes | Your App registration GUID | The Application (client) ID from Azure Portal. |
AzureAd:ClientSecret | string | Yes | Secret value from Certificates & secrets | Client secret. Set via environment variables in production — never hardcode. |
AzureAd:IsMultiTenant | bool | No | true / false | false = only users from your org. true = users from any Entra ID org. Must match the account type set in Azure Portal. Defaults to false. |
PrimusAuth:Security:TokenEncryptionKey | string | Yes | Any random 32+ character string | Encrypts the session cookie (AES-256). Set via environment variables in production — never hardcode. |
Auth:PostLoginRedirect | string | No | 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.
Select Web. Do not select SPA or Mobile/Desktop.
- Web = confidential client. Azure accepts a
client_secret. The Identity Broker uses this flow. - SPA = public client. Azure refuses any
client_secret, returningAADSTS700025: Client is public so neither 'client_assertion' nor 'client_secret' should be presented.
If you already registered a SPA platform and are seeing AADSTS700025, go to Authentication, delete the SPA entry, add a new Web platform, and add the redirect URI there.
- Add your redirect URI:
- Local dev:
http://localhost:5000/api/auth/azure/callback - Production:
https://your-api.com/api/auth/azure/callback
- Local dev:
- Under Implicit grant and hybrid flows, leave both checkboxes unchecked.
- Click Save.
If the callback URI is missing or wrong, Azure returns
AADSTS500113: No reply address is registered for the applicationafter the consent screen. The URI must exactly match — including scheme (httpvshttps) and port.
2b. Verify the app is a confidential client
Go to Authentication → scroll to Advanced settings. Confirm "Allow public client flows" is set to No.
If it is set to Yes, the broker's client_secret will be rejected with AADSTS700025: Client is public. Set it to No and save.
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.
Secrets via environment variables
Never put ClientSecret or TokenEncryptionKey in appsettings.json or commit them to source control. Use dotnet user-secrets for local development and your CI/CD secret store (GitHub Actions secrets, Azure Key Vault, etc.) for production. ASP.NET Core maps them automatically at runtime — no code changes are required.
How to generate a strong TokenEncryptionKey
Generate a cryptographically random string of 32 or more characters and set it as the PrimusAuth__Security__TokenEncryptionKey environment variable. Use your platform's secret management — dotnet user-secrets for local development, and your CI/CD secret store (GitHub Actions secrets, Azure Key Vault, etc.) for production.
Never share this value or commit it to source control. Rotating it will invalidate all existing sessions.
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/azure/callback | OAuth callback — handled automatically |
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
curl http://localhost:5000/api/auth/providers
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"
}
The
rolefield comes from yourIPrimusAuthUserStore, not from Azure AD. The broker does not call Microsoft Graph API, read Azure AD groups, or perform any directory queries. In development,InMemoryPrimusAuthUserStoreauto-provisions new SSO users withRole = "User". In production, set the role in your ownAutoProvisionUserAsyncimplementation.
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. Without one → 401 Unauthorized.
5. Sign out
Call POST /api/auth/logout with the session cookie and the CSRF token to end the session:
curl -b cookies.txt \
-X POST \
-H "X-Primus-CSRF: <value-from-XSRF-TOKEN-cookie>" \
http://localhost:5000/api/auth/logout
Returns 200 on success. After this, GET /api/auth/me returns 401 Unauthorized.
SAML 2.0 SSO
Azure AD (Entra ID) supports both OIDC and SAML 2.0. SAML is commonly required in enterprise environments that need a SAML-based federation.
Do you need one Azure app or two?
For Azure SAML, the object you configure in Azure is usually an Enterprise Application.
| Scenario | Azure object you configure | Required for this broker flow? | Notes |
|---|---|---|---|
Azure OIDC login (/api/auth/azure) | App registration | Yes, for OIDC | This is where you get ClientId, ClientSecret, and the OIDC redirect URI. |
Azure SAML login (/api/auth/saml/{scheme}/SignIn) | Enterprise application | Yes, for SAML | This is where you configure SAML Identifier, Reply URL, Sign-on URL, claims, and SAML certificates. |
| App supports both Azure OIDC and Azure SAML | Both | Usually yes | In practice this means one app registration for OIDC and one enterprise application for SAML. |
If you are implementing SAML only, you typically do not need Azure OIDC credentials in AzureAd:ClientId / ClientSecret. You only need the SAML configuration under PrimusAuth:Saml.
Register the SAML enterprise application
- Go to Azure Portal → Entra ID → Enterprise Applications → New application.
- Choose Create your own application → select Integrate any other application you don't find in the gallery.
- In the app, go to Single sign-on → SAML.
- Set Identifier (Entity ID) to your application's base URL, e.g.
https://app.example.com. - Set Reply URL (ACS URL) to:
https://app.example.com/api/auth/saml/azure-saml/Acs - Copy your Tenant ID from the app's Overview page.
Configure appsettings.json
For Azure SAML, start with the smallest working config first:
{
"PrimusAuth": {
"Saml": {
"EntityId": "https://app.example.com",
"IdentityProviders": [
{
"Scheme": "azure-saml",
"DisplayName": "Company Azure SSO",
"IdpType": "AzureAd",
"AzureTenantId": "YOUR_TENANT_ID"
}
]
}
}
}
The broker code does auto-resolve a generic tenant metadata URL from AzureTenantId:
https://login.microsoftonline.com/{tenantId}/federationmetadata/2007-06/federationmetadata.xml
That means MetadataUrl is not always required in appsettings.json.
However, for Azure Enterprise Application SAML, you may still want to set MetadataUrl explicitly when:
- Azure signs assertions with metadata that does not match the generic tenant metadata you auto-derived.
- You want the app to trust the exact App Federation Metadata Url shown in Azure under Single sign-on → SAML → SAML Certificates.
- You are troubleshooting signature or certificate trust issues locally.
Use explicit MetadataUrl like this only when you need it:
{
"PrimusAuth": {
"Saml": {
"EntityId": "https://app.example.com",
"IdentityProviders": [
{
"Scheme": "azure-saml",
"DisplayName": "Company Azure SSO",
"IdpType": "AzureAd",
"AzureTenantId": "YOUR_TENANT_ID",
"MetadataUrl": "https://login.microsoftonline.com/YOUR_TENANT_ID/federationmetadata/2007-06/federationmetadata.xml?appid=YOUR_APPLICATION_ID"
}
]
}
}
}
What do I actually need for Azure SAML?
| App setting | Required | Where to get it | Notes |
|---|---|---|---|
PrimusAuth:Saml:EntityId | Yes | You choose this | Usually your API base URL, such as https://localhost:5000 in local development. |
Scheme | Yes | You choose this | Becomes part of the login and ACS URLs, for example azure-saml. |
DisplayName | No | You choose this | Only affects provider display text. |
IdpType | Yes for Azure shortcut setup | You choose this | Use AzureAd for Azure SAML. |
AzureTenantId | Yes for Azure shortcut setup | Azure tenant Overview page | Use the Directory (tenant) ID. Do not use the app registration client ID or the enterprise app object ID. |
MetadataUrl | Optional | Azure SAML Certificates section | Use the App Federation Metadata Url when the generic tenant metadata is not enough or you want to pin to the exact enterprise app metadata. |
Auth:PostLoginRedirect | Recommended | You choose this | For cross-origin frontend apps, use the frontend URL. For same-origin apps, / is fine. |
SAML endpoints
| Endpoint | Description |
|---|---|
GET /api/auth/saml/azure-saml | Returns SP metadata XML — use this URL as the Entity ID in the Azure portal |
GET /api/auth/saml/azure-saml/SignIn | Initiates SP-initiated SSO — redirects the browser to Azure AD |
POST /api/auth/saml/azure-saml/Acs | Assertion Consumer Service — handled automatically |
Local development checklist (recommended)
Use this exact sequence for a smooth localhost integration.
- Use a recent package version (SAML support starts in
2.2.0; use latest stable). - Use one base URL everywhere (recommended:
https://localhost:5000). - Configure
appsettings.jsonwith a SAML IdP entry. Start withAzureTenantIdonly, then addMetadataUrlonly if Azure certificate trust does not line up:
{
"PrimusAuth": {
"Saml": {
"EntityId": "https://localhost:5000",
"IdentityProviders": [
{
"Scheme": "azure-saml",
"DisplayName": "Company Azure SSO",
"IdpType": "AzureAd",
"AzureTenantId": "YOUR_TENANT_ID"
}
]
}
},
"Auth": {
"PostLoginRedirect": "/"
}
}
- In Azure Enterprise Application → Single sign-on (SAML) set:
- Identifier (Entity ID):
https://localhost:5000 - Reply URL (ACS):
https://localhost:5000/api/auth/saml/azure-saml/Acs - Sign on URL:
https://localhost:5000/api/auth/saml/azure-saml/SignIn - Relay State:
/
- Identifier (Entity ID):
- If login fails with signature or trust problems, copy App Federation Metadata Url from Azure and add it as
MetadataUrlin your SAML IdP entry. - Restart the app and clear localhost cookies.
- Confirm provider registration:
curl https://localhost:5000/api/auth/providers
Expected to include saml:azure-saml and its loginUrl.
- Start only from SP-initiated endpoint:
https://localhost:5000/api/auth/saml/azure-saml/SignIn?ReturnUrl=%2F
- Verify authenticated session:
https://localhost:5000/api/auth/me
Do not browse directly to
/Acs. It is callback-only and must receive Azure's SAML form post.
Advanced features
The following features work with Azure AD exactly the same way as any other provider:
- Multi-Factor Authentication — TOTP, SMS, and email OTP
- Domain Allowlist — restrict logins to specific email domains
- Bot Protection & Rate Limiting — IP-based rate limiting, CAPTCHA hooks
- Webhooks — receive events on login, logout, and provisioning
Next Steps
- Integration Guide — complete setup walkthrough including frontend integration
- Provider Setup — where to get each Azure, Auth0, Google, Okta, and SAML credential
- MFA Deep Dive — TOTP enrollment UX, SMS codes, and recovery
- Edge Cases — troubleshooting for redirects, MFA enforcement, and SSO callback errors
- Advanced Configuration — custom stores, HA clustering, and data protection
- Database Setup — persist provisioned users with EF Core