Compare commits
2 Commits
main
...
feat/entra-scope
| Author | SHA1 | Date | |
|---|---|---|---|
| ed42ab7586 | |||
| 0f3f3ce5ad |
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user