Skip to content

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:

<PropertyGroup>
  <NoWarn>$(NoWarn);SAIFSECURITY001</NoWarn>
</PropertyGroup>

πŸ“‹ 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:

dotnet run --project src/MyApi.AppHost -- publish

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:

authorized_business_roles:
  - name: Injured Worker
    app_roles:
      - Claims.Read
      - Policy.Read

infra/auth/ext/app/authorized-apps.yml

Upstream API delegated permissions (scopes for Okta):

authorized_apps:
  - project_id: it-api-proc-orders
    scopes:
      - Orders.Read
      - Orders.Write

βœ… Verification

After publishing:

  1. Check generated files - Verify the YAML files exist in infra/
  2. Validate YAML - Ensure the content matches your C# configuration
  3. 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, not dotnet run
  • The AppHost project references SAIF.Platform.Aspire.Hosting

Missing Business Role in Output

βœ… Check:

  • Role is added with WithCorporateAssignment() or WithExternalAssignment()
  • AppRoleAssignment has 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() or WithAppRole()

πŸ’‘ 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