> TL;DR — To let a backend application call the McKim & Creed Employee API without a signed-in user, you register an app-to-app (client credentials) connection in Microsoft Entra ID. The Employee API already exposes the app role ProjectData.Read. For each new client app you (1) assign that application permission to the client app registration, (2) grant admin consent, and (3) give the client app a secret or certificate. The client then requests a token using the scope api:///.default and calls the API with that bearer token.
This article covers the app-to-app pattern only — there is no user login involved. The calling application authenticates as itself using its own credential. If your scenario needs the API to know which user is calling (delegated / on-behalf-of), that is a different setup and is out of scope here.
---
Concepts (read this first)
There are two identities and three moving parts. Getting these straight prevents 90% of the confusion.
| Term | What it is | Where it lives |
| --- | --- | --- |
| Employee API app registration | The "resource" — the API being called. Owns the app roles. | Entra ID → App registrations |
| Client app registration | The "caller" — the app that wants in. Gets assigned a role. | Entra ID → App registrations |
| App role (ProjectData.Read) | An application permission defined on the API. Shows up in the token's roles claim. | On the Employee API registration |
| Permission assignment | The grant that says "this specific client app may use ProjectData.Read." Done per client app. | On the client registration |
| Client secret / certificate | The credential the client uses to prove it is itself. | On the client registration |
Key fact: The role ProjectData.Read is defined once on the Employee API and is reused by every client app. You do not create a new role per app — you create a new assignment per app. You only create a new role when a client needs a different level of access (e.g. write access, or a different set of endpoints).
> [!info] Values you'll need throughout
> Replace these placeholders wherever they appear. Fill in the real values once and reuse them.
>
> - Tenant ID (Directory ID): 354619d1-eb70-4490-b9be-08842a4c270e
> - Employee API — Application (client) ID: 1af74f73-c0b1-4df8-9b83-0e4cb964946e
> - Employee API — Application ID URI: api://1af74f73-c0b1-4df8-9b83-0e4cb964946e
> - Employee API — base URL: https://mckimcreedapi.mckimcreed.com/
> - App role value: ProjectData.Read
---
Prerequisites
- The client application is already registered in Entra ID. (If it isn't, register it first: Entra ID → App registrations → New registration.)
- You have an account with Application Administrator (or Global Administrator) rights, or access to someone who does — admin consent is required and cannot be self-served by the app.
- You know which client app needs access and confirm it needs read access to the same endpoints (
ProjectData.Readis the correct role).
---
Part 1 — Confirm the app role exists on the Employee API
You normally do not need to do anything here — ProjectData.Read already exists. This section is just to verify (or to create a new role if a different access level is needed).
- Go to Entra ID / Microsoft Entra admin center → App registrations → search for and open the Employee API registration.
- In the left menu, click App roles.
- Confirm a role with Value =
ProjectData.Readexists and Allowed member types = Applications (or "Both"). This is what makes it an application permission usable for app-to-app.
> The McKim and Creed Employee API (Pyramid) → App roles blade. Note ProjectData.Read (and EmployeeData.Read) with Allowed member types = Applications — this is what makes them usable for app-to-app.
> [!note] Only if you need a NEW role (different access level)
> Click Create app role and set:
> - Display name: human-readable, e.g. Read Project Data
> - Allowed member types: Applications
> - Value: the string that lands in the token's roles claim, e.g. ProjectData.ReadWrite
> - Description + Do you want to enable this app role? ☑
>
> Then in the API code, map that new role value to the endpoints it should unlock.
---
Part 2 — Assign the permission to the client app
This is the per-app work that actually grants access.
- In App registrations, open the client application registration (the app that will call the Employee API).
- Left menu → API permissions. This is the blade you'll work from — click Add a permission.
> The client app's API permissions blade (example: the BEI Board app). Click + Add a permission to start.
- Choose the My APIs tab (or APIs my organization uses and search for it) and select McKim and Creed Employee API (Pyramid).
- Choose Application permissions — NOT Delegated permissions. This is the critical choice for app-to-app (note the description: "Your application runs as a background service or daemon without a signed-in user.").
- Check the box for
ProjectData.Readunder the ProjectData group and click Add permissions.
> The Request API permissions flyout: Application permissions selected, with ProjectData.Read checked.
At this point the permission is added but not active — it will show status "Not granted" with a warning icon.
---
Part 3 — Grant admin consent
Application permissions always require an administrator to consent on behalf of the organization. The app cannot prompt for this itself.
- Still on the client app's API permissions blade, click Grant admin consent for McKim & Creed, Inc.
- Confirm. The
ProjectData.Readrow should change to a green "Granted for McKim & Creed" status.
> After consent — ProjectData.Read shows a green ✅ Granted for McKim & Creed in the Status column.
> [!warning] If you skip this step
> Tokens issued to the client app will not contain the roles claim, and the Employee API will reject the calls with 403 (or 401, depending on how the API validates). "Granted" must be green.
---
Part 4 — Create a credential for the client app
The client proves its identity with either a client secret (simple, expires) or a certificate (preferred for production). Pick one.
Option A — Client secret (quick start)
- On the client app registration → Certificates & secrets → Client secrets → New client secret.
- Add a description (e.g.
Employee API access) and an expiry (default 6 months; choose per policy). - Click Add, then immediately copy the secret VALUE — it is shown only once and cannot be retrieved later.
> The Certificates & secrets → Client secrets tab. Use + New client secret, then copy the Value immediately — Azure masks it (e.g. C4s••••) as soon as you leave the page and it can never be retrieved again. The Secret ID is just an identifier, not the secret itself.
Option B — Certificate (recommended for production)
Upload the public key (.cer) under Certificates & secrets → Certificates → Upload certificate. The client app uses the corresponding private key to authenticate. Preferred because it avoids a shared secret that can leak or expire silently.
---
Part 5 — What to hand the developer building the client app
Give them the following (the secret is best generated by them in their own app registration so it never travels):
| Item | Value |
| --- | --- |
| Tenant ID | 354619d1-eb70-4490-b9be-08842a4c270e |
| Client (caller) Application ID | (their own app's client ID — they likely have it) |
| Client secret or certificate | (from Part 4 — secret value, or the cert they uploaded) |
| Scope to request | api://1af74f73-c0b1-4df8-9b83-0e4cb964946e/.default |
| Employee API base URL | https://mckimcreedapi.mckimcreed.com/ |
| Allowed endpoints | (list the endpoints this role unlocks) |
> [!important] For client-credentials, the scope is always .default
> The caller requests api://1af74f73-c0b1-4df8-9b83-0e4cb964946e/.default — not ProjectData.Read directly. The roles actually granted come from the admin-consented application permissions, and .default tells Entra "give me everything this app is consented for."
Example client code (MSAL, .NET confidential client)
var app = ConfidentialClientApplicationBuilder
.Create("<CLIENT_APP_ID>") // the calling app's own client ID
.WithClientSecret("<CLIENT_SECRET>") // or .WithCertificate(cert)
.WithAuthority("https://login.microsoftonline.com/354619d1-eb70-4490-b9be-08842a4c270e")
.Build();
// .default is required for client credentials flow
string[] scopes = { "api://1af74f73-c0b1-4df8-9b83-0e4cb964946e/.default" };
AuthenticationResult result =
await app.AcquireTokenForClient(scopes).ExecuteAsync();
// Attach result.AccessToken as a Bearer token on requests to the Employee API
var http = new HttpClient { BaseAddress = new Uri("https://mckimcreedapi.mckimcreed.com/") };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", result.AccessToken);
> The defining method is AcquireTokenForClient — this is the app-to-app (daemon) call. If you see AcquireTokenInteractive or a sign-in popup, that's the delegated user flow, which is not what this setup uses.
> [!tip] Full client implementation
> For the production-grade .NET 8 implementation (DI registration, a DelegatingHandler, and a typed client), see the companion article: Connecting a .NET 8 Service to the Employee API. A Word version for sending outside the vault is attached there.
---
How the Employee API validates the call
For reference, the API checks the incoming bearer token for:
-
Audience (
aud) = the Employee API's client ID or App ID URI. -
Issuer (
iss) = our tenant. -
rolesclaim containsProjectData.Read— and the requested endpoint is one the role permits. -
No user claims are present (no
scp/scope, no username/oid) — this confirms it's an application token, not a delegated one.
Because every app-to-app caller presents the same ProjectData.Read role, the API can't tell callers apart by role alone. To distinguish which app is calling (for logging, throttling, or restricting a shared role to specific apps), inspect the appid / azp claim — that's the client app's ID.
---
Troubleshooting
| Symptom | Likely cause | Fix |
| --- | --- | --- |
| 401 Unauthorized | Wrong audience, expired token, or wrong tenant authority | Verify scope is api://1af74f73-c0b1-4df8-9b83-0e4cb964946e/.default and authority points at tenant 354619d1-eb70-4490-b9be-08842a4c270e |
| 403 Forbidden / missing roles claim | Admin consent not granted, or permission added as Delegated instead of Application | Re-check Part 2 (Application permissions) and Part 3 (green "Granted") |
| AADSTS70011: invalid scope | Requested a role name instead of .default | Request api://1af74f73-c0b1-4df8-9b83-0e4cb964946e/.default |
| invalid_client secret error | Secret expired or mistyped | Generate a new secret (Part 4) and update the client config |
| Token has scp but no roles | App is using the delegated/user flow | Switch to AcquireTokenForClient (client credentials) |
---
Related
- Connecting a .NET 8 Service to the Employee API — the client-side .NET 8 implementation (developer side)
- Add User to Vantagepoint
- Employee API — Project Knowledge Base (see
03. Resources/Project Knowledge Base/EmployeeAPI)
Comments
0 comments
Please sign in to leave a comment.