Skip to main content
Version: 2026.04.0

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.


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.

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>();
Development vs Production behaviour

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:


3. Configure appsettings.json

The minimal configuration needed to start the broker:

appsettings.json
{
"AuthBroker": {
"BasePath": "/api/auth",
"CookieName": "MyApp.Session"
},
"Auth": {
"PostLoginRedirect": "http://localhost:5173/dashboard",
"ErrorRedirect": "http://localhost:5173/login?error=auth_failed"
}
}
PostLoginRedirect must point to your frontend

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): /dashboard works fine.
  • Cross-origin SPA (React/Angular on a separate port or domain): set an absolute URL — e.g. http://localhost:5173/dashboard locally, https://app.example.com/dashboard in production.
Do not use — only correct for same-origin apps
{
"Auth": {
"PostLoginRedirect": "/",
"ErrorRedirect": "/login"
}
}
Full configuration reference
KeyDefaultDescription
AuthBroker:BasePath/api/authURL prefix for all auth endpoints
AuthBroker:CookieNamePrimus.SessionName of the session cookie
AuthBroker:IssueLocalJwtfalseAlso issue a signed JWT alongside the session cookie
AuthBroker:ActivityAnalyticsEnabledtrueRecord 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:ErrorRedirectWhere the browser is redirected on login failure. Appends ?error=<reason> automatically. Should also be an absolute URL for SPA setups
PrimusAuth:Security:CookieSameSiteModeStrictCookie SameSite policy. Use Lax for cross-origin SPA setups. Use None only in third-party iframe contexts (requires HTTPS)
Jwt:KeySigning key — required only when IssueLocalJwt: true (minimum 32 characters)
Jwt:IssuerJWT iss claim — required only when IssueLocalJwt: true
Jwt:AudienceJWT aud claim — required only when IssueLocalJwt: true
AzureAd:TenantIdAzure AD Directory (tenant) ID
AzureAd:ClientIdAzure AD Application (client) ID
AzureAd:ClientSecretAzure AD client secret — use dotnet user-secrets
Auth0:DomainAuth0 tenant domain (e.g. your-tenant.auth0.com)
Auth0:ClientIdAuth0 application client ID
Auth0:ClientSecretAuth0 client secret — use dotnet user-secrets
Google:ClientIdGoogle OAuth2 client ID
Google:ClientSecretGoogle OAuth2 client secret — use dotnet user-secrets
Okta:DomainOkta tenant domain
Okta:ClientIdOkta application client ID
Okta:ClientSecretOkta client secret — use dotnet user-secrets
PrimusAuth:Mfa:EnabledfalseEnable TOTP MFA
PrimusAuth:Mfa:RequireAllUsersfalseBlock logins for users without MFA enrolled
PrimusAuth:BotProtection:EnabledfalseEnable rate limiting on auth endpoints
PrimusAuth:BotProtection:MaxAttemptsPerMinute5Max login attempts per IP per 60-second window before HTTP 429
PrimusAuth:BotProtection:MaxAttemptsPerHour30Max login attempts per IP per 60-minute window before HTTP 429
PrimusAuth:DomainAllowlist:EnabledfalseRestrict logins to specific email domains
PrimusAuth:DomainAllowlist:AllowedDomainsArray of permitted domains e.g. ["yourcompany.com"]
PrimusAuth:Webhooks:EnabledfalseEnable outbound webhook events
PrimusAuth:Webhooks:EndpointURL to POST signed webhook payloads to
PrimusAuth:Webhooks:SigningSecretHMAC signing secret — use dotnet user-secrets
PrimusAuth:Saml:EntityIdYour app's Entity ID (required when any SAML IdP is configured)
PrimusAuth:Saml:IdentityProvidersArray of SAML IdP configurations (Scheme, MetadataUrl)
Never commit secrets to source control

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.

Setting up SSO providers

Each identity provider has a dedicated setup guide with its own configuration keys, portal steps, and callback URLs. See:

Enabling MFA (TOTP)
appsettings.json
{
"PrimusAuth": {
"Mfa": {
"Enabled": true,
"RequireAllUsers": false
}
}
}

Login flow once MFA is enabled:

  1. POST /api/auth/mfa/enroll → returns secret and otpauthUri (render as QR code)
  2. POST /api/auth/mfa/enroll/confirm with a 6-digit TOTP code to activate
  3. Future logins: POST /api/auth/login returns HTTP 202 with mfaPendingToken
  4. POST /api/auth/mfa/verify with { code, mfaPendingToken } to complete login

Set RequireAllUsers: true to block users without MFA from completing login.

Enabling domain allowlist
appsettings.json
{
"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)
appsettings.json
{
"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
appsettings.json
{
"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:

Program.cs
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

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

The CSRF and session cookies default to SameSite=Strict, which blocks all cross-origin cookie delivery. Change this to Lax for cross-origin setups:

appsettings.json
{
"PrimusAuth": {
"Security": {
"CookieSameSiteMode": "Lax"
}
}
}
ValueWhen to use
StrictFrontend and API served from the same origin (same domain + port)
LaxFrontend and API on different ports or subdomains — use for all SPA setups
NoneRequired 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;
Without credentials: "include" / withCredentials: true

The browser sends no cookies with cross-origin requests by default. Every call to /api/auth/me will return 401 even after a successful login.

CSRF contract for API clients

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 ValidateCredentialsAsync to 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