Repo: microsoft/Agents (language-agnostic parent)
Children: Agents-for-js, Agents-for-python, Agents-for-net (linked as sub-issues)
Summary
Make inbound JWT Bearer-token validation consistent across all three Microsoft 365 Agents SDKs
and close a cross-tenant authentication gap by:
- Adding a tenant-binding cross-check (the required, consistent change): when the token's
iss
is a recognized Entra issuer that embeds a tenant GUID and the token carries a tid (tenant id)
claim, that tid claim must equal the issuer tenant. (A token without a tid claim is not
rejected on that basis — the cross-check is simply skipped.)
- Making issuer (
iss) allow-list validation available as an opt-in, per-sample/per-app option
— mirroring how the .NET SDK already exposes ValidIssuers. This is not mandatory,
SDK-enforced behavior.
.NET already exposes the opt-in issuer allow-list (ValidIssuers) that JS/Python mirror, and is the
reference for issuer handling. All three SDKs, including .NET, add the explicit tid ↔ issuer
tenant cross-check; for .NET that is the only change (its issuer handling is unchanged). This is a
focused change to the inbound validation path only — not a broader auth refactor.
Motivation
Security — cross-tenant bypass
Entra ID signing keys are shared across all tenants within a cloud. Therefore validating only the
signature + audience (which two of the three SDKs do today) means a signature-valid token minted
for a different tenant, but carrying a matching aud (e.g. for a multi-tenant app registration),
can be accepted. Binding the issuer's tenant to the tid claim prevents a token issued for tenant A
from being accepted as tenant B. (Optional issuer allow-list validation can further narrow accepted
issuers, but is offered as an opt-in per-sample option rather than mandatory enforcement.)
Consistency
Each SDK currently enforces a different subset of checks, so the security posture depends on which
language an agent is written in. This parent tracks bringing all three to the same baseline.
Background — current state (as of this writing)
| Check |
Agents-for-js |
Agents-for-python |
Agents-for-net |
| Signature (RS256, JWKS) |
✅ |
✅ |
✅ |
Audience (aud → clientId) |
✅ |
✅ |
✅ |
Issuer (iss) allow-list |
❌ not available |
❌ used unverified to pick JWKS |
✅ opt-in ValidIssuers |
| Signing-key ↔ issuer binding |
❌ |
❌ |
✅ EnableAadSigningKeyIssuerValidation() |
Explicit tid claim ↔ iss tenant |
❌ |
❌ |
⚠️ via signing-key binding (no explicit claim check) |
| Issuer config location |
SDK middleware |
SDK validator |
per-sample app config (by design) |
Notes:
- JS:
packages/agents-hosting/src/auth/jwt-middleware.ts (verifyToken) validates audience and
signature; jwt.verify is not given an issuer option and never reads tid.
- Python:
microsoft-agents-hosting-core/.../authorization/jwt_token_validator.py
(validate_token) validates audience and signature; iss is only used unverified to choose the
JWKS URI; tid is never read.
- .NET (issuer handling unchanged; adds
tid cross-check): enforces issuer via opt-in
ValidIssuers and binds the signing key to the issuer tenant via
EnableAadSigningKeyIssuerValidation() (Microsoft.IdentityModel.Validators). Configured per-sample
in AspNetExtensions.cs by design — this issuer-handling model is the pattern JS and Python
converge toward and is not changed. .NET adds only the explicit tid ↔ iss-tenant claim
check (its current signing-key binding does not literally compare the tid claim).
Scope
In scope
- Add a
tid ↔ iss-tenant GUID cross-check for recognized Entra issuers — the required,
consistent change in JS and Python.
- Offer optional, per-sample/per-app issuer (
iss) allow-list validation (opt-in), mirroring
.NET's existing ValidIssuers model — not mandatory, SDK-enforced behavior.
- Support public (
login.microsoftonline.com) and US Gov (login.microsoftonline.us) clouds,
plus the cloud-agnostic v1 issuer host (sts.windows.net).
- Handle multi-tenant / blueprint connections (
common / organizations) where the caller's
tenant is unknown at config time: accept any canonical Entra issuer for the configured cloud, then
bind it to the token's tid.
- Consistent failure semantics: validation failure → 401 with a non-leaky error message.
- Unit tests, including cross-tenant negative tests.
Out of scope
- Changing the .NET SDK's issuer handling — the opt-in
ValidIssuers allow-list and
EnableAadSigningKeyIssuerValidation() signing-key binding remain as-is (no consolidation of
AspNetExtensions.cs, no change to issuer validation). .NET only adds the explicit tid
cross-check.
- Making issuer allow-list validation mandatory / on-by-default in any SDK.
- Broader authentication/authorization refactors (connection manager, OBO, sidecar provider, etc.).
- Changing audience-matching or signing-key fetching behavior beyond what
tid binding needs.
- New configuration surface beyond what already exists (tenant id, authority, issuers).
Required behavior (the tid binding applies to all three SDKs; issuer handling differs per SDK)
tid ↔ issuer binding (required, all SDKs). If iss is a recognized Entra issuer with a GUID
tenant and the token carries a tid claim, require tid to equal that tenant GUID
(case-insensitive); otherwise reject. If the token has no tid claim, skip the cross-check (do
not reject on that basis — signature/audience still apply). Non-Entra issuers (e.g. Azure Bot
Service api.botframework.*, which carry no tid) are likewise skipped. In .NET this is the
only change; JS and Python add it alongside the recognition logic below.
- Canonical Entra issuer recognition. Recognize:
- v1:
https://sts.windows.net/{tenantGuid}/ (cloud-agnostic)
- v2 public:
https://login.microsoftonline.com/{tenantGuid}/v2.0
- v2 gov:
https://login.microsoftonline.us/{tenantGuid}/v2.0
Only GUID tenants are bindable (domain-alias issuers cannot be compared to the GUID tid and
are left unbound for compatibility).
- Issuer allow-list (optional, opt-in). When enabled by a sample/app, build the accepted-issuer
set from the matched connection's configured issuers (or a tenant-scoped default) unioned with
the always-trusted Microsoft first-party issuers for the configured cloud, compared
case-insensitively. When not enabled, issuer allow-list checks are skipped (signature +
audience + tid binding still apply).
- Multi-tenant connections. If the connection tenant is
common/organizations, accept any
canonical Entra issuer for the configured cloud (signature + audience + tid binding still apply).
- Failure → 401, message must not leak internal error codes/help links.
Expected consistency matrix (target end state)
| Capability |
JS |
Python |
.NET |
tid ↔ iss tenant binding |
✅ explicit claim check (added) |
✅ explicit claim check (added) |
✅ explicit claim check (added) |
| Issuer allow-list available (optional, opt-in) |
✅ |
✅ |
✅ existing ValidIssuers (unchanged) |
| Public + US Gov clouds |
✅ |
✅ |
✅ |
Multi-tenant (common/organizations) |
✅ |
✅ |
✅ |
| Bot Framework issuers skipped from binding |
✅ |
✅ |
✅ |
| Cross-tenant negative tests |
✅ |
✅ |
✅ |
Security considerations
- Closes a cross-tenant token-substitution/bypass for multi-tenant app registrations via the required
tid ↔ issuer binding.
- The
tid binding only strengthens (never loosens) validation; it applies only when the token has
a tid claim and iss is a GUID-tenant Entra issuer. Tokens without a tid claim, non-GUID
(domain-alias) issuers, and non-Entra issuers degrade to the existing signature + audience checks.
- Optional issuer allow-list comparison is case-insensitive to tolerate operator-provided GUID casing
while still rejecting unrelated tenants when enabled.
Testing strategy
- ✅ Same-tenant token: accepted.
- ❌ Signature-valid token from a different tenant with matching
aud: rejected (tid binding).
- ✅/❌ Public vs US Gov issuer matched to the configured cloud.
- ✅ Multi-tenant connection accepts a canonical Entra issuer and binds to
tid.
- ✅ Azure Bot Service issuer (no
tid) still accepted (binding skipped).
- ✅ Token with no
tid claim accepted even when the issuer embeds a GUID tenant (binding skipped).
- ❌ Token whose
tid is present but does not match the issuer tenant GUID: rejected.
- ✅ Optional issuer allow-list: when enabled, unlisted issuer rejected; when disabled, skipped.
Acceptance criteria
References
- Microsoft.IdentityModel
EnableAadSigningKeyIssuerValidation / AadIssuerValidator (tenant binding).
Child issues
Summary
Make inbound JWT Bearer-token validation consistent across all three Microsoft 365 Agents SDKs
and close a cross-tenant authentication gap by:
issis a recognized Entra issuer that embeds a tenant GUID and the token carries a
tid(tenant id)claim, that
tidclaim must equal the issuer tenant. (A token without atidclaim is notrejected on that basis — the cross-check is simply skipped.)
iss) allow-list validation available as an opt-in, per-sample/per-app option— mirroring how the .NET SDK already exposes
ValidIssuers. This is not mandatory,SDK-enforced behavior.
.NET already exposes the opt-in issuer allow-list (
ValidIssuers) that JS/Python mirror, and is thereference for issuer handling. All three SDKs, including .NET, add the explicit
tid↔ issuertenant cross-check; for .NET that is the only change (its issuer handling is unchanged). This is a
focused change to the inbound validation path only — not a broader auth refactor.
Motivation
Security — cross-tenant bypass
Entra ID signing keys are shared across all tenants within a cloud. Therefore validating only the
signature + audience (which two of the three SDKs do today) means a signature-valid token minted
for a different tenant, but carrying a matching
aud(e.g. for a multi-tenant app registration),can be accepted. Binding the issuer's tenant to the
tidclaim prevents a token issued for tenant Afrom being accepted as tenant B. (Optional issuer allow-list validation can further narrow accepted
issuers, but is offered as an opt-in per-sample option rather than mandatory enforcement.)
Consistency
Each SDK currently enforces a different subset of checks, so the security posture depends on which
language an agent is written in. This parent tracks bringing all three to the same baseline.
Background — current state (as of this writing)
aud→ clientId)iss) allow-listValidIssuersEnableAadSigningKeyIssuerValidation()tidclaim ↔isstenantNotes:
packages/agents-hosting/src/auth/jwt-middleware.ts(verifyToken) validates audience andsignature;
jwt.verifyis not given anissueroption and never readstid.microsoft-agents-hosting-core/.../authorization/jwt_token_validator.py(
validate_token) validates audience and signature;issis only used unverified to choose theJWKS URI;
tidis never read.tidcross-check): enforces issuer via opt-inValidIssuersand binds the signing key to the issuer tenant viaEnableAadSigningKeyIssuerValidation()(Microsoft.IdentityModel.Validators). Configured per-samplein
AspNetExtensions.csby design — this issuer-handling model is the pattern JS and Pythonconverge toward and is not changed. .NET adds only the explicit
tid↔iss-tenant claimcheck (its current signing-key binding does not literally compare the
tidclaim).Scope
In scope
tid↔iss-tenant GUID cross-check for recognized Entra issuers — the required,consistent change in JS and Python.
iss) allow-list validation (opt-in), mirroring.NET's existing
ValidIssuersmodel — not mandatory, SDK-enforced behavior.login.microsoftonline.com) and US Gov (login.microsoftonline.us) clouds,plus the cloud-agnostic v1 issuer host (
sts.windows.net).common/organizations) where the caller'stenant is unknown at config time: accept any canonical Entra issuer for the configured cloud, then
bind it to the token's
tid.Out of scope
ValidIssuersallow-list andEnableAadSigningKeyIssuerValidation()signing-key binding remain as-is (no consolidation ofAspNetExtensions.cs, no change to issuer validation). .NET only adds the explicittidcross-check.
tidbinding needs.Required behavior (the
tidbinding applies to all three SDKs; issuer handling differs per SDK)tid↔ issuer binding (required, all SDKs). Ifissis a recognized Entra issuer with a GUIDtenant and the token carries a
tidclaim, requiretidto equal that tenant GUID(case-insensitive); otherwise reject. If the token has no
tidclaim, skip the cross-check (donot reject on that basis — signature/audience still apply). Non-Entra issuers (e.g. Azure Bot
Service
api.botframework.*, which carry notid) are likewise skipped. In .NET this is theonly change; JS and Python add it alongside the recognition logic below.
https://sts.windows.net/{tenantGuid}/(cloud-agnostic)https://login.microsoftonline.com/{tenantGuid}/v2.0https://login.microsoftonline.us/{tenantGuid}/v2.0Only GUID tenants are bindable (domain-alias issuers cannot be compared to the GUID
tidandare left unbound for compatibility).
set from the matched connection's configured issuers (or a tenant-scoped default) unioned with
the always-trusted Microsoft first-party issuers for the configured cloud, compared
case-insensitively. When not enabled, issuer allow-list checks are skipped (signature +
audience +
tidbinding still apply).common/organizations, accept anycanonical Entra issuer for the configured cloud (signature + audience +
tidbinding still apply).Expected consistency matrix (target end state)
tid↔isstenant bindingValidIssuers(unchanged)common/organizations)Security considerations
tid↔ issuer binding.tidbinding only strengthens (never loosens) validation; it applies only when the token hasa
tidclaim andissis a GUID-tenant Entra issuer. Tokens without atidclaim, non-GUID(domain-alias) issuers, and non-Entra issuers degrade to the existing signature + audience checks.
while still rejecting unrelated tenants when enabled.
Testing strategy
aud: rejected (tidbinding).tid.tid) still accepted (binding skipped).tidclaim accepted even when the issuer embeds a GUID tenant (binding skipped).tidis present but does not match the issuer tenant GUID: rejected.Acceptance criteria
tidcross-check).tidbinding required everywhere; issuerallow-list opt-in; .NET issuer handling unchanged).
References
EnableAadSigningKeyIssuerValidation/AadIssuerValidator(tenant binding).Child issues
Agents-for-js: Add TID→Issuer cross-check in inbound JWT validation Agents-for-js#1152Agents-for-python: Add TID→Issuer cross-check in inbound JWT validation Agents-for-python#423Agents-for-net: Add TID→Issuer cross-check in inbound JWT validation Agents-for-net#881