From 2846cbbcb4cf2ce6ebeca4d584d57f7807accb0d Mon Sep 17 00:00:00 2001 From: Corina Gum <14900841+corinagum@users.noreply.github.com> Date: Wed, 20 May 2026 14:16:47 -0700 Subject: [PATCH] Add Trust Model essentials page Documents the layered authentication model: the SDK validates inbound JWTs at the HTTP boundary, downstream handlers operate on already-validated tokens via typed accessors, and bots are responsible for authenticating custom surfaces (MCP, callbacks, webhooks). Resolves the documentation gap raised by security finding #6, which flagged the JsonWebToken accessor class for not performing signature verification. Verification is intentionally separated from the accessor; this page makes the architectural invariant explicit for SDK consumers. --- .../app-configuration/trust-model.mdx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 teams.md/src/pages/templates/essentials/app-configuration/trust-model.mdx diff --git a/teams.md/src/pages/templates/essentials/app-configuration/trust-model.mdx b/teams.md/src/pages/templates/essentials/app-configuration/trust-model.mdx new file mode 100644 index 000000000..f8bcb3892 --- /dev/null +++ b/teams.md/src/pages/templates/essentials/app-configuration/trust-model.mdx @@ -0,0 +1,71 @@ +--- +sidebar_position: 2 +title: Trust Model +summary: How the Teams SDK validates inbound tokens and what you can rely on downstream +languages: ['typescript','python','csharp'] +--- + +# Trust Model + +The Teams SDK enforces a layered authentication model. Inbound JSON Web Tokens are validated once, at the HTTP boundary, before any handler in your application sees them. Everything downstream — your activity handlers, the parsed token accessor on the context, and any custom routes that opt into the same policy — operates on tokens that have already passed signature, issuer, audience, and expiry checks. + +This page describes where validation happens, what you can trust afterwards, and how to extend the model when you add your own protected surfaces. + +## What the SDK validates automatically + +For every inbound request on `/api/messages`, the SDK validates the bearer token before invoking any activity handler. Validation includes: + +- **Signature verification** against the authoritative JSON Web Key Set (JWKS). The signing keys are discovered from the OpenID Configuration endpoint for the configured cloud environment, so key rotation requires no action from you. +- **Issuer check** against the set of expected issuers for your cloud (commercial, US Government, DoD, or China). +- **Audience check** against your bot's application identifier. +- **Lifetime check** with default clock skew tolerance. +- **Signing algorithm check** — only `RS256` is accepted. + +Requests that fail any of these checks are rejected with HTTP 401 before reaching your code. If your application observes an inbound activity at all, the underlying token has been verified. + +## What you can rely on downstream + +Once the request is past the validator, the SDK exposes selected token claims through typed accessors on the activity context. The accessor classes (`JsonWebToken` in each SDK) are payload views over an already-validated token — they perform no validation of their own. You can read fields like the tenant ID, application ID, service URL, and expiration directly without re-checking the token. + +The same pattern applies to tokens the SDK acquires on your behalf — for example, when MSAL returns an access token for an outbound API call, or when an OAuth user-token exchange completes. Those tokens originate from Microsoft identity infrastructure and are wrapped in the same accessor type so your code reads them with a consistent API. + +## Adding custom authentication to your own surfaces + +If your bot exposes additional HTTP surfaces beyond the default Teams activity endpoint — such as a Model Context Protocol server, a callback endpoint for an external system, or a webhook handler — you are responsible for authenticating those requests yourself. The SDK provides hooks that let you reuse the same trust posture. + +For example, the MCP plugin accepts an authentication callback that runs before each inbound MCP request. Returning `false` (or throwing) rejects the request with HTTP 401 before the handler runs: + +```typescript +new McpPlugin({ + requireAuth: (req) => req.headers.authorization === `Bearer ${process.env.MCP_TOKEN}`, +}); +``` + +```python +async def require_auth(request: Request) -> bool: + return request.headers.get("authorization") == f"Bearer {os.environ['MCP_TOKEN']}" + +McpServerPlugin(name="my-mcp", require_auth=require_auth) +``` + +```csharp +builder.AddTeamsMcp(options => +{ + options.RequireAuth = ctx => + Task.FromResult(ctx.Request.Headers.Authorization.ToString() == $"Bearer {Environment.GetEnvironmentVariable("MCP_TOKEN")}"); +}); +``` + +For custom routes registered directly against the SDK's HTTP adapter, apply your own middleware. The Teams activity validator can be reused — see the [App Authentication](/teams/app-authentication) guide for how to compose validators. + +## What not to do + +The accessor types described above are not safe to construct against arbitrary input. They expose claims from a parsed token; they do not check whether the token was signed by a party you trust. + +Do not: + +- Read a token from an untrusted HTTP request and construct a `JsonWebToken` from it as a basis for any authorization decision. +- Pass user-supplied tokens directly to downstream services without first running them through the SDK's validator (or your own equivalent). +- Disable the SDK's automatic validation on `/api/messages`. The default-deny posture relies on it. + +If you need to validate a token outside the activity pipeline, use the validator components directly rather than the accessor. Each SDK exposes a token validator that performs the full signature-verification flow against the same JWKS endpoint the activity pipeline uses.