Compare commits

...

2 Commits

3 changed files with 35 additions and 6 deletions
+7 -1
View File
@@ -116,7 +116,7 @@ a failed request with no response body and a console error naming the origin.
```
GET <bootstrap_url> # after interpolation
Authorization: Bearer <entra_id_token> # only if entra_sso=1 in manifest
Authorization: Bearer <entra_token> # only if entra_sso=1 in manifest
X-Claude-User-Agent: claude-<app>/<version> # always sent
```
@@ -138,6 +138,12 @@ With `entra_sso=1`, validate the JWT before trusting it:
| `exp` | Not expired. Libraries handle this; don't hand-roll it. |
| `oid` | The user's stable object ID. This is your lookup key — email (`upn`/`preferred_username`) can change, `oid` doesn't. |
If you set `entra_scope` in the [manifest](manifest.md#entra-sso), the Bearer
is an **access token**, not an ID token. Validate `aud` = your API's
Application ID URI (`api://<guid>`, not the client GUID) and check `scp`
contains the scope you defined. `iss`, `exp`, `oid`, and signature verification
are the same.
Signature verification needs Microsoft's JWKS
(`https://login.microsoftonline.com/<TENANT_ID>/discovery/v2.0/keys`). Use a
library — `jose` (Node), `PyJWT` + `cryptography` (Python), `Microsoft.IdentityModel.Tokens`
+19 -3
View File
@@ -44,9 +44,25 @@ the app in Entra as a single-tenant **Single-page application** with redirect
URI `https://pivot.claude.ai/msal-redirect.html`. You handle consent on your
own app — [consent](consent.md) covers the default app only.
`graph_client_id` is manifest-only — the add-in needs it to initialize NAA
*before* it can read extension attrs or call your bootstrap endpoint, so it
can't arrive through either of those layers.
**Send an access token instead of the ID token.** With `graph_client_id` alone
the add-in still sends an *ID token* to your bootstrap endpoint`aud` is your
app's GUID, but there's no `scp` claim. If your endpoint is a standard OAuth2
protected resource that validates `aud` + `scp`, or an RFC 8693 token-exchange
service, set `entra_scope=api://<your-app-guid>/<scope>` and the add-in
requests an *access token* for that scope instead. The Bearer it sends carries
`aud` = your API's App ID URI and `scp` = the granted scope. In Entra, on your
app registration: **Expose an API** (Application ID URI `api://<guid>`), add a
scope such as `access_as_user`, and grant the same app delegated permission to
it, then grant admin consent for the tenant. In the app manifest, set
`accessTokenAcceptedVersion: 2` so the issued token uses v2.0 claims
(`iss = login.microsoftonline.com/<tid>/v2.0`, `azp`, `preferred_username`);
leave it unset and you get v1.0 tokens, which your validator may reject.
`/.default` (requests all consented scopes) also works.
`entra_scope` requires `graph_client_id` — the build script enforces this. Both
are manifest-only: the add-in needs them to initialize NAA *before* it can read
extension attrs or call your bootstrap endpoint, so neither can arrive through
those layers. Leave `entra_scope` unset and the ID token is sent.
## Bootstrap endpoint
+9 -2
View File
@@ -53,13 +53,17 @@ const KEYS = {
pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
hint: "your Entra app registration's Application (client) ID — overrides the default multi-tenant app",
},
entra_scope: {
pattern: /^(api|https):\/\/\S+\/[\w.-]+$/i,
hint: "scope URI for your Entra-protected API, e.g. api://<your-app-guid>/.default — requires graph_client_id",
},
allow_1p: {
pattern: /^[01]$/,
hint: "1 allows Claude.ai OAuth alongside 3P (default: locked when other keys present)",
},
};
const NEEDS_ENTRA = ["aws_role_arn", "graph_client_id"];
const NEEDS_ENTRA = ["aws_role_arn", "graph_client_id", "entra_scope"];
async function main() {
const [out, ...pairs] = process.argv.slice(2);
@@ -89,7 +93,10 @@ async function main() {
const needsEntra = NEEDS_ENTRA.find((k) => params.has(k));
if (needsEntra && params.get("entra_sso") !== "1") {
throw new Error(`${needsEntra} requires entra_sso=1 (the add-in needs an Entra ID token to use it)`);
throw new Error(`${needsEntra} requires entra_sso=1 (the add-in needs an Entra token to use it)`);
}
if (params.has("entra_scope") && !params.has("graph_client_id")) {
throw new Error("entra_scope requires graph_client_id (the scope is requested as your own Entra app, not the default)");
}
// URLSearchParams joins with `&`; XML attribute values need it escaped.