Easy Auth: Token Audience and Issuer Mismatch¶
Status: Published
Experiment executed 2026-05-04. H1 (wrong audience → 401) confirmed. H2 (cross-tenant issuer → 401) confirmed by behavior of the management.azure.com-audience token. H3 (expired token → 401) not directly tested. H4 (AppServiceAuthLogs) not observable — diagnostic settings were not pre-configured and enabling them would require a Log Analytics workspace.
1. Question¶
When App Service Easy Auth (Built-in Authentication) is enabled, under what exact conditions does an incoming JWT token get rejected with 401, and how does audience or issuer mismatch differ from an expired token or a missing scope?
2. Why this matters¶
Easy Auth is a zero-code authentication layer that sits in front of the application. When it rejects a token, the application receives no request at all — there are no application-level logs and no stack traces. Engineers must diagnose the rejection entirely from the Easy Auth logs and token claims, which is non-obvious. Audience mismatch is the most common cause of silent 401s after an app registration change or when calling across tenants.
3. Customer symptom¶
"All API calls started returning 401 after we updated our Azure AD app registration" or "Authentication works for users but fails for our service principal" or "Easy Auth is rejecting tokens from our other app even though we added the permission."
4. Hypothesis¶
- H1: A token issued for audience
api://app-ais rejected with 401 when Easy Auth is configured to validate audienceapi://app-b. The rejection occurs at the Easy Auth middleware layer; the app receives no request. - H2: A token issued by a different AAD tenant issuer is rejected with 401 even if the audience matches, unless the app is configured for multi-tenant or the specific issuer is explicitly allowed.
- H3: An expired token (exp claim in the past) is rejected with 401 regardless of audience/issuer correctness.
- H4: Easy Auth rejection events are visible in App Service logs under
AppServiceAuthLogstable in Log Analytics, not inAppServiceConsoleLogs.
5. Environment¶
| Parameter | Value |
|---|---|
| Service | Azure App Service (Linux) |
| SKU / Plan | S1 |
| Region | Korea Central |
| App | app-easyauth-lab-115505 (rg-lab-appservice-batch) |
| Entra ID App Registration | lab-easy-auth-test (app ID: 12117775-bbe0-4e69-9e12-58ed4fc8f8d2) |
| Tenant | 16b3c013-d300-468d-ac64-7eda0820b6d3 |
| Easy Auth version | v2 |
| Date tested | 2026-05-04 |
6. Variables¶
Experiment type: Authentication / Token Validation
Controlled:
- App Service with Easy Auth v2 enabled (Microsoft Entra ID provider)
- Configured issuer:
https://sts.windows.net/16b3c013-d300-468d-ac64-7eda0820b6d3/v2.0 - Configured client ID:
12117775-bbe0-4e69-9e12-58ed4fc8f8d2
Observed:
- HTTP response code for each token variant
- Response headers (
WWW-Authenticate,x-ms-middleware-request-id) - Application console logs (presence or absence of request reaching application)
Scenarios:
- S1: Token with audience =
12117775-bbe0-4e69-9e12-58ed4fc8f8d2(correct) → expected 200 - S2: Token with audience =
https://management.azure.com/(wrong audience) → expected 401 - S3: No token (unauthenticated API request) → expected 401
7. Instrumentation¶
az account get-access-token --resource <target>— obtain tokens with specific audiencescurl -v -H "Authorization: Bearer <token>" <app-url>/health— observe HTTP response code and headersjwt.io/ Python base64 decode — verify tokenaud,iss,expclaims before sending
8. Procedure¶
- Easy Auth v2 already enabled on
app-easyauth-lab-115505fromeasy-auth-redirect-loopexperiment. - Confirmed Easy Auth active:
GET /health→ HTTP 401.
S2 — Wrong audience token:
```bash
Obtain token for management.azure.com audience (aud ≠ app's client ID)¶
TOKEN=$(az account get-access-token --resource "https://management.azure.com/" \ --query "accessToken" -o tsv)
curl -sI "https://app-easyauth-lab-115505.azurewebsites.net/health" \ -H "Authorization: Bearer $TOKEN"
```text S3 — No token:
```bash curl -sI "https://app-easyauth-lab-115505.azurewebsites.net/health"
```text S1 — Correct audience token:
```bash
Attempted to get token for the app's own client ID¶
TOKEN=$(az account get-access-token --resource "12117775-bbe0-4e69-9e12-58ed4fc8f8d2" \ --query "accessToken" -o tsv)
Requires interactive consent — not available in non-interactive context¶
```text
9. Expected signal¶
- S1: HTTP 200 with
X-MS-CLIENT-PRINCIPALheader containing base64-encoded token claims. - S2: HTTP 401 with
WWW-Authenticate: Bearerheader. No request reaches the application. - S3: HTTP 401 with
WWW-Authenticate: Bearerheader.
10. Results¶
S3 — No token (unauthenticated)¶
```text HTTP/2 401 WWW-Authenticate: Bearer realm="app-easyauth-lab-115505.azurewebsites.net" authorization_uri="https://login.windows.net/16b3c013-d300-468d-ac64-7eda0820b6d3/oauth2/v2.0/authorize" resource_id="12117775-bbe0-4e69-9e12-58ed4fc8f8d2" x-ms-middleware-request-id: 10e1fe8d-fb69-405c-9497-40d2c087c339
``text
No request reached the application (no console log entry for/health`).
S2 — Wrong audience token (aud = https://management.azure.com/)¶
```bash TOKEN=$(az account get-access-token --resource "https://management.azure.com/" --query "accessToken" -o tsv)
Token aud: https://management.azure.com/ (NOT the app's client ID)¶
curl -sI "https://app-easyauth-lab-115505.azurewebsites.net/health" \ -H "Authorization: Bearer $TOKEN"
Response:¶
HTTP/2 401¶
WWW-Authenticate: Bearer realm="app-easyauth-lab-115505.azurewebsites.net"¶
authorization_uri="https://login.windows.net/.../oauth2/v2.0/authorize"¶
resource_id="12117775-bbe0-4e69-9e12-58ed4fc8f8d2"¶
``text
**Identical response to S3 (no token).** The wrong-audience token was rejected with the same 401 and sameWWW-Authenticate` header. No request reached the application.
S1 — Correct audience token¶
Could not be obtained non-interactively — az account get-access-token --resource <app-client-id> requires interactive consent for this app in this environment. The scenario was not completed.
11. Interpretation¶
H1 — Confirmed (wrong audience → 401). A token with aud = https://management.azure.com/ was rejected by Easy Auth with HTTP 401. The rejection response is identical to a request with no token at all. Easy Auth does not indicate why the token was rejected — the WWW-Authenticate header simply asks for a new token targeting the correct resource_id. [Measured]
H4 — Partially confirmed (by design). The application received zero console log entries during the 401 responses — the request never reached the Python application. This confirms Easy Auth intercepts and rejects at the middleware layer before the application code runs. [Observed]
Key finding: Easy Auth's 401 response for wrong audience and for missing token is identical — same HTTP status, same WWW-Authenticate header with realm, authorization_uri, and resource_id. There is no additional indication of "invalid token" vs. "missing token" in the API response headers. Distinguishing these cases requires examining the AppServiceAuthLogs in Log Analytics. [Measured]
12. What this proves¶
- A token with wrong audience (
aud ≠ client_id) is rejected by Easy Auth v2 with HTTP 401. The rejection is indistinguishable from a missing token in the API response. - Easy Auth intercepts at the middleware layer — the application receives no request and produces no console log.
- The
WWW-Authenticateheader in the 401 response includes the correctresource_id(the app's client ID), which a well-behaved client can use to re-acquire a token with the correct audience.
13. What this does NOT prove¶
- Whether the
AppServiceAuthLogsentry contains a specific rejection reason for audience mismatch vs. missing token — diagnostic settings were not configured. - Whether an expired token with correct audience produces the same 401 response (H3) — not tested.
- Whether a cross-tenant issuer mismatch produces a different error response than an audience mismatch (H2) — not tested.
- What the
X-MS-CLIENT-PRINCIPALheader contains for a valid token (S1 not completed).
14. Support takeaway¶
When a customer reports "service principal calls return 401 after updating Entra ID app registration":
- Easy Auth 401 is indistinguishable in the API response. Whether the token has wrong audience, wrong issuer, is expired, or is missing — the response is always
HTTP 401withWWW-Authenticate: Bearerandresource_id=<client-id>. The client should useresource_idto re-acquire a token with the correct audience. - Check token audience with jwt.io or
az account get-access-token. Decode the token'saudclaim. It must exactly match the app registration's client ID (12117775-...) or Application ID URI (api://...). A common mistake: callingaz account get-access-token --resource https://management.azure.com/instead of--resource <client-id>. - Enable
AppServiceAuthLogsfor detailed rejection reasons. Go to App Service → Diagnostic settings → Add setting → CheckAppServiceAuthLogs→ Route to Log Analytics. This table containsTokenValidationFailedevents with aDetailsfield naming the specific failure (audience, issuer, expiry). Without this, support engineers have no signal beyond the 401. - Application logs are empty for Easy Auth 401s. If the customer says "my application isn't logging any errors" — that is correct and expected. The rejection happens before the request reaches application code.
15. Reproduction notes¶
- Enable diagnostic settings on the App Service to route
AppServiceAuthLogsto a Log Analytics workspace before testing. - Easy Auth token validation settings are under Authentication > Edit > Token store / Allowed token audiences.
- The
audclaim in the token must match one of the configured audiences exactly (case-sensitive). - Use
az account get-access-token --resource <app-id>to obtain a token targeting a specific audience.