Identity Broker Cookbook
Three production-ready recipes, each fully self-contained. Pick the one that matches your needs and follow the numbered steps from top to bottom.
Auth + RBAC
Azure AD sign-in with hierarchical role-based access control. Roles flow Manager to Team Lead to User. One attribute enforces permissions.
View recipeAuth + RBAC + Logging
Recipe 01 with Azure Application Insights. Every authenticated request is tagged with user identity and queryable via KQL.
View recipeAuth + RBAC + Logging + Notifications
Recipe 02 with a welcome email on first login via SMTP and Liquid templates.
View recipeRecipe 01 — Auth + RBAC
Azure AD sign-in with hierarchical role-based access control. Roles inherit permissions downward Manager to Team Lead to User. Enforcement is a single [PrimusAuthorize] attribute per endpoint; no hand-written permission checks.
dotnet add package PrimusSaaS.Identity.Broker --version 2.5.1
dotnet add package PrimusSaaS.Rbac --version 1.0.0
dotnet add package PrimusSaaS.Rbac.InMemory --version 1.0.0
dotnet add package PrimusSaaS.Rbac.AspNetCore --version 1.0.0
using PrimusSaaS.Identity.Broker;
using PrimusSaaS.Rbac;
using PrimusSaaS.Rbac.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Identity Broker -- reads AzureAd and PrimusAuth:Security from configuration
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment())
.WithAutoProvision(
checkUser: async (email, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email);
return user == null
? null
: new PrimusAuthUser { Id = user.Id.ToString(), Email = user.Email, Role = user.Role };
},
autoProvision: async (email, provider, principal, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var newUser = new User { Email = email, Source = provider, Role = "User" };
db.Users.Add(newUser);
await db.SaveChangesAsync();
return new PrimusAuthUser { Id = newUser.Id.ToString(), Email = email, Role = "User" };
});
// RBAC in-memory store. Swap for AddPrimusRbacEfCore<AppDbContext>() for persistence.
builder.Services.AddPrimusRbacInMemory();
builder.Services.AddControllers();
var app = builder.Build();
// CSRF protection must be registered before authentication middleware
app.UsePrimusCsrfProtection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapPrimusAuthBroker();
// Seed roles, permissions, and org groups once on startup
app.Services.SeedPrimusRbac(rbac =>
{
rbac.UpsertPermission(new RbacPermission("perm:reports:read", "read", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertPermission(new RbacPermission("perm:reports:write", "write", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertPermission(new RbacPermission("perm:reports:approve", "approve", "reports", RbacEffect.Allow, new RbacScope()));
// Each role declares only its own permissions; inherited ones flow in automatically
rbac.UpsertRole(new RbacRole("role:user", "User", new RbacScope(), new[] { "perm:reports:read" }));
rbac.UpsertRole(new RbacRole("role:teamlead", "TeamLead", new RbacScope(), new[] { "perm:reports:write" },
Inherits: new[] { "role:user" }));
rbac.UpsertRole(new RbacRole("role:manager", "Manager", new RbacScope(), new[] { "perm:reports:approve" },
Inherits: new[] { "role:teamlead" }));
// Org groups mirror the role hierarchy
rbac.UpsertGroup(new RbacGroup("grp:managers", "Managers", "Org", new RbacScope()));
rbac.UpsertGroup(new RbacGroup("grp:teamleads", "Team Leads", "Org", new RbacScope(), ParentId: "grp:managers"));
rbac.UpsertGroup(new RbacGroup("grp:users", "Users", "Org", new RbacScope(), ParentId: "grp:teamleads"));
});
app.Run();
using PrimusSaaS.Rbac.AspNetCore;
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
[HttpGet]
[PrimusAuthorize("reports", "read")] // User, TeamLead, Manager
public IActionResult GetAll() => Ok(new { reports = "..." });
[HttpPost]
[PrimusAuthorize("reports", "write")] // TeamLead, Manager
public IActionResult Create() => Ok(new { id = "new-report" });
[HttpPost("{id}/approve")]
[PrimusAuthorize("reports", "approve")] // Manager only
public IActionResult Approve(string id) => Ok(new { approved = id });
}
{
"AzureAd": {
"TenantId": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"ClientSecret": "<USE_USER_SECRETS>"
},
"PrimusAuth": {
"Security": {
"TokenEncryptionKey": "<USE_USER_SECRETS>"
}
},
"Auth": {
"PostLoginRedirect": "/"
}
}
Store AzureAd:ClientSecret and PrimusAuth:Security:TokenEncryptionKey in dotnet user-secrets (local) or CI secret injection (production). The encryption key must be 32+ characters.
# Open Azure AD sign-in in a browser
# http://localhost:5000/api/auth/azure
# Confirm the signed-in user and role
curl -b cookies.txt http://localhost:5000/api/auth/me
# Response: {"email":"alice@org.com","role":"Manager","provider":"azure"}
# Test permission enforcement
curl -b cookies.txt http://localhost:5000/api/reports # 200 all roles
curl -b cookies.txt -X POST http://localhost:5000/api/reports # 200 TeamLead+, 403 User
curl -b cookies.txt -X POST http://localhost:5000/api/reports/1/approve # 200 Manager only
Recipe 02 — Auth + RBAC + Logging
Everything from Recipe 01, plus Azure Application Insights. Every authenticated request is tagged with user identity and queryable in the Azure portal via KQL under Logs → traces.
dotnet add package PrimusSaaS.Identity.Broker --version 2.5.1
dotnet add package PrimusSaaS.Rbac --version 1.0.0
dotnet add package PrimusSaaS.Rbac.InMemory --version 1.0.0
dotnet add package PrimusSaaS.Rbac.AspNetCore --version 1.0.0
dotnet add package Microsoft.ApplicationInsights.AspNetCore
using PrimusSaaS.Identity.Broker;
using PrimusSaaS.Rbac;
using PrimusSaaS.Rbac.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Application Insights -- reads ConnectionString from ApplicationInsights:ConnectionString
builder.Services.AddApplicationInsightsTelemetry();
// Identity Broker
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment())
.WithAutoProvision(
checkUser: async (email, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email);
return user == null
? null
: new PrimusAuthUser { Id = user.Id.ToString(), Email = user.Email, Role = user.Role };
},
autoProvision: async (email, provider, principal, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var newUser = new User { Email = email, Source = provider, Role = "User" };
db.Users.Add(newUser);
await db.SaveChangesAsync();
return new PrimusAuthUser { Id = newUser.Id.ToString(), Email = email, Role = "User" };
});
// RBAC
builder.Services.AddPrimusRbacInMemory();
builder.Services.AddControllers();
var app = builder.Build();
app.UsePrimusCsrfProtection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapPrimusAuthBroker();
app.Services.SeedPrimusRbac(rbac =>
{
rbac.UpsertPermission(new RbacPermission("perm:reports:read", "read", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertPermission(new RbacPermission("perm:reports:write", "write", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertPermission(new RbacPermission("perm:reports:approve", "approve", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertRole(new RbacRole("role:user", "User", new RbacScope(), new[] { "perm:reports:read" }));
rbac.UpsertRole(new RbacRole("role:teamlead", "TeamLead", new RbacScope(), new[] { "perm:reports:write" },
Inherits: new[] { "role:user" }));
rbac.UpsertRole(new RbacRole("role:manager", "Manager", new RbacScope(), new[] { "perm:reports:approve" },
Inherits: new[] { "role:teamlead" }));
rbac.UpsertGroup(new RbacGroup("grp:managers", "Managers", "Org", new RbacScope()));
rbac.UpsertGroup(new RbacGroup("grp:teamleads", "Team Leads", "Org", new RbacScope(), ParentId: "grp:managers"));
rbac.UpsertGroup(new RbacGroup("grp:users", "Users", "Org", new RbacScope(), ParentId: "grp:teamleads"));
});
app.Run();
Standard ILogger<T> calls are forwarded to Application Insights automatically. Use named parameters so they appear as queryable custom dimensions.
using System.Security.Claims;
using PrimusSaaS.Rbac.AspNetCore;
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
private readonly ILogger<ReportsController> _logger;
public ReportsController(ILogger<ReportsController> logger) => _logger = logger;
[HttpGet]
[PrimusAuthorize("reports", "read")] // User, TeamLead, Manager
public IActionResult GetAll()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var role = User.FindFirstValue(ClaimTypes.Role);
_logger.LogInformation("Reports read by {UserId} with role {Role}", userId, role);
return Ok(new { reports = "..." });
}
[HttpPost]
[PrimusAuthorize("reports", "write")] // TeamLead, Manager
public IActionResult Create()
{
_logger.LogInformation("Report created by {UserId}", User.FindFirstValue(ClaimTypes.NameIdentifier));
return Ok(new { id = "new-report" });
}
[HttpPost("{id}/approve")]
[PrimusAuthorize("reports", "approve")] // Manager only
public IActionResult Approve(string id)
{
_logger.LogInformation("Report {ReportId} approved by {UserId}", id, User.FindFirstValue(ClaimTypes.NameIdentifier));
return Ok(new { approved = id });
}
}
Named log parameters become custom dimensions. Query them with KQL under Logs > traces:
traces
| where message contains "Reports read"
| extend userId = customDimensions["UserId"]
| summarize count() by tostring(userId)
{
"AzureAd": {
"TenantId": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"ClientSecret": "<USE_USER_SECRETS>"
},
"PrimusAuth": {
"Security": {
"TokenEncryptionKey": "<USE_USER_SECRETS>"
}
},
"Auth": {
"PostLoginRedirect": "/"
},
"ApplicationInsights": {
"ConnectionString": "<USE_USER_SECRETS>"
}
}
Use dotnet user-secrets (local) or CI secret injection (production) for AzureAd:ClientSecret, PrimusAuth:Security:TokenEncryptionKey, and ApplicationInsights:ConnectionString.
# Sign in via Azure AD
# http://localhost:5000/api/auth/azure
# Confirm session
curl -b cookies.txt http://localhost:5000/api/auth/me
# Response: {"email":"alice@org.com","role":"User","provider":"azure"}
# Generate telemetry events
curl -b cookies.txt http://localhost:5000/api/reports
curl -b cookies.txt -X POST http://localhost:5000/api/reports/report-1/approve
# Azure portal: Application Insights > Logs > traces
# Filter: message contains "Reports read"
# Custom dimension UserId maps to the signed-in user
Recipe 03 — Auth + RBAC + Logging + Notifications
Everything from Recipe 02, plus a welcome email sent automatically on first login. The email is rendered from a Liquid template and delivered over SMTP. All credentials stay in dotnet user-secrets.
dotnet add package PrimusSaaS.Identity.Broker --version 2.5.1
dotnet add package PrimusSaaS.Rbac --version 1.0.0
dotnet add package PrimusSaaS.Rbac.InMemory --version 1.0.0
dotnet add package PrimusSaaS.Rbac.AspNetCore --version 1.0.0
dotnet add package PrimusSaaS.Notifications --version 2.0.0
dotnet add package Microsoft.ApplicationInsights.AspNetCore
dotnet user-secrets init
dotnet user-secrets set "AzureAd:ClientSecret" "<azure-ad-client-secret>"
dotnet user-secrets set "PrimusAuth:Security:TokenEncryptionKey" "<32-char-random-string>"
dotnet user-secrets set "ApplicationInsights:ConnectionString" "<app-insights-connection-string>"
dotnet user-secrets set "Notifications:Smtp:Host" "smtp.yourprovider.com"
dotnet user-secrets set "Notifications:Smtp:Port" "587"
dotnet user-secrets set "Notifications:Smtp:Username" "<smtp-username>"
dotnet user-secrets set "Notifications:Smtp:Password" "<smtp-password>"
dotnet user-secrets set "Notifications:Smtp:FromAddress" "no-reply@yourapp.com"
dotnet user-secrets set "Notifications:Smtp:EnableSsl" "true"
using PrimusSaaS.Identity.Broker;
using PrimusSaaS.Notifications;
using PrimusSaaS.Notifications.Abstractions;
using PrimusSaaS.Rbac;
using PrimusSaaS.Rbac.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Application Insights
builder.Services.AddApplicationInsightsTelemetry();
// Notifications -- SMTP credentials read from Notifications:Smtp section
builder.Services.AddPrimusNotifications(n => n
.UseSmtp(builder.Configuration.GetSection("Notifications:Smtp"))
.UseFileTemplates("NotificationTemplates")
.UseLogger());
// Identity Broker
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment())
.WithAutoProvision(
checkUser: async (email, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email);
return user == null
? null
: new PrimusAuthUser { Id = user.Id.ToString(), Email = user.Email, Role = user.Role };
},
autoProvision: async (email, provider, principal, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var notif = sp.GetRequiredService<INotificationService>();
var newUser = new User { Email = email, Source = provider, Role = "User" };
db.Users.Add(newUser);
await db.SaveChangesAsync();
// Welcome email fires only on first login (autoProvision is not called again)
await notif.SendEmailTemplateAsync(
to: email,
templateName: "WelcomeEmail",
model: new { email, appName = "YourApp" });
return new PrimusAuthUser { Id = newUser.Id.ToString(), Email = email, Role = "User" };
});
// RBAC
builder.Services.AddPrimusRbacInMemory();
builder.Services.AddControllers();
var app = builder.Build();
app.UsePrimusCsrfProtection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapPrimusAuthBroker();
app.Services.SeedPrimusRbac(rbac =>
{
rbac.UpsertPermission(new RbacPermission("perm:reports:read", "read", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertPermission(new RbacPermission("perm:reports:write", "write", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertPermission(new RbacPermission("perm:reports:approve", "approve", "reports", RbacEffect.Allow, new RbacScope()));
rbac.UpsertRole(new RbacRole("role:user", "User", new RbacScope(), new[] { "perm:reports:read" }));
rbac.UpsertRole(new RbacRole("role:teamlead", "TeamLead", new RbacScope(), new[] { "perm:reports:write" },
Inherits: new[] { "role:user" }));
rbac.UpsertRole(new RbacRole("role:manager", "Manager", new RbacScope(), new[] { "perm:reports:approve" },
Inherits: new[] { "role:teamlead" }));
rbac.UpsertGroup(new RbacGroup("grp:managers", "Managers", "Org", new RbacScope()));
rbac.UpsertGroup(new RbacGroup("grp:teamleads", "Team Leads", "Org", new RbacScope(), ParentId: "grp:managers"));
rbac.UpsertGroup(new RbacGroup("grp:users", "Users", "Org", new RbacScope(), ParentId: "grp:teamleads"));
});
app.Run();
Add this to your .csproj so templates are copied on build:
<ItemGroup>
<Content Include="NotificationTemplates\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
NotificationTemplates/WelcomeEmail/EmailSubject.liquid
Welcome to {{ appName }}
NotificationTemplates/WelcomeEmail/EmailBody.liquid
<p>Hi there,</p>
<p>Your account has been created. You signed in with <strong>{{ email }}</strong>.</p>
<p>If you did not create this account, contact support immediately.</p>
<p>The {{ appName }} team</p>
Only non-secret values belong here. Secrets are in user-secrets (Step 1):
{
"AzureAd": {
"TenantId": "<your-tenant-id>",
"ClientId": "<your-client-id>"
},
"PrimusAuth": {
"Security": {
"TokenEncryptionKey": ""
}
},
"Auth": {
"PostLoginRedirect": "/"
},
"ApplicationInsights": {
"ConnectionString": ""
},
"Notifications": {
"Smtp": {
"Host": "",
"Port": 587,
"FromAddress": "",
"EnableSsl": true
}
}
}
AzureAd:ClientSecret, PrimusAuth:Security:TokenEncryptionKey, ApplicationInsights:ConnectionString, Notifications:Smtp:Username, and Notifications:Smtp:Password must come from dotnet user-secrets (local) or CI environment variables (production).
# Confirm all secrets are set
dotnet user-secrets list
# First-time login -- triggers the welcome email
# http://localhost:5000/api/auth/azure
# Second login with the same account -- no duplicate email
# http://localhost:5000/api/auth/azure
# Confirm session
curl -b cookies.txt http://localhost:5000/api/auth/me
# Response: {"email":"alice@org.com","role":"User","provider":"azure"}
Next steps
- Endpoint Reference - every
/api/auth/*endpoint with full request and response shapes - Advanced Configuration - custom user stores, HA clustering, Data Protection for multi-node deployments
- MFA / 2FA - TOTP enrollment, recovery codes, and required MFA enforcement
- Bot Protection - rate limiting and CAPTCHA integration for production hardening
- Webhooks - event-driven callbacks for
user.created,session.started, and more