Skip to main content
Version: 2026.04.0

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();
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": "http://localhost:5173/dashboard"
}
}
PostLoginRedirect for cross-origin SPAs

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

KeyTypeRequiredExpected valuesDescription
AzureAd:TenantIdstringYesYour Directory GUID (e.g. cbd15a9b-...)Your Entra ID tenant. Never use "common" for single-tenant apps.
AzureAd:ClientIdstringYesYour App registration GUIDThe Application (client) ID from Azure Portal.
AzureAd:ClientSecretstringYesSecret value from Certificates & secretsClient secret. Set via environment variables in production — never hardcode.
AzureAd:IsMultiTenantboolNotrue / falsefalse = 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:TokenEncryptionKeystringYesAny random 32+ character stringEncrypts the session cookie (AES-256). Set via environment variables in production — never hardcode.
Auth:PostLoginRedirectstringNoAny 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 platform.
Choose "Web" — not SPA, not Mobile

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, returning AADSTS700025: 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.

  1. Add your redirect URI:
    • Local dev: http://localhost:5000/api/auth/azure/callback
    • Production: https://your-api.com/api/auth/azure/callback
  2. Under Implicit grant and hybrid flows, leave both checkboxes unchecked.
  3. Click Save.

If the callback URI is missing or wrong, Azure returns AADSTS500113: No reply address is registered for the application after the consent screen. The URI must exactly match — including scheme (http vs https) 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) 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.

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().

EndpointDescription
GET /api/auth/providersReturns the list of configured providers
GET /api/auth/azureInitiates the Azure AD login flow
GET /api/auth/azure/callbackOAuth callback — handled automatically
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

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 role field comes from your IPrimusAuthUserStore, not from Azure AD. The broker does not call Microsoft Graph API, read Azure AD groups, or perform any directory queries. In development, InMemoryPrimusAuthUserStore auto-provisions new SSO users with Role = "User". In production, set the role in your own AutoProvisionUserAsync implementation.

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.

ScenarioAzure object you configureRequired for this broker flow?Notes
Azure OIDC login (/api/auth/azure)App registrationYes, for OIDCThis is where you get ClientId, ClientSecret, and the OIDC redirect URI.
Azure SAML login (/api/auth/saml/{scheme}/SignIn)Enterprise applicationYes, for SAMLThis is where you configure SAML Identifier, Reply URL, Sign-on URL, claims, and SAML certificates.
App supports both Azure OIDC and Azure SAMLBothUsually yesIn 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

  1. Go to Azure Portal → Entra ID → Enterprise Applications → New application.
  2. Choose Create your own application → select Integrate any other application you don't find in the gallery.
  3. In the app, go to Single sign-on → SAML.
  4. Set Identifier (Entity ID) to your application's base URL, e.g. https://app.example.com.
  5. Set Reply URL (ACS URL) to:
    https://app.example.com/api/auth/saml/azure-saml/Acs
  6. 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 settingRequiredWhere to get itNotes
PrimusAuth:Saml:EntityIdYesYou choose thisUsually your API base URL, such as https://localhost:5000 in local development.
SchemeYesYou choose thisBecomes part of the login and ACS URLs, for example azure-saml.
DisplayNameNoYou choose thisOnly affects provider display text.
IdpTypeYes for Azure shortcut setupYou choose thisUse AzureAd for Azure SAML.
AzureTenantIdYes for Azure shortcut setupAzure tenant Overview pageUse the Directory (tenant) ID. Do not use the app registration client ID or the enterprise app object ID.
MetadataUrlOptionalAzure SAML Certificates sectionUse 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:PostLoginRedirectRecommendedYou choose thisFor cross-origin frontend apps, use the frontend URL. For same-origin apps, / is fine.

SAML endpoints

EndpointDescription
GET /api/auth/saml/azure-samlReturns SP metadata XML — use this URL as the Entity ID in the Azure portal
GET /api/auth/saml/azure-saml/SignInInitiates SP-initiated SSO — redirects the browser to Azure AD
POST /api/auth/saml/azure-saml/AcsAssertion Consumer Service — handled automatically

Use this exact sequence for a smooth localhost integration.

  1. Use a recent package version (SAML support starts in 2.2.0; use latest stable).
  2. Use one base URL everywhere (recommended: https://localhost:5000).
  3. Configure appsettings.json with a SAML IdP entry. Start with AzureTenantId only, then add MetadataUrl only 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": "/"
}
}
  1. 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: /
  2. If login fails with signature or trust problems, copy App Federation Metadata Url from Azure and add it as MetadataUrl in your SAML IdP entry.
  3. Restart the app and clear localhost cookies.
  4. Confirm provider registration:
curl https://localhost:5000/api/auth/providers

Expected to include saml:azure-saml and its loginUrl.

  1. Start only from SP-initiated endpoint:
https://localhost:5000/api/auth/saml/azure-saml/SignIn?ReturnUrl=%2F
  1. 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:


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