Skip to content

App Permissions

Allow other applications to call your API with proper authorization.

Property Value
Goal Authorize upstream applications to call your API
Prerequisites API deployed, upstream app's Project ID
Time Estimate 15-20 minutes
Difficulty Intermediate

📋 Overview

When your application is an API that other apps need to call, you must:

  1. Define permissions — declare what your API exposes
  2. Authorize callers — grant specific apps access to those permissions
  3. Enforce permissions — declare @useAuth in TypeSpec so APIM enforces them

Define your API's permissions in both identity providers. Authorize each upstream caller in every provider it uses.


Step 1: Define Permissions

Use the permission naming convention to choose names, then put them in the right config section for each provider.

Prefix Mechanism Entra — infra/api/config.yml Okta
Client.* Scope — requested by callers scopes: infra/auth/ext/okta-client/scopes.yml
App.* Role — granted to users or apps app_roles: infra/auth/ext/okta-client/user_groups.yml

What calling applications request — on behalf of a user, or as themselves.

infra/api/config.yml:

scopes:
  - value: Client.Read
    display_name: Read access
    description: Allows the calling app to read data
  - value: Client.Write
    display_name: Write access
    description: Allows the calling app to write data

infra/auth/ext/okta-client/scopes.yml:

scopes:
  Client.Read: Allows the calling app to read data
  Client.Write: Allows the calling app to write data

Keep both files in sync

The same Client.* values must appear in both files.

What is granted — to users via Business Roles, or to applications for Entra service-to-service calls.

infra/api/config.yml:

app_roles:
  - value: App.Read
    display_name: Read access
    description: Allows reading data
  - value: App.Write
    display_name: Write access
    description: Allows writing data

infra/auth/ext/okta-client/user_groups.yml:

app_permissions:
  App.Read: Allows reading data
  App.Write: Allows writing data

See User Permissions to map Business Roles to these app roles.

Service-to-service needs both

Okta callers request your Client.* scopes directly. Entra callers always request .default and receive the App.* roles you grant them in Step 2. To expose a permission for service-to-service access, define the Client.* scope and a matching App.* role (for example Client.Read + App.Read), then accept either in Step 3.


Step 2: Authorize Upstream Apps

Specify which upstream applications are allowed to call your API. Grant them the permissions they need.

Configure each provider the caller uses

List the upstream app in the config for every provider it authenticates through — for most APIs that means both. At runtime each request uses exactly one provider (selected by tenant origin), so a missing entry surfaces only when a caller arrives via that tenant.

infra/auth/corp/config.yml:

authorized_apps:
  # User-delegated: frontend calls API on behalf of users
  - project_id: it-web-frontend
    scopes:
      - user_impersonation  # Required for user-context calls (platform-managed)
      - Client.Read
    app_roles: []

  # Service-to-service: Entra callers receive App.* roles (requested via .default)
  - project_id: it-api-worker
    scopes: []
    app_roles:
      - App.Read

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

authorized_apps:
  # User-delegated: frontend calls API on behalf of users
  - project_id: it-web-frontend
    scopes:
      - user-groups         # Required for user-context calls (platform-managed)
      - Client.Read

  # Service-to-service: Okta callers request Client.* scopes directly
  - project_id: it-api-worker
    scopes:
      - Client.Read

Step 3: Enforce Permissions

Declare @useAuth in your TypeSpec so APIM generates the correct enforcement policies. See the TypeSpec Authorization guide for the full syntax.

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

One expression covers every caller

Scopes<["Client.Read"]> matches the scp claim — delegated callers and Okta service-to-service. Roles<["App.Read"]> matches the roles/user-groups claims — users with Business Roles and Entra service-to-service callers. Together they cover all access paths with one declaration.


Step 4: Deploy

Which pipeline you run depends on what you changed:

What you changed Pipeline
Permission definitions (infra/api/config.yml) or TypeSpec @useAuth (Steps 1, 3) azure-pipelines-api.yml
Authorized apps only (Step 2) azure-pipelines-auth.yml (faster) — also runs as part of the API pipeline

Deployment order

Your API must be deployed before upstream apps can be authorized. Upstream apps must be deployed before they can call your API.


Removing Authorized Apps

When removing an authorized app and the scopes it uses, follow this order. Entra ID requires that all pre-authorized applications are removed before the scopes they reference are deleted.

flowchart TD
    A["1. Remove authorized app from\ninfra/auth/corp/config.yml"] --> B["2. Run azure-pipelines-auth.yml\n(removes pre-authorization)"]
    B --> C["3. Remove scope from\ninfra/api/config.yml"]
    C --> D["4. Run azure-pipelines-api.yml\n(removes scope)"]

Do not remove scopes first

Removing a scope from infra/api/config.yml before removing the pre-authorized apps that reference it will cause a Terraform error. See Troubleshooting if you have already done this.


✅ Verification

After deploying, verify the configuration:

  1. Check Entra ID — verify the upstream app has API permissions granted
  2. Check Okta — verify the upstream app is listed in authorized clients
  3. Test API call — have the upstream app call your API and verify success

🔍 Troubleshooting

Upstream App Can't Call Your API

Check:

  • Your API is deployed and accessible
  • The upstream app is listed in authorized apps for the provider it authenticates through:
  • Corporate: infra/auth/corp/config.ymlauthorized_apps
  • External: infra/auth/ext/app/authorized-apps.ymlauthorized_apps
  • Requested permissions are defined in your API configuration
  • The upstream app has the correct Project ID configured

Terraform Fails When Removing Pre-Authorized Apps

Error:

Error: Removing pre-authorized application "..." from Application (Application: "...")

unexpected status 400 (400 Bad Request) with error: InvalidValue: Property
api.preAuthorizedApplications.delegatedPermissionIds has a Permission Id
that cannot be found in the AppPermissions sets.

Cause: A scope was removed from infra/api/config.yml and deployed before the pre-authorized apps referencing it were removed.

Fix:

  1. Re-add the deleted scope back to infra/api/config.yml and run azure-pipelines-api.yml
  2. Remove the authorized app entry from infra/auth/corp/config.yml and run azure-pipelines-auth.yml
  3. Remove the scope from infra/api/config.yml again and run azure-pipelines-api.yml