Identity Broker Quickstart
Add secure login with cookie sessions, MFA, and optional SSO to any ASP.NET Core app.
Estimated time: ~15 minutes for local auth. Add 10 minutes if configuring an SSO provider.
If you are building a SaaS app with tenant subdomains and separate admin login on the platform domain, use the tenant-routed composition described in Tenant-Routed Login. The quickstart below stays focused on a single-app broker setup.
1. Install the package
dotnet add package PrimusSaaS.Identity.Broker
2. Configure middleware
Register the broker and your two required implementation classes in Program.cs.
using PrimusSaaS.Identity.Broker;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment());
builder.Services.AddScoped<IPrimusAuthCredentialValidator, MyCredentialValidator>();
builder.Services.AddScoped<IPrimusAuthUserStore, MyUserStore>();
In development mode (IsDevelopment() = true) the broker registers an in-memory user store automatically so local sign-in works without any setup. In production that fallback is removed — your IPrimusAuthUserStore registration is required or all logins will be blocked.
For implementation details of these two classes, see:
- IPrimusAuthCredentialValidator — validate email/password; return a
PrimusAuthUseron success ornullto reject - IPrimusAuthUserStore — look up users by email; handle JIT provisioning on first SSO login
- EF Core user store — use the built-in
EfCorePrimusUserStoreinstead of implementing manually
3. Configure appsettings.json
The minimal configuration needed to start the broker:
{
"AuthBroker": {
"BasePath": "/api/auth",
"CookieName": "MyApp.Session"
},
"Auth": {
"PostLoginRedirect": "http://localhost:5173/dashboard",
"ErrorRedirect": "http://localhost:5173/login?error=auth_failed"
}
}
PostLoginRedirect is where the browser is sent after the OAuth/SAML callback completes. The default is / — which redirects to the API server root, not your frontend app.
- Same-origin (frontend served by the same ASP.NET app):
/dashboardworks fine. - Cross-origin SPA (React/Angular on a separate port or domain): set an absolute URL — e.g.
http://localhost:5173/dashboardlocally,https://app.example.com/dashboardin production.
{
"Auth": {
"PostLoginRedirect": "/",
"ErrorRedirect": "/login"
}
}
Full configuration reference
| Key | Default | Description |
|---|---|---|
AuthBroker:BasePath | /api/auth | URL prefix for all auth endpoints |
AuthBroker:CookieName | Primus.Session | Name of the session cookie |
AuthBroker:IssueLocalJwt | false | Also issue a signed JWT alongside the session cookie |
AuthBroker:ActivityAnalyticsEnabled | true | Record login events for usage analytics |
Auth:PostLoginRedirect | / | Where the browser is redirected after successful login. Must be an absolute URL for cross-origin SPA setups (e.g. https://app.example.com/dashboard) |
Auth:ErrorRedirect | — | Where the browser is redirected on login failure. Appends ?error=<reason> automatically. Should also be an absolute URL for SPA setups |
PrimusAuth:Security:CookieSameSiteMode | Strict | Cookie SameSite policy. Use Lax for cross-origin SPA setups. Use None only in third-party iframe contexts (requires HTTPS) |
Jwt:Key | — | Signing key — required only when IssueLocalJwt: true (minimum 32 characters) |
Jwt:Issuer | — | JWT iss claim — required only when IssueLocalJwt: true |
Jwt:Audience | — | JWT aud claim — required only when IssueLocalJwt: true |
AzureAd:TenantId | — | Azure AD Directory (tenant) ID |
AzureAd:ClientId | — | Azure AD Application (client) ID |
AzureAd:ClientSecret | — | Azure AD client secret — use dotnet user-secrets |
Auth0:Domain | — | Auth0 tenant domain (e.g. your-tenant.auth0.com) |
Auth0:ClientId | — | Auth0 application client ID |
Auth0:ClientSecret | — | Auth0 client secret — use dotnet user-secrets |
Google:ClientId | — | Google OAuth2 client ID |
Google:ClientSecret | — | Google OAuth2 client secret — use dotnet user-secrets |
Okta:Domain | — | Okta tenant domain |
Okta:ClientId | — | Okta application client ID |
Okta:ClientSecret | — | Okta client secret — use dotnet user-secrets |
PrimusAuth:Mfa:Enabled | false | Enable TOTP MFA |
PrimusAuth:Mfa:RequireAllUsers | false | Block logins for users without MFA enrolled |
PrimusAuth:BotProtection:Enabled | false | Enable rate limiting on auth endpoints |
PrimusAuth:BotProtection:MaxAttemptsPerMinute | 5 | Max login attempts per IP per 60-second window before HTTP 429 |
PrimusAuth:BotProtection:MaxAttemptsPerHour | 30 | Max login attempts per IP per 60-minute window before HTTP 429 |
PrimusAuth:DomainAllowlist:Enabled | false | Restrict logins to specific email domains |
PrimusAuth:DomainAllowlist:AllowedDomains | — | Array of permitted domains e.g. ["yourcompany.com"] |
PrimusAuth:Webhooks:Enabled | false | Enable outbound webhook events |
PrimusAuth:Webhooks:Endpoint | — | URL to POST signed webhook payloads to |
PrimusAuth:Webhooks:SigningSecret | — | HMAC signing secret — use dotnet user-secrets |
PrimusAuth:Saml:EntityId | — | Your app's Entity ID (required when any SAML IdP is configured) |
PrimusAuth:Saml:IdentityProviders | — | Array of SAML IdP configurations (Scheme, MetadataUrl) |
All keys marked "use dotnet user-secrets" must be stored outside the repo:
dotnet user-secrets set "AzureAd:ClientSecret" "your-value"
dotnet user-secrets set "PrimusAuth:Webhooks:SigningSecret" "your-value"
Or inject them as environment variables in CI/production.
Each identity provider has a dedicated setup guide with its own configuration keys, portal steps, and callback URLs. See:
Enabling MFA (TOTP)
{
"PrimusAuth": {
"Mfa": {
"Enabled": true,
"RequireAllUsers": false
}
}
}
Login flow once MFA is enabled:
POST /api/auth/mfa/enroll→ returnssecretandotpauthUri(render as QR code)POST /api/auth/mfa/enroll/confirmwith a 6-digit TOTP code to activate- Future logins:
POST /api/auth/loginreturns HTTP 202 withmfaPendingToken POST /api/auth/mfa/verifywith{ code, mfaPendingToken }to complete login
Set RequireAllUsers: true to block users without MFA from completing login.
Enabling domain allowlist
{
"PrimusAuth": {
"DomainAllowlist": {
"Enabled": true,
"AllowedDomains": ["yourcompany.com", "partner.com"]
}
}
}
Anyone with an email outside the listed domains is blocked before a session cookie is issued. Set Enabled: false (the default) to allow all domains.
Enabling rate limiting (bot protection)
{
"PrimusAuth": {
"BotProtection": {
"Enabled": true,
"MaxAttemptsPerMinute": 5,
"MaxAttemptsPerHour": 30
}
}
}
Rate limiting runs in-memory per process by default. For multi-instance or Kubernetes deployments, replace the default limiter with a Redis-backed IPrimusAuthRateLimiter — see the Advanced Configuration page.
Enabling webhook events
{
"PrimusAuth": {
"Webhooks": {
"Enabled": true,
"Endpoint": "https://your-service.com/hooks/auth",
"SigningSecret": "use-dotnet-user-secrets"
}
}
}
dotnet user-secrets set "PrimusAuth:Webhooks:SigningSecret" "your-32-char-secret"
The SDK POSTs a signed JSON payload on user.login, login.failed, user.logout, and mfa.enrolled. Verify the request using the X-Primus-Signature header and your signing secret.
4. Configure endpoints
Add the auth middleware and register the broker routes in the request pipeline:
var app = builder.Build();
// CSRF protection — must come before UseAuthentication so the XSRF-TOKEN
// cookie is issued on all requests, including the initial GET /api/auth/providers
app.UsePrimusCsrfProtection();
// Order matters — Authentication must come before Authorization
app.UseAuthentication();
app.UseAuthorization();
// Registers all /api/auth/* endpoints
app.MapPrimusAuthBroker();
await app.RunAsync();
4a. Cross-origin SPA setup (React / Angular / Vue on a separate port or domain)
If your frontend runs on a different origin from your API (e.g. API on localhost:5000, frontend on localhost:5173), two additional steps are required.
Step 1 — Add CORS in Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("FrontendPolicy", policy =>
{
policy
.WithOrigins(
"http://localhost:5173", // local dev
"https://app.example.com" // production — replace with real URL
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // required — cookies must travel cross-origin
});
});
// ...
var app = builder.Build();
app.UseCors("FrontendPolicy"); // must be before UsePrimusCsrfProtection
app.UsePrimusCsrfProtection();
app.UseAuthentication();
app.UseAuthorization();
app.MapPrimusAuthBroker();
Step 2 — Relax cookie SameSite policy
The CSRF and session cookies default to SameSite=Strict, which blocks all cross-origin cookie delivery. Change this to Lax for cross-origin setups:
{
"PrimusAuth": {
"Security": {
"CookieSameSiteMode": "Lax"
}
}
}
| Value | When to use |
|---|---|
Strict | Frontend and API served from the same origin (same domain + port) |
Lax | Frontend and API on different ports or subdomains — use for all SPA setups |
None | Required only for third-party iframe or widget contexts (must also use HTTPS) |
Step 3 — Send credentials on every fetch / axios request
The session cookie only reaches the API if the browser is told to include credentials:
// fetch
fetch("http://localhost:5000/api/auth/me", { credentials: "include" });
// axios (set once globally)
axios.defaults.withCredentials = true;
credentials: "include" / withCredentials: trueThe browser sends no cookies with cross-origin requests by default. Every call to /api/auth/me will return 401 even after a successful login.
MapPrimusAuthBroker() automatically sets an XSRF-TOKEN cookie on the first request to any /api/auth/* endpoint. Any client making mutating requests (POST, PUT, PATCH, DELETE) must read that cookie and send its value back as the X-Primus-CSRF request header. GET requests (e.g. SSO redirects, /api/auth/me) do not require this header.
If you change AuthBroker:BasePath, update your client's base URL to match.
5. Test the endpoints
Start your app and run these commands to confirm everything is wired correctly:
# Health check
curl http://localhost:5000/health
# Expected: { "status": "ok" }
# List available providers (also seeds the XSRF-TOKEN cookie)
curl -c cookies.txt http://localhost:5000/api/auth/providers
# Expected: JSON array of enabled provider names
# Login with email/password
CSRF=$(grep XSRF-TOKEN cookies.txt | awk '{print $7}')
curl -b cookies.txt -c cookies.txt \
-H "Content-Type: application/json" \
-H "X-Primus-CSRF: $CSRF" \
-d '{"email":"you@example.com","password":"YourPass1!"}' \
http://localhost:5000/api/auth/login
# Expected: 200 with user claims, session cookie set
# Confirm session is active
curl -b cookies.txt http://localhost:5000/api/auth/me
# Expected: 200 with your claims
401 on login — credentials look correct
- Add a log line inside
ValidateCredentialsAsyncto confirm it is being called at all. - Confirm the password was stored with the same
PasswordHasher<string>used to verify. - Confirm the user record exists in the database.
400 Bad Request on every POST
You are missing the X-Primus-CSRF header. Call GET /api/auth/providers first to receive the XSRF-TOKEN cookie, then send its value as X-Primus-CSRF on every mutating request.
401 on /api/auth/me after a successful login
The session cookie is not being sent back. Confirm your HTTP client is configured to send cookies with requests (e.g. credentials: "include" in fetch, withCredentials: true in axios). For local development confirm CookieDomain is not set (or is set to localhost).
403 with mfaSetupRequired: true
PrimusAuth:Mfa:RequireAllUsers is true and this user has not enrolled. Call POST /api/auth/mfa/enroll to start the enrollment flow.
503 on an SSO provider endpoint
The required credentials for that provider (e.g. TenantId, ClientId, ClientSecret) are missing or still placeholder values. Confirm your dotnet user-secrets or environment variables are loaded before the app starts. See the relevant provider guide (Azure, Okta, Auth0, Google) for the exact keys required.
Next steps
- Provider Setup — where to get tenant IDs, client IDs, secrets, metadata URLs, and SAML settings
- Advanced Configuration — custom user stores, HA clustering, Data Protection for multi-node deployments
- Provider guides — Azure AD, Okta, Auth0, Google, Custom SAML
- Endpoint Reference — every
/api/auth/*endpoint with request and response shapes - MFA Deep Dive — enrollment UX, recovery codes, required MFA enforcement
- Edge Cases — non-happy-path behavior for callbacks, MFA, and SSO login enforcement