Aspire Security Configuration¶
Define security configuration in C# code and generate YAML files during publish.
| Property | Value |
|---|---|
| Goal | Configure authentication and authorization using Aspire hosting extensions |
| Prerequisites | Forge API project with Aspire AppHost |
| Time Estimate | 15-30 minutes |
| Difficulty | Intermediate |
Experimental Feature (SAIFSECURITY001)
The Aspire security configuration APIs are experimental and subject to change in future releases. The [Experimental] attribute will generate compiler warning SAIFSECURITY001 when using these APIs.
To suppress the warning, add the following to your project file:
π Overview¶
Instead of manually editing YAML configuration files, you can define security configuration in your AppHost using strongly-typed C# APIs. During aspire publish, the configuration is automatically generated into the correct YAML files consumed by Terraform.
Benefits¶
- Compile-time safety - Invalid configurations fail at build time
- IntelliSense support - Discover available permissions and roles
- Single source of truth - Configuration lives alongside your application code
- Automatic generation - YAML files are created during publish pipeline
Step 1: Define Permissions¶
Create static classes to define the permissions your API exposes and consumes.
App Roles (Application Permissions)¶
App roles are application-level permissions assigned to users via Business Roles or to other applications for service-to-service access:
// Auth/Permissions.cs
public static class Permissions
{
// App roles this API exposes
public static readonly AppRole ClaimsRead = new("Claims.Read", "Read claims data");
public static readonly AppRole ClaimsWrite = new("Claims.Write", "Write claims data");
public static readonly AppRole PolicyRead = new("Policy.Read", "Read policy data");
}
Scopes (Delegated Permissions)¶
Scopes are delegated permissions where user context flows through for app-to-app calls:
// Auth/Permissions.cs
public static class Permissions
{
// Scopes for upstream API calls (user context flows through)
public static readonly Scope OrdersRead = new("Orders.Read", "Read orders");
public static readonly Scope OrdersWrite = new("Orders.Write", "Write orders");
// App roles for upstream API calls (service-to-service)
public static readonly AppRole OrdersAppRead = new("Orders.App.Read", "App-level read");
}
Step 2: Define Business Roles¶
Business roles are groups of users defined in your identity system (Entra ID security groups or Okta groups). These are created in dedicated business roles repositories and deployed org-wide for any application to use.
Business Roles Are Org-Wide
Business Roles are created in team-specific repos (e.g., {Project}-okta-business-roles-corp, {Project}-okta-business-roles-external) and deployed organization-wide. Your application references existing Business Rolesβit doesn't create new ones.
See Business Roles for details on creating new Business Roles.
Define references to existing Business Roles in a dedicated class:
// Auth/BusinessRoles.cs
public static class BusinessRoles
{
// Corporate (Entra) users - must exist in business roles repo
public static readonly BusinessRole ClaimsAdjuster = new("Claims Adjuster");
public static readonly BusinessRole PolicyViewer = new("Policy Viewer");
// External (Okta) users - must exist in business roles repo
public static readonly BusinessRole InjuredWorker = new("Injured Worker");
}
Names Must Match Exactly
Business role names must match the names defined in the business roles repository exactly. If the role doesn't exist, the auth pipeline will fail with a "group not found" error.
Step 3: Define App Role Assignments¶
Create AppRoleAssignment instances that pair business roles with the app roles they should receive:
// Auth/AppRoleAssignments.cs
public static class AppRoleAssignments
{
// Corporate (Entra) users
public static readonly AppRoleAssignment ClaimsAdjuster = new(
BusinessRoles.ClaimsAdjuster,
[Permissions.ClaimsRead, Permissions.ClaimsWrite]);
public static readonly AppRoleAssignment PolicyViewer = new(
BusinessRoles.PolicyViewer,
[Permissions.PolicyRead]);
// External (Okta) users
public static readonly AppRoleAssignment InjuredWorker = new(
BusinessRoles.InjuredWorker,
[Permissions.ClaimsRead, Permissions.PolicyRead]);
}
Compile-Time Validation
AppRoleAssignment only accepts AppRole arrays, not Scope. This ensures you can't accidentally add a delegated permission to a user role.
Step 4: Configure in AppHost¶
Use the Aspire hosting extensions to configure security:
// Program.cs in your AppHost project
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.MyApi>("api");
// Add security resource with app role assignments
builder.AddSecurity()
.WithCorporateAssignment(AppRoleAssignments.ClaimsAdjuster) // Entra users
.WithCorporateAssignment(AppRoleAssignments.PolicyViewer) // Entra users
.WithExternalAssignment(AppRoleAssignments.InjuredWorker); // Okta users
// Configure upstream API dependencies
builder.AddUpstreamApi("it-api-proc-orders")
.WithScope(Permissions.OrdersRead) // Delegated permission
.WithScope(Permissions.OrdersWrite) // Delegated permission
.WithAppRole(Permissions.OrdersAppRead); // Application permission
builder.Build().Run();
Architecture Pattern
SecurityResource owns the pipeline steps for generating security configuration files. It collects corporate and external app role assignments from annotations, then generates YAML files during aspire publish.
Extension Methods¶
| Method | Description |
|---|---|
AddSecurity() |
Creates the security resource |
WithCorporateAssignment(assignment) |
Adds an app role assignment for corporate (Entra) users |
WithExternalAssignment(assignment) |
Adds an app role assignment for external (Okta) users |
AddUpstreamApi(projectId) |
Declares a dependency on an upstream API |
WithScope(scope) |
Grants a delegated permission to the upstream API |
WithAppRole(appRole) |
Grants an application permission to the upstream API |
Step 5: Publish to Generate Configuration¶
Run aspire publish to generate the YAML configuration files:
The publish pipeline generates four configuration files in your infra/ folder.
Generated Files¶
infra/api/config.yml¶
All app roles exposed by this API (deduplicated from business roles):
app_roles:
- value: Claims.Read
display_name: Read claims data
- value: Claims.Write
display_name: Write claims data
- value: Policy.Read
display_name: Read policy data
scopes: []
infra/auth/corp/config.yml¶
Business roles for corporate (Entra) users and authorized upstream apps:
business_roles:
- name: Claims Adjuster
app_roles:
- Claims.Read
- Claims.Write
- name: Policy Viewer
app_roles:
- Policy.Read
authorized_apps:
- project_id: it-api-proc-orders
app_roles:
- Orders.App.Read
scopes:
- Orders.Read
- Orders.Write
infra/auth/ext/user/business-role-app-role.yml¶
Business roles for external (Okta) users:
infra/auth/ext/app/authorized-apps.yml¶
Upstream API delegated permissions (scopes for Okta):
β Verification¶
After publishing:
- Check generated files - Verify the YAML files exist in
infra/ - Validate YAML - Ensure the content matches your C# configuration
- Run auth pipeline - Deploy the configuration to your identity providers
π Troubleshooting¶
Files Not Generated¶
β Check:
AddSecurity()is called in your AppHost- You're running
aspire publish, notdotnet run - The AppHost project references
SAIF.Platform.Aspire.Hosting
Missing Business Role in Output¶
β Check:
- Role is added with
WithCorporateAssignment()orWithExternalAssignment() AppRoleAssignmenthas at least one app role defined
Upstream API Not in Output¶
β Check:
AddUpstreamApi()is called with the correct project ID- At least one permission is granted with
WithScope()orWithAppRole()
π‘ Design Notes¶
Permission Types¶
| Type | Class | Use Case |
|---|---|---|
| App Role | AppRole |
Application permissions for users and service-to-service |
| Scope | Scope |
Delegated permissions where user context flows through |
| Business Role | BusinessRole |
Name reference to an identity system group |
| App Role Assignment | AppRoleAssignment |
Pairs a business role with app roles it should receive |
Why Separate CorporateAssignment and ExternalAssignment?¶
These map to different identity providers (Entra vs Okta) and generate to different file locations. The API abstracts where the config goes, but the distinction is meaningful for the infrastructure.
Why C# Instead of YAML?¶
- Type safety - Compile-time validation prevents misconfiguration
- Refactoring - Rename a permission and all usages update
- Discoverability - IntelliSense shows available permissions
- Testing - Unit test your security configuration
π Related Documentation¶
- Aspire Publish - Generate pipelines and infrastructure config from your AppHost
- User Permissions - Understanding YAML-based user permission configuration
- App Permissions - YAML-based app permission configuration
- Business Roles - How Business Roles work
- Deployment Workflow - Step-by-step deployment guide