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:
- Define permissions — declare what your API exposes
- Authorize callers — grant specific apps access to those permissions
- Enforce permissions — declare
@useAuthin 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:
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.
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:
- Check Entra ID — verify the upstream app has API permissions granted
- Check Okta — verify the upstream app is listed in authorized clients
- 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.yml→authorized_apps - External:
infra/auth/ext/app/authorized-apps.yml→authorized_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:
- Re-add the deleted scope back to
infra/api/config.ymland runazure-pipelines-api.yml - Remove the authorized app entry from
infra/auth/corp/config.ymland runazure-pipelines-auth.yml - Remove the scope from
infra/api/config.ymlagain and runazure-pipelines-api.yml
📚 Related Documentation¶
- Permission Naming Conventions — naming table and rules
- User Permissions — map Business Roles to App Roles
- Business Roles — how Business Roles work
- Calling Downstream APIs — configure your app to call other APIs