vishalsachdev/claude-skills

entra-token-verification

Verify a Microsoft Entra ID (Azure AD) access token server-side and derive identity from its claims.

First seen Aug 7, 2026

Installation

$ npx skills add vishalsachdev/claude-skills --skill entra-token-verification

Summary

  • Verify a Microsoft Entra ID (Azure AD) access token server-side and derive identity from its claims.
  • Use when adding or reviewing Entra/Azure AD JWT verification, when a verifier accepts tokens it should reject (or rejects tokens it should accept), or when binding a verified token to an identity record (GUID/oid comparison, netid derivation).
  • Covers the v2-issuer/audience trap, the ID-token-as-access-token trap, the missing-email-claim trap, and case-folding hazards in identity comparison.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from vishalsachdev/claude-skills · top by installs.

npx skills add vishalsachdev/claude-skills

Browse all from vishalsachdev/claude-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 5,900 B
  • docs SUMMARY.md 526 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 5 installs

SKILL.md

Entra Token Verification

Tier: spec anchored to live code (second consumer adapted the original — the invariants below are the value, not a shared package; regenerate the implementation per consumer, per CONTRIBUTING.md).

Verifying an Entra access token server-side (via jose + the tenant JWKS endpoint) is easy to get subtly, silently wrong: a misconfigured issuer/audience check passes v1 tokens that should be rejected, or a verifier that never checks for scp/roles accepts an ID token as if it were an access token. These are the constraints that keep recurring across consumers — freeze them, regenerate the verification code itself.

The constraints

  1. accessTokenAcceptedVersion must be 2. The v2-issuer check

(https://login.microsoftonline.com/<tenant>/v2.0) rejects every token if the app registration still issues v1 tokens — v1 tokens carry iss https://sts.windows.net/<tenant>/, which never matches. verify: az ad app show --id <appId> --query api.requestedAccessTokenVersion expect: 2 checked: 2026-08-01

  1. Audience must accept BOTH the bare clientId and api://<clientId>. v2 tokens

carry either form depending on how the scope was requested (api://<clientId>/.default vs. <clientId>/.default); accepting only one silently rejects the other. verify: decode a real token's aud claim (base64url-decode the JWT's middle segment) and confirm which form the tenant actually issues for this app. checked: 2026-08-01 (both quick and mykai-portal issue the bare-clientId form; accept both anyway — do not narrow to what's currently observed).

  1. Reject tokens carrying neither scp nor roles. An access-token verifier that

only checks signature/issuer/audience will also accept a valid ID token presented as an access token — ID tokens pass every one of those checks but were never scoped for API access. Real access tokens carry scp (delegated permissions) or roles (app permissions); ID tokens carry neither. verify: acquire an ID token deliberately (e.g. via the auth-code flow's id_token response field) and confirm the verifier throws on it. checked: 2026-08-01

  1. The email claim is frequently ABSENT from Entra v2 access tokens. Identity must

be derived from preferred_username (the UPN), not email. This is a live failure mode, not a theoretical one. verify: decode a real access token's payload and check for the email key. checked: 2026-06 (mindforum production failure — verifier assumed email present, broke for real users whose tokens omitted it).

  1. GUID/oid comparison must be case-insensitive. GUIDs are case-insensitive per

RFC 4122; a case-sensitive compare against an append-only identity ledger (e.g. a provisioning registry's history) permanently locks out a legitimate user the moment Entra or any client normalizes casing differently than the stored record. verify: compare a stored oid and a live token's oid with .toLowerCase() on both sides before ===; confirm a differently-cased-but-equal pair matches. checked: 2026-08-01 (mykai portal review cycle, identity.mjs)

  1. Case-fold AFTER rejecting non-ASCII, never before. U+212A KELVIN SIGN lowercases

to ASCII k under .toLowerCase(), so a crafted non-ASCII UPN/netid can collide with a distinct ASCII identifier once folded. Reject any non-printable-ASCII input (^[\x20-\x7E]+$) before calling .toLowerCase() on anything used for identity comparison — this removes the whole confusable-codepoint class rather than trying to enumerate it. verify: feed [email protected] through the netid/identity comparison path and confirm it is rejected as non-ASCII, not folded and matched. checked: 2026-08-01 (mykai portal review cycle, identity.mjs)

Failure modes if skipped

  • Skip (1) or (2): the verifier throws on legitimate tokens (loud, but wastes a debugging

session per consumer since the error is a generic issuer/audience mismatch).

  • Skip (3): silent privilege escalation — an ID token grants access it was never scoped for.
  • Skip (4): silent identity-resolution failure for a subset of real users (mindforum: this

shipped to production before being caught).

  • Skip (5) or (6): silent identity confusion — either locks out a legitimate user forever

(case-sensitive GUID compare against an append-only ledger) or lets one identifier impersonate another (Kelvin-sign collision).

Anchors

  • ~/code/quick/src/auth/entra.ts — original, TypeScript, jose-based verification

(verifyEntraToken, mapClaims). Constraints 1-4 live here as inline comments at the point they matter.

  • ~/code/mykai/portal/lib/entra.mjs — second consumer, direct JS port

of the above (same constraints 1-4, unchanged).

  • ~/code/mykai/portal/lib/identity.mjs — second consumer's hardening

beyond the port: constraints 5-6 (bindIdentity, netidFromUpn), plus fail-closed handling of malformed/missing history and epoch-scoped oid binding (a provisioning- registry-specific invariant, not a general Entra constraint — read the file's own comments if adapting a similar ledger).

Checked: 2026-08-01.

Related

  • [../CONTRIBUTING.md](../CONTRIBUTING.md) — why this is a spec, not extracted code