When a background service or a daemon process needs to call a protected Web API in Azure AD, there is no user sitting at a browser to complete a login. The client acquires a token on its own using client credentials, either a secret or a certificate. This changes how permissions get modeled in the App Registration, and it trips up a lot of developers who are used to working with delegated scopes.
Scopes work fine when a user is present because the user consents to what the app can do on their behalf. A daemon has no user, so scopes do not apply. Azure AD instead uses App Roles for this scenario, and the access token carries a roles claim instead of a scp claim. Getting this distinction right, early, saves a lot of confused debugging later when the API keeps returning 401 responses for reasons that are not obvious from the error message alone.
App Roles vs Delegated Scopes
This is worth stating plainly because the Azure portal UI does not make it obvious. Delegated scopes exist for the OAuth2 authorization code flow, where a signed in user consents to permissions on their own behalf. App Roles exist for the client credentials flow, where the calling application itself is the identity, not any user. If you set up scopes for a daemon client, the token simply will not carry the permission the API is checking for, and you will spend time looking in the wrong place.
The setup below assumes a single tenant Azure AD app, since most internal service to service scenarios do not need multi tenant support. It also assumes the App Registration is being created purely to expose an API, not to host any UI, so no platform (Web, SPA, mobile) needs to be configured on it.
Step 1: Create the App Registration
Start in the Azure portal under Azure Active Directory, App Registrations, and create a new registration. Choose the single tenant option unless you specifically need multi tenant access, and skip the redirect URI field entirely since this registration will not handle interactive sign in.

Once the registration exists, note down the Application (client) ID and the Directory (tenant) ID from the Overview page. Both values are needed later when the client application requests a token, and it is easy to grab the wrong one if you are copying values across multiple tabs.
Step 2: Set the Application ID URI
Open the Expose an API blade and set the Application ID URI. Azure AD proposes a default value in the api://{client-id} format, and in most cases that default is fine. This URI becomes part of the audience the API validates against, so the value here has to match what the resource server expects when it checks the aud claim on incoming tokens.

A common mistake here is changing this URI after other services have already been configured against it. If you rename it later, every client and every API validation rule pointing at the old identifier breaks at the same time, usually during a deployment window when nobody wants to be debugging auth failures.
Step 3: Define an App Role
Still in the Expose an API blade (or the App roles blade depending on the portal version), add a new App role. Give it a display name and a value, for example access_as_application, and this is the important part: set Allowed member types to Applications, not Users/Groups or Both.

Selecting Applications here is what makes this role assignable to a client application rather than to a signed in user. If you leave it as Users/Groups by mistake, you will not be able to assign the role to another App Registration at all, and the option simply will not show up later in the API permissions step.
Step 4: Update the Manifest
Open the Manifest editor for the App Registration and check the accessTokenAcceptedVersion field. Set it to 2 and save. This tells Azure AD to issue v2.0 tokens, which have a cleaner claims structure and are what most current Microsoft Identity libraries (MSAL in particular) expect by default.

If this value is left as null or set to 1, you can end up with v1.0 tokens whose claims look slightly different from what your validation code expects, particularly around the roles claim format. It is a small setting that is easy to miss because it lives in raw JSON rather than a portal form field.
Step 5: Assign the App Role to the Client Application
Now switch to the client App Registration, the one that will actually request tokens, and open API permissions. Add a permission, choose APIs my organization uses, search for the name of the API App Registration created above, and select Application permissions rather than Delegated permissions. The App role you defined earlier appears in this list.

After adding the permission, click Grant admin consent. This step needs a tenant administrator, and without it the permission shows up in the portal as configured but the token still will not include the role. This is the step people forget most often, and the resulting failure looks identical to a misconfigured role from the client’s point of view.
Acquiring the Token
The client needs either a client secret or, better for production, a certificate to authenticate itself when requesting a token. Using MSAL, this is a standard client credentials call against the tenant’s token endpoint.
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
var scopes = new[] { "api://<api-app-id>/.default" };
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
Notice the .default suffix on the scope. For client credentials flow, this is not a real scope name, it is a signal to Azure AD to issue a token containing whatever Application permissions have already been granted and consented to for that client, which in this case is the App role created above. Running this successfully returns an access token, but that alone does not confirm the role is present, since Azure AD will still issue a token even if the roles claim ends up empty.
Validating the Role Claim in the API
On the API side, the incoming access token needs to be checked for the expected role, not just for a valid signature and audience. A decoded token with the role correctly assigned looks roughly like this.
{
"aud": "api://<api-app-id>",
"iss": "https://sts.windows.net/<tenant-id>/",
"appid": "<client-app-id>",
"roles": [
"access_as_application"
]
}
If the admin consent step was skipped, or the Allowed member types was left as Users/Groups, the roles array in the token is simply missing or empty. The API’s authorization middleware then rejects the call with a 401, and the response body rarely explains why. This is the single most common failure mode in this setup, and checking the decoded token claims directly (a tool like jwt.ms works well for this) is faster than guessing from the portal configuration.
Rotation and Automation
Whichever credential you choose, secret or certificate, plan for rotation from day one. Secrets in the portal have an expiry date, and when they expire without a replacement in place, the daemon client fails silently until someone notices the downstream errors. Certificates are generally the better choice for anything running in production since they can be managed through Key Vault and rotated without redeploying the client.
Doing all of this through the Azure portal is fine for a one off setup or for learning the concepts, but it does not scale well across multiple environments. The natural next step is to express the App Registration, the App role definition, and the permission grant as a Bicep or Terraform module, so the same configuration can be reproduced consistently across dev, test, and production without anyone clicking through portal blades by hand.
Leave a Reply