Skip to content

User Permissions

Set up Business Roles and map them to application permissions for user access control.

Property Value
Goal Configure which users can access your application
Prerequisites Forge API project created, understanding of Business Roles
Time Estimate 15-30 minutes
Difficulty Intermediate

📋 Overview

User permissions control which Business Roles can access your application and what they can do. Configuration must be done in both identity providers to support corporate and external users. Users authenticate through Entra or Okta and receive your App Roles through their Business Roles; see How Business Roles Work for the full flow.


Step 1: Define Application Roles

Define the roles available in your application in infra/api/config.yml:

# App Roles - assigned to users/groups and applications
app_roles:
  - value: App.Read
    display_name: Read Access
    description: Allows reading data
  - value: App.Write
    display_name: Write Access
    description: Allows writing data
  - value: App.Admin
    display_name: Administrator
    description: Full administrative access

Entra ID Constraint

App roles and scopes cannot share the same value in Entra ID. Use the permission naming convention: App.* for app roles and Client.* for scopes.

For external (Okta) authentication, also define permissions in infra/auth/ext/okta-client/user_groups.yml:

# Keep in sync with infra/api/config.yml app_roles
app_permissions:
  App.Read: Allows reading data
  App.Write: Allows writing data
  App.Admin: Full administrative access

Keep Files Synchronized

Both files must contain the same role values. Changes to one should be reflected in the other.


Step 2: Map Business Roles to App Roles

Configure which Business Roles have access to which roles in both identity providers:

Provider File Key
Corporate (Entra ID) infra/auth/corp/config.yml business_roles
External (Okta) infra/auth/ext/user/business-role-app-role.yml authorized_business_roles

Each entry names an existing Business Role and lists the App.* roles from Step 1 that it receives. See Using Business Roles in Your App for the YAML for each provider and the required deployment order.


Step 3: Deploy Your API

Before running authentication pipelines, your API must be deployed:

  1. Deploy your API using the standard pipeline (azure-dotnet-api.yml)
  2. This creates the Entra app registration and Okta client
  3. Required: API must be deployed to all environments including production before auth pipelines will work

Step 4: Run Authentication Pipeline

Apply your security configuration:

Option A: Standalone Auth Pipeline

Run azure-pipelines-auth.yml to deploy auth configuration independently:

  • Use when you only need to update auth configuration
  • Deploys both corporate (Entra) and external (Okta) auth
  • Useful for granting new apps access without redeploying your API

Option B: With API Deployment

Auth configuration also deploys automatically with your API:

  • Standard API deployment includes auth configuration
  • Use for regular deployments where both code and auth change

✅ Verification

After deploying, verify the configuration:

  1. Check Entra ID: Verify app roles appear in the Enterprise Application
  2. Check Okta: Verify groups are created for each app role
  3. Test Access: Log in as a user with a mapped Business Role and verify permissions

My Apps visibility

Enterprise applications created by the identity module are hidden from users' Microsoft 365 My Apps page by default (feature_tags.hide is set on the service principal). This is controlled by the identity module's visible_to_users variable, which defaults to false. Set visible_to_users = true for user-facing web apps where users sign in directly, such as saif-web-service, once it pins a resources/saif registry version that publishes this input. APIs, event subscribers, and other apps consumed programmatically should keep the default and stay hidden.

saif-web-service is exact-pinned to a resources/saif version that predates visible_to_users, so it is not affected by this default either way yet: its enterprise application stays on its current, always-visible behavior until the caller is wired up as described above.


🔍 Troubleshooting

Users Can't Access the Application

✅ Check:

  • Business Role is correctly mapped in the appropriate config file:
    • Corporate users: infra/auth/corp/config.yml → business_roles
    • External users: infra/auth/ext/user/business-role-app-role.yml → authorized_business_roles
  • User has the correct Business Role assigned in the identity system
  • Application roles are defined in infra/api/config.yml
  • Auth pipeline has been run after configuration changes

"Insufficient Permissions" Errors

✅ Check:

  • The Business Role has the necessary app roles assigned
  • The app roles match what the application code expects
  • Both corp and ext configurations are in sync

Configuration Out of Sync

✅ Check:

  • App roles in infra/api/config.yml match infra/auth/ext/okta-client/user_groups.yml
  • Auth pipeline has been run after all configuration changes

📋 TypeSpec Auth Configuration

Your API's TypeSpec definition includes an @useAuth decorator that specifies required authentication. One declaration covers both providers and all access patterns:

@useAuth(Scopes<["Client.Read"]> | Roles<["App.Read"]>)

Roles<> is the helper that users with a mapped Business Role satisfy. How Authentication Works explains which token claim each helper checks in each provider.

Experience APIs and the access scope

External-facing Experience APIs are generated with Scopes<["access"]> instead of a Client.* scope. The access scope is platform-managed — automatically exposed for the OIDC login flow — so you don't define it in your config files. Treat it like user_impersonation/user-groups: leave it alone, and use Client.* for any custom scopes you add.

Ensure your TypeSpec auth decorator matches your configuration files:

TypeSpec Reference Configuration File Key
Roles<["App.Read"]> infra/api/config.yml app_roles
Scopes<["Client.Read"]> infra/api/config.yml scopes

Tip

The template generates the correct @useAuth decorator based on your API type. If you add custom roles, update both the TypeSpec file and configuration files.