Auth0
Configure B2C or B2B SaaS authentication with Auth0.
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();
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
{
"Auth0": {
"Domain": "your-tenant.us.auth0.com",
"ClientId": "YOUR_CLIENT_ID",
"ClientSecret": "YOUR_CLIENT_SECRET"
},
"Auth": {
"PostLoginRedirect": "/"
},
"PrimusAuth": {
"Security": {
"TokenEncryptionKey": "change-this-to-a-32-char-secret!!"
}
}
}
Configuration keys
| Key | Type | Required | Expected Values | Description |
|---|---|---|---|---|
Auth0:Domain | string | Yes | e.g. your-tenant.us.auth0.com | Your Auth0 tenant domain. Found in Auth0 Dashboard → Applications → Settings. |
Auth0:ClientId | string | Yes | Application Client ID GUID | The Client ID from your Auth0 application settings. |
Auth0:ClientSecret | string | Yes | Secret value from Auth0 | Client secret. Use environment variables in production — never hardcode. |
Auth0:Organization | string | No | e.g. org_xxxxxxxxxx | Auth0 Organization ID for B2B multi-tenancy. Omit if not using organizations. See Using Organizations below. |
Auth:PostLoginRedirect | string | No | Any route path, e.g. /, /dashboard | Where to redirect the user after successful login. Defaults to /. |
PrimusAuth:Security:TokenEncryptionKey | string | Yes | Any random 32+ character string | Encrypts the session cookie (AES-256). Shared across all providers. Use environment variables in production — never hardcode. |
How to get these values from Auth0 Dashboard
1. Create the application
- Log in to the Auth0 Dashboard.
- Go to Applications > Applications > Create Application.
- Select Regular Web Application and click Create.
The Identity Broker uses a server-side Authorization Code + PKCE flow with a client secret. Using any other type (SPA, Native, Machine to Machine) will result in a 403 Forbidden error at the /authorize endpoint.
2. Configure application URLs
In the Settings tab, set:
- Allowed Callback URLs:
http://localhost:5000/api/auth/auth0/callback - Allowed Logout URLs:
http://localhost:5000 - Allowed Web Origins:
http://localhost:5000
Click Save Changes.
Auth0 does exact-match validation.
http://localhost:5000alone is not enough — the full/api/auth/auth0/callbackpath is required in Allowed Callback URLs.
3. Copy your credentials
From the Settings tab:
- Domain →
Auth0:Domain - Client ID →
Auth0:ClientId - Client Secret →
Auth0:ClientSecret
Secrets via environment variables
Never put ClientSecret or TokenEncryptionKey in appsettings.json or commit them to source control. Supply them as environment variables instead. ASP.NET Core maps double-underscore (__) to the colon (:) separator:
# Linux / macOS / Docker
export Auth0__ClientSecret="your-auth0-client-secret"
export PrimusAuth__Security__TokenEncryptionKey="your-32-char-random-key"
# Windows PowerShell
$env:Auth0__ClientSecret = "your-auth0-client-secret"
$env:PrimusAuth__Security__TokenEncryptionKey = "your-32-char-random-key"
# Docker Compose / .env file (git-ignored)
Auth0__Domain=your-tenant.us.auth0.com
Auth0__ClientId=your-client-id
Auth0__ClientSecret=your-auth0-client-secret
PrimusAuth__Security__TokenEncryptionKey=your-32-char-random-key
In CI/CD (GitHub Actions, Azure DevOps), map your pipeline secrets to these same environment variable names.
# Linux/macOS
openssl rand -base64 32
# PowerShell
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Max 256 }))
Using Organizations (optional)
The broker supports two org modes:
Static (one org per deployment) — set Auth0:Organization in config. Every login is scoped to that org.
Dynamic (multiple orgs, one deployment) — not yet supported in the current release. The broker reads only the static Auth0:Organization config value. For multi-org routing today, deploy one broker instance per organization, each with its own Auth0:Organization config. Dynamic org selection via a query parameter is on the roadmap.
For the static approach, add Organization to your config:
"Auth0": {
"Domain": "your-tenant.us.auth0.com",
"ClientId": "YOUR_CLIENT_ID",
"ClientSecret": "YOUR_CLIENT_SECRET",
"Organization": "org_xxxxxxxxxx"
}
When Organization is set, Auth0 returns an error unless all four of these are configured:
- Organizations enabled on the app — Applications → your app → Login Experience tab → under "Types of Users" select "Business Users" or "Both" → Save Changes. Without this, Auth0 rejects the
organizationparameter withparameter organization is not allowed for this client. - Connections enabled — Organizations → your org → Connections tab → Enable Connections → enable at least
Username-Password-Authentication. - Application added to org — Organizations → your org → Overview tab → scroll to Applications → Add Applications → select your app.
- User is a member — Organizations → your org → Members tab → add the user account you are logging in with.
If you are just testing without org enforcement, omit the Organization field entirely.
Step 4: Available endpoints
Endpoints are registered automatically by app.MapPrimusAuthBroker().
| Endpoint | Method | Description |
|---|---|---|
GET /api/auth/providers | GET | Returns the list of configured providers. Also seeds the XSRF-TOKEN cookie. |
GET /api/auth/auth0 | GET | Initiates the Auth0 login redirect |
GET /api/auth/me | GET | Returns the current authenticated user (401 if not logged in) |
POST /api/auth/logout | POST | Clears 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":["auth0"]}
2. Sign in via browser
Open this URL in your browser — it will redirect to the Auth0 login page:
http://localhost:5000/api/auth/auth0
Sign in with your Auth0 user credentials. After login, Auth0 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@example.com",
"role": "User",
"provider": "auth0"
}
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.
The logout endpoint is POST-only. Navigating to a URL in the browser sends a GET, which returns 405 Method Not Allowed and does not clear the session. The CSRF header requirement also prevents third-party pages from logging users out silently.
Notes
Consent screen on first login
On Auth0 development tenants, users see a consent screen asking to approve openid profile email scopes on their first login after each logout. This is normal behaviour. To suppress it, go to Auth0 Dashboard → APIs → your API → enable "Allow Skipping User Consent".
Supported application types
Only Regular Web Application is supported. The broker is a server-side session broker — it holds session cookies on the backend and exchanges an authorization code for tokens server-side. SPA and Native flows handle tokens client-side, which is outside the broker's scope.
TokenEncryptionKey is shared across providers
PrimusAuth:Security:TokenEncryptionKey encrypts session tokens for all configured providers (Auth0, Azure AD, Google, Okta). Define it once and all providers use it.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
403 Forbidden at /authorize | Wrong application type | Change app type to Regular Web Application in Auth0 Dashboard → Applications → Settings |
parameter organization is not allowed for this client | Types of Users not set to include organizations | Applications → your app → Login Experience tab → Types of Users → select "Business Users" or "Both" → Save Changes |
403 Forbidden with Organization set | App not enabled for the org | Dashboard → Organizations → your org → Overview → Applications → Add your app |
403 Forbidden with Organization set | No connections enabled on org | Dashboard → Organizations → your org → Connections → Enable Connections |
403 Forbidden with Organization set | User not a member of the org | Dashboard → Organizations → your org → Members → Add the user |
400 Invalid CSRF token on logout | Sending a hardcoded value like '1' | Read the XSRF-TOKEN cookie value and send it in the X-Primus-CSRF header |
405 Method Not Allowed on logout | Navigating to logout URL in browser (GET) | Call logout as a POST from JavaScript — see Step 5 above |
| Consent screen appears after every logout | Auth0 development tenant behaviour | Enable "Allow Skipping User Consent" on the API in Auth0 Dashboard |
SAML 2.0 (Enterprise SSO)
Auth0 can act as a SAML 2.0 Identity Provider, which lets you use the Primus SAML integration while Auth0 handles the upstream user directory. This is common when your enterprise customers authenticate against Auth0 organizations and you want a standards-based SAML assertion flowing into your app.
Use IdpType: Custom and point MetadataUrl at your Auth0 tenant's SAML metadata endpoint:
{
"PrimusAuth": {
"Saml": {
"EntityId": "https://your-app.example.com",
"IdentityProviders": [
{
"Scheme": "auth0-saml",
"DisplayName": "Auth0 (SAML)",
"IdpType": "Custom",
"MetadataUrl": "https://YOUR_DOMAIN.auth0.com/samlp/metadata/YOUR_CLIENT_ID"
}
]
}
}
}
Replace YOUR_DOMAIN with your Auth0 tenant domain (e.g. my-tenant.us.auth0.com) and YOUR_CLIENT_ID with the Client ID of the Auth0 application you have configured as a SAML IdP.
To use Auth0 as a SAML IdP, you must enable the SAML2 Web App addon on the application in Auth0 Dashboard → Applications → your app → Addons tab. Set the Application Callback URL to https://your-app.example.com/api/auth/auth0-saml/callback.
For full SAML configuration options see the SAML 2.0 page.
Advanced features
The following features work with Auth0 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
- MFA Deep Dive — TOTP enrollment UX, SMS codes, and recovery
- Advanced Configuration — custom stores, HA clustering, and data protection
- Database Setup — persist provisioned users with EF Core