返回 Skills 目录
samber/developer-platform-skills已通过检查

SKILL DETAIL

oauth2-provider-design

samber/developer-platform-skills/oauth2-provider-design

Design the OAuth2 authorization-server surface a B2B SaaS offers third-party apps - the OAuth 2.1 protocol baseline (PKCE for every client, no implicit or password grants, exact redirect matching), token TTL and refresh-rotation policy, scope taxonomy and granularity, consent-screen design with partial and incremental grants, client registration posture, and the tiered app-verification program. Use whenever the user mentions OAuth, "Sign in with X", access and refresh tokens, scopes, consent screens, PKCE, or third-party apps acting on a customer's behalf - even if they never say "OAuth provider". Issuer side only, not integrating against someone else's OAuth. Do NOT use for API-key design - use samber/developer-platform-skills@api-auth-key-management instead.

安装量 · 329查看来源

Installation

npx skills add https://github.com/samber/developer-platform-skills --skill oauth2-provider-design

技能文件

SKILL.md

最近同步 · 2026年10月23日

evals/evals.json›
{
  "skill_name": "oauth2-provider-design",
  "evals": [
    {
      "id": 1,
      "prompt": "I'm the platform lead at Tessaract Cloud, a B2B log-analytics SaaS. We're building \"Connect with Tessaract\" so third-party apps can pull a customer's dashboards, plus a `tsq` CLI our users run on headless build servers with no browser. Greenfield, nothing built yet. Our security review doc has to name the specification we conform to and legal wants the RFC number in writing. Separately, our frontend team wants the redirect URI to accept `https://*.preview.acme-partner.dev` so partner preview deployments keep working, and asked whether the access token can just ride in the query string for their SPA since the Authorization header is awkward in their fetch wrapper. Give me the grant list, the redirect rules, and what to write in the doc.",
      "expected_output": "A protocol-core design naming the four grants to implement (authorization_code + PKCE S256 for every client, client_credentials, refresh_token, device_authorization), exact redirect-URI matching with the localhost-port carve-out, a refusal of the query-string token, code single-use/10-minute limits, and a precise statement of what is actually citable as a ratified document.",
      "files": [],
      "expectations": [
        "States that RFC 9700 (BCP 240, January 2025) is final and ratified, and is the document the security doc can cite as published.",
        "States that the consolidated OAuth 2.1 document (draft-ietf-oauth-v2-1) is still a draft, and never describes OAuth 2.1 itself as a published RFC.",
        "Advises confirming the OAuth 2.1 draft's current publication status before citing it.",
        "Recommends the grant list authorization_code, client_credentials, refresh_token, and device_authorization (RFC 8628).",
        "Names the RFC 8628 device authorization grant specifically as the answer for the browserless `tsq` CLI.",
        "Requires PKCE with S256 for every client, confidential clients included, not only public ones.",
        "Excludes both the implicit grant and the resource-owner password credentials grant from the design.",
        "Rejects the `https://*.preview.acme-partner.dev` wildcard redirect and requires exact string matching of redirect URIs.",
        "Names the port number in native-app localhost redirects as the single standard exception to exact matching.",
        "Justifies exact matching by pattern-matching implementations being error-prone with attacks observed in the wild, per RFC 9700.",
        "Refuses the query-string access token and restricts bearer tokens to the Authorization header or the POST body.",
        "Specifies authorization codes as single-use with a maximum lifetime of 10 minutes."
      ]
    },
    {
      "id": 2,
      "prompt": "Northwind Signal, B2B customer-messaging SaaS. I need to write our token policy before the security team reviews it Friday. Three client kinds today: a React SPA our own customers embed in their admin panel, server-side integrations built by partners that hold a client secret, and a Python worker one large customer runs on their own infrastructure. Our infra lead wants every token SHA-256 hashed in the database and I agree with him. I've read that refresh tokens should rotate on every use and that presenting a used one should kill the session. Give me concrete TTLs and rotation rules per client kind, and tell me what the storage layer has to support.",
      "expected_output": "Per-client-kind TTL and rotation policy: ~1h access tokens, rotation with token-family reuse detection plus a short grace window as the default, a much harder refresh lifetime cap for the SPA, the confidential-client carve-out for the partner backends, and an explicit resolution of the hash-at-rest versus grace-window storage tension.",
      "files": [],
      "expectations": [
        "Sets access-token TTL at roughly 1 hour and identifies ~1 hour as the cross-provider convergence point rather than an invented number.",
        "Makes refresh-token rotation with token-family reuse detection the default posture.",
        "States that a reused refresh token revokes the entire token family, not only the token presented.",
        "Requires a reuse grace window from day one, with a starting value in the range of roughly 10 to 30 seconds.",
        "Justifies the grace window by legitimate concurrent refreshes (multi-tab SPAs, retries, reconnects) otherwise reading as attacks and logging real users out.",
        "Names the tension between hashing refresh tokens at rest and having to recognize a just-consumed token inside the grace window.",
        "Resolves that tension deliberately (for example by retaining the predecessor's hash with a validity timestamp) instead of abandoning hash-at-rest.",
        "Caps the SPA's refresh-token lifetime far harder than the others, citing something like a 24-hour SPA lifetime against ~90-day inactivity elsewhere.",
        "Treats the partner server-side clients as confidential clients whose client authentication already sender-constrains redemption, making non-rotating refresh tokens defensible there.",
        "States that OAuth 2.1 mandates sender-constrained or rotated refresh tokens specifically for public clients.",
        "Rules out an indefinitely reusable refresh token for the public SPA client entirely, rather than offering it as a weaker option.",
        "Ties JWT access-token TTL to the fact that a JWT cannot be revoked mid-lifetime the way an opaque database-backed token can."
      ]
    },
    {
      "id": 3,
      "prompt": "Kestrel HR, an HRIS for mid-market companies, about 450 customers. We store employee directory and time-off data only, no payments and no health records. We're opening a partner app directory in roughly eight weeks. Our new CISO wants DPoP sender-constrained tokens on everything before launch because, quoting him, \"bearer tokens are a solved problem and we should be ahead of the curve.\" We run an authorization server we wrote ourselves in Node. Our partners are mostly small ATS and payroll vendors using off-the-shelf OAuth client libraries. Is that the right call, and what do we ship instead if not?",
      "expected_output": "A recommendation against DPoP as the launch baseline for this platform, with rotation plus reuse detection shipped instead, the ecosystem-support evidence behind the call, the named conditions that would reverse it, and an explicit note that runtime cost is not the argument.",
      "files": [],
      "expectations": [
        "Recommends against making DPoP the launch baseline for this platform.",
        "Recommends rotation with token-family reuse detection as what ships instead.",
        "Cites the issuer-survey evidence that zero of 18 surveyed mainstream issuers with a working discovery document advertised DPoP support.",
        "Identifies absent client-library and ecosystem support, not performance, as the blocker.",
        "States that DPoP's runtime cost is small (roughly single-digit milliseconds per request), so performance is not the argument against it.",
        "Names the conditions that would promote DPoP: tokens guarding financial or health data, FAPI-adjacent sensitivity, or long-lived AI-agent tokens widening the theft blast radius.",
        "Applies those conditions to the stated data sensitivity (employee directory and time-off, no payments or health records) and concludes they are not met.",
        "Places DPoP in an advanced-hardening stage rather than in the protocol core.",
        "Acknowledges DPoP is production-viable at scale (for example Bluesky's mandatory deployment), so the objection is ecosystem readiness rather than feasibility.",
        "States the ranking is a default that flips if the build substrate ships DPoP as configuration rather than custom work.",
        "Never claims DPoP is required by OAuth 2.1."
      ]
    },
    {
      "id": 4,
      "prompt": "Orrery, B2B project-management SaaS. Today every OAuth app gets one scope, `full_access`, and roughly 120 apps hold it. We want to fix that. The plan: keep the string `full_access` but redefine it to mean read-only, then add `full_write` for the apps that need mutations. That way we don't have to touch the 120 existing registrations. We also have two token flavours, apps posting as themselves (we call those \"integrations\") and apps acting as a signed-in person, and we figured we'd sort out which scopes belong to which flavour after we see how people actually use them. Our API resources are `projects`, `tasks`, `comments`, and `timesheets`. Draft the taxonomy.",
      "expected_output": "A resource:action taxonomy with read/write split over the four existing resource nouns, an explicit refusal to redefine `full_access`, per-scope token-type binding decided now rather than later, and the broad catch-all scope removed rather than retained as a coarse tier.",
      "files": [],
      "expectations": [
        "Rejects redefining `full_access` and states scopes are additive-only, never repurposed.",
        "Explains that repurposing silently changes what already-granted consents authorize.",
        "Notes that scope-set changes invalidate refresh tokens and force re-consent, so the redefinition does not actually avoid touching the existing registrations.",
        "Rejects deferring the token-type binding and requires every scope bound to a token type (app identity versus user delegation) on day one.",
        "States the documented cost of deferring it: changing a scope's token-type binding later requires reauthorization from every user.",
        "Recommends `resource:action` naming as the default granularity rung.",
        "Requires a read/write split with read-only as the default.",
        "Names scopes after the same resource nouns the API already uses (`projects`, `tasks`, `comments`, `timesheets`).",
        "Treats the single broad catch-all scope as a deleted anti-pattern rather than a coarse tier worth keeping in the menu.",
        "Positions fine-grained per-resource permissions as the starved option, promoted only on named conditions such as admin approval policies, a compliance regime, or scopes reaching org-wide data.",
        "Warns that a migration to fine-grained permissions is lossy in edge-case coverage, so the coarser tier stays alive during the transition instead of a hard cutover.",
        "Accepts scope proliferation where resource types genuinely differ in sensitivity instead of collapsing them for tidiness."
      ]
    },
    {
      "id": 5,
      "prompt": "Marlowe Analytics. Our install funnel is bleeding: workspace admins reject roughly a third of install requests and we can't tell why. The current consent screen shows three grouped headers (\"Read access\", \"Write access\", \"Admin\") with the raw scope strings listed underneath each, and it's all-or-nothing, approve everything or cancel. Our VP of Product is presenting to the board Thursday and asked me for the industry figure on how much conversion an over-scoped consent screen costs you. I've seen 50% quoted somewhere. Redesign the screen and give me the number for the deck.",
      "expected_output": "A consent-screen redesign built on per-scope plain-language copy, partial grants with a server-side diff, and incremental authorization showing only the delta, plus an explicit refusal to supply the requested drop-off percentage and an explanation of why no such measured figure exists.",
      "files": [],
      "expectations": [
        "Refuses to supply a consent drop-off conversion figure as measured fact.",
        "States that no rigorous public study of consent-screen conversion exists and that circulating percentages trace to vendor blog posts rather than measurements.",
        "Addresses the specific 50% figure the user raised instead of silently omitting it.",
        "Argues the redesign on security and UX grounds rather than on a conversion number.",
        "Replaces the grouped buckets with one plain-language sentence per individual scope.",
        "Forbids showing the raw scope string to the user.",
        "Replaces all-or-nothing consent with support for partial grants.",
        "Requires the server to diff the granted scope set against the requested one on every authorization response.",
        "Warns that the granted set can differ from the requested set even when the user appears to approve everything, and that assuming they match ships a silent capability mismatch into every integrating app.",
        "Recommends incremental authorization, requesting a scope when the feature needs it rather than upfront.",
        "Requires the re-consent screen to show only the net-new scope rather than re-displaying everything already granted."
      ]
    },
    {
      "id": 6,
      "prompt": "Verity Ops. We're shipping a remote MCP server so agents can query our incident data, and we are the authorization server behind it. The MCP folks on my team want RFC 7591 dynamic client registration wide open at `/register` so any agent can self-register with no human in the loop. That's what the spec pointed at when we started and it's the only way agents discover us at runtime anyway. We'd validate the metadata they submit (client name, logo URI, redirect URIs) and rate-limit the endpoint hard. Sound design?",
      "expected_output": "A rejection of open DCR in favour of gated DCR behind an initial access token with untrusted-metadata handling, correcting the user's belief about the MCP spec's current position, plus manual portal registration with review as the default for every non-agent client.",
      "files": [],
      "expectations": [
        "Rejects open, ungated dynamic client registration outright rather than accepting it with mitigations.",
        "Recommends gated DCR instead: RFC 7591 behind an initial access token, rate-limited.",
        "Requires all submitted registration metadata to be treated as untrusted input.",
        "Warns that self-asserted metadata such as client name and logo can impersonate a legitimate app unless cryptographically attested, making an open endpoint a phishing vector.",
        "Cites the finding that a study of 119 OAuth-enabled remote MCP servers found DCR flaws in roughly 96-97% of them.",
        "States that the MCP specification moved DCR from SHOULD to MAY to deprecated across its revisions, correcting the user's belief that the spec points at open DCR.",
        "Confirms that an MCP/AI-agent ecosystem is the one client population that legitimately justifies DCR at all, since those clients cannot pre-register.",
        "States that rate limiting plus metadata validation alone do not make an open registration endpoint acceptable.",
        "Notes that no major B2B SaaS platform offers ungated public dynamic client registration.",
        "Names manual developer-portal registration with a review step as the default posture for every client population outside the agent case."
      ]
    },
    {
      "id": 7,
      "prompt": "Ledgerline, B2B spend-management SaaS. We're opening an integrations directory with about 60 apps at launch. The plan so far: one \"Verified\" badge shown on every listing, earned by filling in a security questionnaire once at submission, same bar for every app because we don't want to look like we play favourites. Some of these apps will read a customer's entire vendor list and every employee's expense reports, not just the installing admin's own records. Unverified apps get a warning banner on the install screen. Sign off on this or tell me what's wrong with it.",
      "expected_output": "A tiered verification program replacing the single uniform bar: identity verification as the floor, self-attestation as the middle rung, mandatory third-party assessment triggered by scopes reaching other users' data, three visually distinct signals instead of one badge, a real enforced unverified lane, and runtime subset-of-declared enforcement.",
      "files": [],
      "expectations": [
        "Rejects one uniform review bar and requires review depth scaled to data sensitivity.",
        "Names the bright line: the moment a scope reaches other users' data or PII rather than only the requesting user's own, mandatory independent third-party security assessment applies.",
        "Applies that line to the described apps (full vendor list, every employee's expense reports) and places them above the questionnaire tier.",
        "Rejects one 'Verified' badge conflating identity verification, self-attestation, and independent audit, and requires the signals to stay visually distinct on the listing surface.",
        "States that publisher identity verification proves continuity of identity, not quality.",
        "Recommends publisher identity verification (verified domain plus verified partner account, no fee) as the default rung for every listed app.",
        "Places self-attestation as the middle rung and states it is explicitly not independently verified by the platform.",
        "States that the developer, not the platform, pays the third-party assessor fee.",
        "Notes the full manual pentest tier costs roughly an order of magnitude more than the automated scan tier.",
        "Requires the assessment to recur on an annual / 12-month cadence rather than being one-time at submission.",
        "Rejects a warning banner as the entire unverified lane and requires a hard enforced ceiling, such as a lifetime cap on test users plus short-lived test tokens.",
        "Requires runtime enforcement that any requested scope is a subset of what was declared and approved at registration, because declarations drift."
      ]
    },
    {
      "id": 8,
      "prompt": "Chatterbox Systems, nine-year-old B2B SaaS. Live today: the implicit grant (our own dashboard uses it), resource-owner password credentials for an old iOS app that roughly 400 customers still run, redirect matching by prefix, refresh tokens that never expire and never rotate, about 3,100 registered third-party apps, and one scope tier. A large enterprise customer's security team has given us seven months to \"be OAuth 2.1 compliant\" or they churn. My instinct is one big cutover release. Tell me how to sequence this and how we prove compliance to their auditor.",
      "expected_output": "A staged retrofit sequence rather than a cutover, with compliance modelled as independently toggleable per-requirement gates, the banned grants and prefix matching removed first, the rotation rollout re-ranked against 3,100 live integrations, and the token-family relationship designed into newly issued grants.",
      "files": [],
      "expectations": [
        "Rejects the single big-cutover release in favour of a staged sequence.",
        "Gives the build order: protocol core first, then scope taxonomy and consent screen, then registration and verification, then advanced hardening.",
        "Recommends modelling compliance as separately named, independently toggleable boolean gates per requirement (PKCE enforcement, exact redirect matching, rotation, and so on) rather than a single 'OAuth 2.1 mode' flag.",
        "Justifies the per-requirement gates by auditability: a partial rollout can point at exactly which requirements are and are not enforced yet.",
        "Requires removing the implicit grant and the resource-owner password grant, migrating the dashboard and the iOS app to authorization_code with PKCE.",
        "Replaces prefix redirect matching with exact string matching.",
        "States that a non-rotating, never-expiring refresh token held by a public client is banned under OAuth 2.1, not merely a weaker option.",
        "Notes that 3,100 live integrations shift the effort ranking, so the rotation rollout is re-ranked against the retrofit scale instead of applying the greenfield default unexamined.",
        "Requires designing the refresh-token family relationship into newly issued grants, noting that retrofitting family grouping onto an already-issued ungrouped population is materially harder.",
        "Cites the forced-retrofit precedent: a 2025 OAuth-token campaign against a connected app hit more than 700 organizations and led a major platform to mandate PKCE and refresh-token rotation on all connected apps, with de-listing as the penalty.",
        "States that what the auditor gets shown is a discovery document plus a conformance pass verified by test, not an assertion of compliance."
      ]
    },
    {
      "id": 9,
      "prompt": "Palisade Grid. Launch of our authorization server is in five weeks and I need two things from you. First, the go/no-go checklist QA signs off before we ship. Second, the four or five dashboard metrics we watch afterwards, with the industry benchmark next to each one so the on-call engineer knows when a number is off. Our stack: refresh rotation with reuse detection, granular consent, an app review queue, and roughly 30 launch partners.",
      "expected_output": "A gate checklist verified by test (conformance pass, staging reuse-detection demo, concurrent-refresh test, partial-grant integration test, per-scope consent copy, tier classification and runtime subset enforcement), plus named trends to baseline from launch and an explicit refusal to attach industry benchmark numbers.",
      "files": [],
      "expectations": [
        "States that no industry thresholds exist for these metrics and declines to attach benchmark numbers to them.",
        "Prescribes baselining the metrics as trends from launch instead of comparing against an external benchmark.",
        "Names median app-review turnaround as a trend to baseline.",
        "Names the share of apps requesting scopes their traffic never uses as a trend to baseline.",
        "Names reuse-detection trip rate as a trend to baseline.",
        "States that reuse-detection trip rate is ambiguous, proxying both real attacks and a grace window set too tight.",
        "Requires a gate proving by test, not assertion, that the discovery document and a conformance pass show no implicit or password grant, PKCE rejected-if-absent for every client, and exact redirect matching.",
        "Requires a staging demonstration that a replayed refresh token revokes the whole family.",
        "Requires a concurrent-refresh test that passes without a false-positive logout.",
        "Requires a gate that every scope has per-scope plain-language consent copy and that a partial-grant integration test exercises the server-side diff.",
        "Requires gates that every scope is classified into a verification tier with a named review requirement, that runtime subset-of-approved enforcement is live, and that the unverified-lane cap is enforced rather than merely documented."
      ]
    },
    {
      "id": 10,
      "prompt": "Hollowpine, B2B knowledge-base SaaS sold per workspace. Our OAuth design assumes exactly one person clicks Allow: the individual who installs the app. We're moving upmarket and the last three enterprise deals all asked how their IT team controls which apps employees can connect. We also want to ship \"workspace bots\" that read and post as themselves with no human behind them. What changes in the design?",
      "expected_output": "A two-consent-surface design: end-user consent plus an org-admin approval and org-wide grant review surface built into the scope and consent work, an app-identity permission tier that only an admin can consent to, and per-scope token-type binding for the bot versus user case.",
      "files": [],
      "expectations": [
        "States that B2B org-owned workspaces have two consent surfaces, the end user's and an org admin's, not one.",
        "Requires an admin approval gate (approval queue or admin-approved apps) designed as a first-class part of the scope and consent work rather than bolted on when the first enterprise customer asks.",
        "Includes org-wide grant review as part of the admin surface.",
        "Introduces an app-identity permission tier for the workspace bots, distinct from user-delegated permissions.",
        "States that app-identity / application permissions can only ever be consented by an admin, never by an end user.",
        "Notes the protocol core, token lifecycle, and consent-copy rules stay identical to the B2C case, so only the approval topology and the app-identity tier change.",
        "States that over-scoping is a conversion risk at the admin gate, which the end user's approval cannot pass on the app's behalf.",
        "Binds each scope to a token type (bot/app identity versus user) rather than reusing one scope name across both undeclared.",
        "Warns that the same scope name means different things on a bot token and on a user token, so the binding must be decided per scope up front."
      ]
    }
  ],
  "trigger_queries": [
    { "query": "design the OAuth provider side of our new third-party app directory", "should_trigger": true },
    { "query": "we're adding \"Sign in with Loomis\" to our product - what do we actually have to build", "should_trigger": true },
    { "query": "how long should our access tokens live and should refresh tokens rotate", "should_trigger": true },
    { "query": "define the scope taxonomy for our public API's OAuth apps", "should_trigger": true },
    { "query": "we only have one full_access scope and it's starting to scare me", "should_trigger": true },
    { "query": "our consent screen is a wall of scary permissions and admins keep rejecting installs", "should_trigger": true },
    { "query": "should we support dynamic client registration", "should_trigger": true },
    { "query": "design the app verification program before we open the integrations directory", "should_trigger": true },
    { "query": "do we need PKCE for our confidential clients too or just the public ones", "should_trigger": true },
    { "query": "what should the lifetime of an authorization code be on our server", "should_trigger": true },
    { "query": "third-party apps need to act on behalf of our customers - how do we let them do that safely", "should_trigger": true },
    { "query": "should we implement DPoP", "should_trigger": true },
    { "query": "we're building an authorization server - which grant types do we support", "should_trigger": true },
    { "query": "partners keep asking for an API that acts as the logged-in user, not a service account", "should_trigger": true },
    { "query": "help me write the refresh token rotation policy", "should_trigger": true },
    { "query": "what should happen on our side if someone replays a refresh token we already consumed", "should_trigger": true },
    { "query": "we want apps to ask for fewer permissions up front and more later as features need them", "should_trigger": true },
    { "query": "our users get logged out when they have two tabs open since we turned on token rotation", "should_trigger": true },
    { "query": "can we let apps register a redirect URI with a wildcard subdomain", "should_trigger": true },
    { "query": "how do other platforms decide which apps earn a verified badge", "should_trigger": true },
    { "query": "we need an admin approval flow before employees can connect apps to their workspace", "should_trigger": true },
    { "query": "design the permissions model for the apps listed on our platform", "should_trigger": true },
    { "query": "our bot tokens and user tokens share scope names and it's getting confusing", "should_trigger": true },
    { "query": "planning the connect-your-account flow for partners building on top of us", "should_trigger": true },
    { "query": "is it ok to reuse an existing scope name for a slightly different meaning", "should_trigger": true },
    { "query": "we're shipping a remote MCP server and we're the auth server behind it - how do agents get tokens", "should_trigger": true },
    { "query": "what exactly do we put on the consent screen", "should_trigger": true },
    { "query": "an enterprise customer wants us to be OAuth 2.1 compliant within six months", "should_trigger": true },
    { "query": "how do we make sure a leaked token from one integration isn't a company-ending event", "should_trigger": true },
    { "query": "review our authorization server design before we ship it", "should_trigger": true },
    { "query": "we're the identity provider for our own ecosystem now - what's the checklist", "should_trigger": true },
    { "query": "how should we tier app reviews by how sensitive the data an app can reach is", "should_trigger": true },
    { "query": "what should happen when a user only approves some of the permissions we asked for", "should_trigger": true },
    { "query": "should the refresh token for our browser app live as long as the one for a backend integration", "should_trigger": true },
    { "query": "our CLI runs on headless servers with no browser - how do users authorize it", "should_trigger": true },
    { "query": "write the token policy document for our platform", "should_trigger": true },
    { "query": "how many refresh tokens should one app be allowed to hold per user account", "should_trigger": true },
    { "query": "right now anyone can self-register an OAuth client with us and I'm nervous about it", "should_trigger": true },
    { "query": "what's a sane default TTL for the tokens we issue", "should_trigger": true },
    { "query": "customers want to restrict which third-party apps their staff can install", "should_trigger": true },
    { "query": "designing permissions for apps that read other users' data, not just the installer's own", "should_trigger": true },
    { "query": "our app review is a one-time questionnaire at submission - is that enough", "should_trigger": true },
    { "query": "plan the security review program for apps listed in our directory", "should_trigger": true },
    { "query": "how do we let developers test their integration before they've been approved", "should_trigger": true },
    { "query": "decide between broad scopes and fine-grained permissions for our platform", "should_trigger": true },
    { "query": "our security team says the bearer tokens we hand out are too weak - what are the options", "should_trigger": true },
    { "query": "we're opening our API to third-party apps for the first time - what's the auth design", "should_trigger": true },
    { "query": "how do we stop an app from requesting more access at runtime than it declared to us", "should_trigger": true },
    { "query": "set up the authorization endpoints and discovery document for our platform", "should_trigger": true },
    { "query": "design the API key format and rotation policy for our public API", "should_trigger": false },
    { "query": "should we store API keys hashed or encrypted at rest", "should_trigger": false },
    { "query": "build a self-service dashboard where customers can create and revoke their own API keys", "should_trigger": false },
    { "query": "how do I add Google OAuth login to my Next.js app", "should_trigger": false },
    { "query": "set up Auth0 for my SaaS signup flow", "should_trigger": false },
    { "query": "configure Firebase Authentication with email and password", "should_trigger": false },
    { "query": "we're consuming Salesforce's OAuth API - where should we store their refresh token", "should_trigger": false },
    { "query": "find the OAuth misconfigurations in this third-party authorization server", "should_trigger": false },
    { "query": "audit our JWT validation code for algorithm confusion attacks", "should_trigger": false },
    { "query": "write a pentest report on the token handling in this mobile app", "should_trigger": false },
    { "query": "our SAML assertions aren't validating against the customer's IdP", "should_trigger": false },
    { "query": "add two-factor authentication to our login page", "should_trigger": false },
    { "query": "users can read another tenant's records just by changing the ID in the URL - fix it", "should_trigger": false },
    { "query": "design the rate limit tiers and response headers for our public API", "should_trigger": false },
    { "query": "write the deprecation and sunset policy for v1 of our REST API", "should_trigger": false },
    { "query": "design our outbound webhook signing scheme and retry policy", "should_trigger": false },
    { "query": "review our REST resource naming and pagination conventions", "should_trigger": false },
    { "query": "design the machine-readable error code taxonomy for our API", "should_trigger": false },
    { "query": "how should idempotency keys be scoped per caller", "should_trigger": false },
    { "query": "plan the sandbox and test mode for our API", "should_trigger": false },
    { "query": "what should our developer portal's information architecture look like", "should_trigger": false },
    { "query": "design the partner developer onboarding journey from signup to first submitted app", "should_trigger": false },
    { "query": "write the listing content standards every app in our marketplace has to meet", "should_trigger": false },
    { "query": "design the ongoing re-review pipeline and revocation policy for apps already listed on our marketplace", "should_trigger": false },
    { "query": "how much revenue share should we take from marketplace apps", "should_trigger": false },
    { "query": "should we build our own app marketplace or just join HubSpot's", "should_trigger": false },
    { "query": "optimize our AppExchange listing's view-to-install conversion", "should_trigger": false },
    { "query": "which languages should get official SDKs first", "should_trigger": false },
    { "query": "design our public GraphQL schema's pagination and error result types", "should_trigger": false },
    { "query": "should we publish gRPC externally or transcode it to JSON", "should_trigger": false },
    { "query": "plan bulk Parquet exports into our customers' object storage", "should_trigger": false },
    { "query": "design customer-facing JDBC access to our product's data", "should_trigger": false },
    { "query": "curate the tool surface for our MCP server - which workflows become tools", "should_trigger": false },
    { "query": "what should our status page show during a partial outage", "should_trigger": false },
    { "query": "surface delivery logs and replay to partners whose integrations keep failing", "should_trigger": false },
    { "query": "which integration surface do we build first, REST or GraphQL", "should_trigger": false },
    { "query": "audit whether every endpoint in our API reference has request and response examples", "should_trigger": false },
    { "query": "design the interview loop for an API product manager", "should_trigger": false },
    { "query": "how do I move from backend engineer into a platform engineering role", "should_trigger": false },
    { "query": "which technology partners should we integrate with next quarter", "should_trigger": false },
    { "query": "build a source connector for Fivetran so customers can sync our data", "should_trigger": false },
    { "query": "implement session cookies and CSRF protection for our web app", "should_trigger": false },
    { "query": "set up SSO with Okta for our employees' internal tools", "should_trigger": false },
    { "query": "configure row-level security in Postgres for our multi-tenant app", "should_trigger": false },
    { "query": "our OAuth client library keeps failing the token exchange with invalid_grant - debug it", "should_trigger": false },
    { "query": "add GitHub and Google login buttons to our signup page", "should_trigger": false },
    { "query": "harden the Kubernetes service account tokens in our cluster", "should_trigger": false },
    { "query": "write our answers to a customer's vendor security questionnaire", "should_trigger": false },
    { "query": "encrypt the secrets our CI pipeline uses at rest", "should_trigger": false }
  ]
}
references/app-verification-program.md›
# App verification program landscape

The five review programs behind SKILL.md step 3's tier ladder, and the comparison table. All figures from the named vendor's own documentation unless labeled otherwise.

## Comparison

| Program                      | Trigger                                 | Requirement                                                 | Timeline                                                        | Recurrence             |
| ---------------------------- | --------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------- | ---------------------- |
| Google CASA                  | Restricted scopes                       | Tier 2 DAST / Tier 3 pentest (OWASP ASVS-based)             | ~10 days (sensitive tier) + assessment months (restricted)      | Annual                 |
| Microsoft M365 Certification | Optional / tenant-required              | Independent audit mapped to SOC 2 / PCI DSS / ISO 27001     | Not published                                                   | Yearly                 |
| Salesforce Security Review   | AppExchange listing                     | OWASP + cross-org isolation + OAuth token handling          | 1-2 wk verification + 3-4 wk testing; each resubmission +2-3 wk | On new versions        |
| Shopify                      | Public app with protected customer data | Data-minimization approval + mandatory GDPR webhooks        | Varies                                                          | Per review             |
| Atlassian Cloud Fortified    | Enterprise trust badge                  | Continuous Ecoscanner + Bug Bounty + annual self-assessment | Ongoing                                                         | Annual self-assessment |

The convergent design across all five: **review depth scales to data sensitivity** - self-serve or attestation for low-sensitivity scopes, mandatory independent testing once an app can reach bulk or other-users' data.

## Google - the tiered-scope archetype

- Three scope tiers:
  - **non-sensitive**: basic profile, no review.
  - **sensitive**: Gmail send, Drive file access, Calendar..., Google's own verification, ~10 days after a complete submission.
  - **restricted**: full Gmail/Drive, third-party CASA assessment _before_ Google's review begins, pushing total time to months.
- **CASA** (Cloud Application Security Assessment, run by the App Defense Alliance on OWASP ASVS) has three tiers: Tier 1 self-assessment, Tier 2 third-party DAST scan (the common landing tier), Tier 3 full manual pentest. Reassessment happens every 12 months, and adding a restricted scope can trigger an out-of-cycle one.
- **The developer pays the assessor, not Google**: Tier 2 around $500+ (community-reported discounted rate with assessor TAC Security), Tier 3 $5,000+ (community-reported). The cost, not just the calendar, makes restricted scopes a genuine go/no-go for small developers.
- **The unverified lane**: "Testing" status allows named test users without review. It ships with a tester warning screen, a **100-user lifetime cap** that only completing verification lifts (current Google docs; older cached sources say 50 - the figure moved once already), and **7-day** consent/refresh-token expiry for test users.
- **The enforcement trigger**: the unverified-app warning fires when runtime-requested scopes diverge from the declared, approved consent-screen configuration - the model for enforcing "runtime request ⊆ approved declaration" rather than trusting declarations to stay accurate.
- The warning copy, as a reference for designing an equivalent: "Google hasn't verified this app" / "...you shouldn't use it", with the click-through reading "Go to [App] (unsafe)". Apps requesting only basic profile skip the warning, the cap, and the short expiry entirely - the apparatus keys on data sensitivity, not on verification status per se.

## Microsoft - three signals, kept deliberately distinct

1. **Publisher Verification** - the blue consent-prompt badge. Verified partner (CPP) account, work/school Entra registration, verified non-`onmicrosoft.com` domain, MFA; no fee; "can be verified in minutes" for a prepared publisher. It measures identity continuity only - Microsoft states the badge "doesn't imply or indicate quality criteria".
2. **Publisher Attestation** - self-assessment; "Microsoft does not independently verify the information submitted" and "most attestations can be completed in one hour or less".
3. **M365 Certification** - yearly independent third-party audit including a pentest, mapped to SOC 2 / PCI DSS / ISO 27001.

**The real enforcement lever is consent blocking, not the badge**: since November 2020, with risk-based step-up consent enabled, users can't consent to most newly registered multitenant apps that aren't publisher-verified when the app requests permissions beyond basic sign-in/profile (Microsoft Learn). Copy both lessons: keep identity/attestation/audit legible as three separate signals, and make the bottom tier a consent gate, not a decoration.

## Salesforce - the review, and the breach that hardened it

- The Security Review (required for AppExchange/AgentExchange listing) tests OWASP classes plus cross-org data isolation and OAuth token handling - "token leakage, improper refresh token handling, authentication bypass" (Salesforce partner guidance). Timeline per Salesforce: 1-2 weeks verification, 3-4 weeks testing, +2-3 weeks per resubmission.
- **The incident context**: the August 8-18, 2025 Salesloft Drift OAuth-token campaign (actor UNC6395) hit "more than 700 organizations" (Google Threat Intelligence Group/Mandiant); the ShinyHunters actor separately _claimed_ ~1.5 billion Salesforce records from 760 companies via OAuth-token abuse (actor claim, not a verified count).
- **The response - what a forced retrofit looks like**: four mandatory OAuth controls including PKCE and refresh-token rotation on all Connected Apps and External Client Apps, enforced May 11, 2026, de-listing/suspension as penalty. Plus a September 2025 shift from trust-by-default (uninstalled connected apps now need explicit permission to authorize; device flow removed). Salesforce ISV practitioners published detailed retrofit accounts - the concrete cost a 2.1-compliant day-one build avoids.

## Shopify - data minimization as the organizing principle

Since API version 2022-10, Shopify redacts customer personal data by default. Apps apply for protected-data access, and approval is scoped to "the minimum amount required" for the stated purpose. GDPR webhooks (`customers/redact`, `shop/redact`, `customers/data_request`) are mandatory for every listed app regardless of whether it stores customer data.

Documented rejection reasons worth turning into your own checklist:

- broken functionality
- excessive permission requests relative to stated functionality
- misleading listing copy
- non-compliant billing
- storefront performance impact

## Atlassian - continuous scanning, earned badge

- **Ecoscanner** scans every Marketplace cloud app continuously, not once at submission.
- **Cloud Fortified** badge stacks process on top: continuous scan passing, a Vulnerability Disclosure Program, bug-fix SLAs, a Bugcrowd bug-bounty, annual security self-assessment.
- **Partner Verification** gates listing for partners whose first app listed on or after June 2, 2025, rolling, not retroactive.

The lesson: point-in-time review and continuous posture are different products. A marketplace at scale needs both.

## Design implications

1. Tier by data sensitivity, with the bright line at scopes reaching **other users'** data or PII - that's where every program above places mandatory independent testing in some form.
2. Recurring, never one-shot: annual reassessment (Google, Microsoft, Atlassian) plus re-review on new versions (Salesforce).
3. Build the recurring rejection themes into the review checklist: over-broad scope requests and weak OAuth token handling recur across Salesforce, Shopify, and Atlassian's stated rejection reasons.
4. The unverified lane needs a real, enforced ceiling (Google's lifetime cap) - a warning banner alone doesn't change developer behavior.
references/consent-screen-design.md›
# Consent screen design

The mechanics behind SKILL.md step 2's consent rules. Sources named inline; the evidence-quality warning at the end applies to the whole topic.

## Plain-language scope mapping

Every provider studied renders scopes as human sentences, never raw identifiers - Google "lists each permission scope being requested in plain language": `crm.leads.read` becomes "View your sales leads". The tension is real and unresolved in the literature:

- Language too technical scares users into bouncing.
- Language too vague is a security problem.

The only concrete lever the field agrees on is **per-scope specificity** - Auth0 recommends individual per-scope descriptions over grouped/bucketed permission names.

- Write the consent copy for each scope when the scope is created.
- Review it like UI copy.
- Treat a scope that can't be explained in one plain sentence as a scope-design smell.

## Partial and granular grants - the implementation trap

Google's granular-permissions model lets the user grant some requested scopes and deny others in one flow, and Google runs an automated check reporting whether an app handles partial grants correctly (Google docs).

**The trap**: the scope set in the authorization _response_ may differ from the _request_, even when the user appears to approve everything. Both sides of the ecosystem must handle it:

- The provider must return the actually-granted set explicitly and support apps re-requesting denied scopes later without penalty.
- Every integrating client must diff granted-vs-requested and disable dependent features on the difference.

This is a correctness requirement, not an edge case - a provider that supports partial consent without surfacing the diff ships a silent capability mismatch into every app built against it.

## Incremental authorization - and its documented failure mode

- The intended pattern (Google's stated best practice, formalized by William Denniss's IETF draft `draft-ietf-oauth-incremental-authz`): request scopes "incrementally, at the time access is required, rather than up front" - don't request Calendar access until the user clicks "Add to Calendar". The draft names what upfront requesting produces: "over-scoped authorization and a sub-optimal end-user consent experience."
- **The war story**: GMass (Ajay Goel, practitioner blog) documents Google's own implementation re-displaying **all previously granted scopes** alongside the new incremental request (the `include_granted_scopes=true` behavior) - confusing users who already granted them and making the developer "look sloppy". Spec-compliant incremental auth with a bad re-display is still a bad consent experience.
- **The rule for your implementation**: the re-consent screen shows only the net-new scope. Treat "does the screen cleanly show the delta" as a first-class UX requirement, tested explicitly - not an assumed byproduct of protocol support.

## Re-consent triggers

Refresh tokens are invalidated when the underlying consent configuration changes (Google docs) - adding scopes to an authorized app forces the user back through consent regardless of how the request is framed. Two consequences:

- Batch scope additions where possible; every addition is a re-consent event with drop-off risk.
- Never repurpose a scope to avoid a re-consent - that silently changes what the standing consent authorizes, trading a UX cost for a security hole.

## RAR and PAR - transaction-level consent

For finance/health-grade consent:

- **Rich Authorization Requests** (`authorization_details`): structured data the consent screen renders directly - "transfer $500 to account ending 4321" instead of a generic payment scope.
- **Pushed Authorization Requests** (PAR, RFC 9126): the authorization payload goes server-to-server before redirect, keeping it out of browser URLs, logs, and history.

Neither is warranted for a typical B2B SaaS consent flow; both are the named mechanism when sensitivity crosses into per-transaction authorization.

## Evidence-quality warning

There is no rigorous, public A/B study of consent-screen conversion from a major identity provider. Circulating figures (e.g. "50% drop-off on over-scoped consent screens") trace to vendor blog posts, not measurements.

Use them, if at all, as illustrative talking points explicitly labeled as vendor claims - never as benchmarks to design against, and never in a design doc as measured fact. The defensible arguments for plain language, minimalism, and incremental consent are security and UX-design grounds, plus the B2B admin-rejection gate (Slack's own guidance) - those stand without invented numbers.
references/protocol-baseline.md›
# Protocol baseline - endpoints, grants, token lifecycle

The concrete OAuth 2.1-profile checklist behind SKILL.md step 1. Spec-status precision, repeated because it gets misquoted: RFC 9700 (BCP 240, January 2025) is final; the consolidated OAuth 2.1 document was at draft-ietf-oauth-v2-1-15 (March 2026) - settled convergence, not yet a ratified RFC.

## Endpoint checklist

- `/authorize` - authorization endpoint.
- `/token` - token endpoint.
- `/revoke` - RFC 7009 token revocation.
- `/introspect` - RFC 7662 token introspection, for your own resource servers.
- `/.well-known/oauth-authorization-server` - RFC 8414 discovery document. Publish it from day one; it is also where later capabilities (DPoP, PAR) get advertised.
- PAR endpoint (RFC 9126) - only if targeting FAPI-adjacent sensitivity later; zero of 18 surveyed mainstream issuers advertised one (MojoAuth issuer survey).

## Grant checklist

- `authorization_code` - with PKCE S256, enforced for every client, public and confidential.
- `client_credentials` - machine-to-machine.
- `refresh_token`.
- `device_authorization` (RFC 8628) - CLIs, TVs, headless clients.
- Never: `implicit`, resource-owner password credentials - removed under OAuth 2.1.

Client vocabulary worth adopting: alongside public/confidential, Aaron Parecki's (OAuth 2.1 co-editor) **"credentialed client"** - a client holding credentials that prove it is the same client that registered, without proving who its operator is. Client authentication buys identity continuity; real-world identity is what the verification program (SKILL.md step 3) buys.

## Token-TTL benchmark table

All figures from the named provider's own documentation:

| Provider                  | Access token TTL             | Refresh token TTL               | Rotation on use?             |
| ------------------------- | ---------------------------- | ------------------------------- | ---------------------------- |
| GitHub App installation   | 1 hour                       | N/A (installation-scoped)       | Regenerated                  |
| GitHub App user-to-server | 8 hours                      | 6 months                        | Yes - new refresh each use   |
| Slack (rotation enabled)  | 12 hours                     | until rotated                   | Yes, old revoked             |
| Microsoft Entra           | 60-90 min (variable)         | 90-day inactivity; 24h for SPAs | Replaces, doesn't revoke old |
| Salesforce                | ~2 hours (session-dependent) | policy-defined                  | Moving to mandatory rotation |
| Google                    | ~1 hour                      | until revoked or capped         | No - reused until revoked    |

Reading the table:

- **The convergence point is ~1-hour access tokens.** Start there rather than inventing a figure.
- **Slack's default posture is the risky one**: "Without token rotation, the access token never expires. With token rotation, it expires every 12 hours" (Slack docs) - the hardened configuration is opt-in, a shape to avoid copying: make rotation the default, not the upgrade.
- **Google is the deliberate outlier**: no rotation, risk bounded instead by a cap of 100 refresh tokens per account per client. The oldest is silently invalidated at the cap (current Google docs - older cached sources say 50; the number already moved once, re-verify at implementation time).
- **Microsoft compensates differently**: no hard rotation-invalidation, but Continuous Access Evaluation for near-real-time revocation - a different tool solving the problem rotation solves elsewhere (Microsoft Learn).
- **Salesforce's figures are a snapshot, not a target** - actively changing under the May 2026 mandatory-OAuth-controls rollout; re-verify against Salesforce's ISV communications.
- GitHub layers a live-token cap on top of TTLs: more than 10 tokens for the same user/scope pair auto-revokes the oldest (GitHub docs) - a per-app population cap, not a per-token expiry, worth copying.

## Rotation mechanics

- **Token families**: every refresh token belongs to a family traceable to the original grant. Use invalidates the presented token and issues a successor in the same family. A consumed token presented again (outside the grace window) revokes the entire family - the convergent Auth0/Keycloak/Django OAuth Toolkit pattern; Auth0: reuse detection "immediately invalidates the refresh token family".
- **Why family-wide, not single-token**: the attacker may hold an earlier or later token in the same chain than the one that tripped the wire.
- **RFC 9700 §4.14's own framing**: the server "cannot determine which party submitted the invalid refresh token, but it will revoke the active refresh token" - a tripwire, never an arbitration mechanism.
- **Grace window, from day one**: multi-tab SPAs, mobile reconnects, and ordinary retries legitimately re-present a just-consumed token. Both Django OAuth Toolkit (`REFRESH_TOKEN_GRACE_PERIOD_SECONDS`) and Keycloak ("Refresh Token Max Reuse") expose the knob. Starting value ~10-30 seconds - long enough for concurrency races, short enough to still catch replay fast.
- **Design the family relationship into the first grant** - retrofitting family grouping onto an already-issued ungrouped token population is materially harder than starting with it.

## Storage tensions

- Store all tokens hashed at rest (SHA-256) - same rationale as high-entropy API keys.
- **The documented tension**: a grace window must recognize a _prior_ token if re-presented within the window, which pure hash-at-rest storage complicates. Django OAuth Toolkit's docs name this explicitly as a tradeoff to resolve deliberately (e.g. retain the predecessor's hash with a validity timestamp) - not an oversight to "fix" by dropping hashing.
- Opaque tokens revoke instantly via the database; JWTs don't. If issuing JWT access tokens, the short TTL _is_ the revocation story - another reason not to stretch past ~1 hour.

## The compliance-gate pattern

Django OAuth Toolkit 3.4 models RFC 9700 compliance as separately named boolean gates (`COMPLIANT_BCP_RFC9700_*`) - one per BCP requirement (PKCE enforcement, exact redirect matching, rotation, ...) - rather than one "OAuth 2.1 mode" flag. Copy the shape: independently toggleable, independently auditable gates let a partial rollout or an audit point at exactly which requirements are and aren't enforced yet.
references/scope-taxonomy-archetypes.md›
# Scope taxonomy archetypes

The named provider models behind SKILL.md step 2's granularity menu. All figures and quotes from the named provider's own documentation or blog.

## Cross-provider comparison

| Provider                    | Scope pattern                               | Read/write split           | Admin consent tier                       | Notable                                     |
| --------------------------- | ------------------------------------------- | -------------------------- | ---------------------------------------- | ------------------------------------------- |
| GitHub (Apps)               | Fine-grained per-resource permissions (50+) | Yes, per-resource          | Org/enterprise approval policies         | Installation-scoped, 1h tokens              |
| GitHub (classic OAuth Apps) | Broad scopes (`repo`, `user`)               | No - `repo` = full control | Org can restrict                         | Legacy; Apps preferred                      |
| Slack                       | `resource:action`                           | Yes                        | Admin-approved apps                      | Bot vs. user token scopes                   |
| Microsoft Graph             | `Resource.Action.Scope` (`User.Read.All`)   | Yes                        | Delegated vs. application; admin consent | Unified across M365                         |
| Stripe Connect              | `read_only` / `read_write`                  | Coarse, 2 levels           | N/A (platform model)                     | OAuth deprecated for new Standard platforms |
| Google                      | URL scopes; sensitive/restricted tiers      | Yes (`.readonly`)          | Workspace domain-wide delegation         | Incremental auth supported                  |

## GitHub - the canonical broad-vs-granular case study

- Classic scopes are the anti-pattern in the vendor's own words: classic tokens "are given permissions from a broad set of read and write scopes. They have access to all of the repositories and organizations that the user could access, and are allowed to live forever" (GitHub docs). The fine-grained-PAT launch post (October 2022) is unusually candid, framing coarse scopes plus immortal tokens as a leading contributor to real breaches.
- The replacement: 50+ granular permissions, each independently no-access/read/read-write, per-repository restriction, org-owner approval policies (GitHub docs). GitHub's stated preference: "GitHub Apps are preferred to OAuth apps because they use fine-grained permissions, give more control over which repositories the app can access, and use short-lived tokens."
- **The migration is documented as lossy**: fine-grained tokens "cannot accomplish every task" classic ones can - confirmed gaps include contributing to public repos where the user isn't a member, and outside-collaborator repos (GitHub docs). This is why classic stays "fully supported" rather than removed: granular models lag broad ones in edge-case coverage throughout the transition. Plan the same lag into your own migration.
- **Lifetime is coupled to granularity, not independent**: installation tokens expire in 1 hour; classic PATs live forever. Fine-grained permissions and short lifetimes shipped as one security upgrade.
- Rate-limit nuance, commonly mis-cited: default 5,000 requests/hour; the 15,000/hour tier applies only to Enterprise Cloud-owned/approved apps; non-Enterprise installations scale from 5,000 by +50/hour per repo or user to a 12,500 cap (GitHub docs).

## Slack - per-resource-type scopes and token-type binding

- Scopes split by conversation type, not action alone: `channels:history`, `groups:history`, `im:history`, `mpim:history` - four scopes where a coarse model has one `messages:read`. The purchase: a DM grant that doesn't silently include private channels. Scope explosion is granularity's price; pay it where resource types genuinely differ in sensitivity.
- **Every scope belongs to exactly one token type**, and the same name means different things per type: `chat:write` on a bot token (`xoxb-`) posts as the app; on a user token (`xoxp-`) it posts as the authorizing human. Slack's migration guidance: "Migrating from user tokens to bot tokens requires reauthorization from every user" - the token-type binding decision is load-bearing for every future migration; make it per-scope on day one.
- Admin governance sits above scopes: workspace admins can require pre-install approval. Slack's stated operating principle: "Asking for more permissions than necessary can cause users or admins to reject the installation" - over-scoping fails at a consent surface the end user cannot approve for you.

## Microsoft Graph - delegated vs. application permissions

Two structurally different permission types, not two tiers of one thing (Microsoft Learn):

- **Delegated** - the app acts on behalf of the signed-in user and can never exceed what that user could do.
- **Application** - app-only identity ("app roles"), independent of any signed-in user; only a Privileged Role Administrator or Global Administrator can consent. Many high-privilege delegated permissions also require admin consent.

This is Slack's bot-vs-user split expressed as a field on the permission itself rather than the token. Either encoding works; not choosing one is the failure.

## Design rules distilled

1. Default to `resource:action` with read/write split and read-only defaults - the dominant idiom (Slack, Microsoft Graph, Stripe).
2. Scopes are additive-only, forever. Repurposing a scope silently rewrites what past consents authorize, and scope-set changes invalidate refresh tokens and force re-consent (Google docs).
3. Bind scope to token type (app identity vs. user delegation) from the first scope shipped, if both exist.
4. Gate high-privilege and org-wide scopes on admin consent - the Microsoft-proven pattern, and the second consent surface every B2B provider needs.
5. Keep the coarse tier alive while a fine-grained tier matures - GitHub's documented edge-case gaps are the expected shape of that transition, not a GitHub-specific accident.
SKILL.md›
---
name: oauth2-provider-design
description: Design the OAuth2 authorization-server surface a B2B SaaS offers third-party apps - the OAuth 2.1 protocol baseline (PKCE for every client, no implicit or password grants, exact redirect matching), token TTL and refresh-rotation policy, scope taxonomy and granularity, consent-screen design with partial and incremental grants, client registration posture, and the tiered app-verification program. Use whenever the user mentions OAuth, "Sign in with X", access and refresh tokens, scopes, consent screens, PKCE, or third-party apps acting on a customer's behalf - even if they never say "OAuth provider". Issuer side only, not integrating against someone else's OAuth. Do NOT use for API-key design - use samber/developer-platform-skills@api-auth-key-management instead.
license: MIT
metadata:
  author: Samuel Berthe
  version: "1.0.2"
---

# OAuth2 Provider Design

You are an OAuth provider designer. Design the authorization-server surface a platform offers third-party apps: protocol profile, token lifecycle, scope taxonomy, consent screen, registration, verification. Do it so a compromised app or leaked token is a contained event, and so the platform never faces the forced retrofit Salesforce's ISV ecosystem lived through in 2026.

This is decision guidance for a platform/API team, not an RFC tutorial. The target profile is OAuth 2.1, with one precision to carry everywhere:

- RFC 9700 (BCP 240, the OAuth Security Best Current Practice) is final and ratified (January 2025).
- The consolidated OAuth 2.1 document (draft-ietf-oauth-v2-1) remains in draft. Check its current status before citing it as published.

The requirements are settled convergence regardless of the document's own publication status. Never state that OAuth 2.1 itself is a published RFC without first confirming that's still accurate.

**Memory (advised):** When memory lives in a file, consider using `developer-platform-context.md`; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.

## Clarifying questions

Ask before designing anything; each answer changes a later step. Batch them - this is tactical design, not a strategy interview.

1. Greenfield or retrofit? If retrofit: which grants run today, do any clients depend on the implicit or password grant, is refresh rotation on, and roughly how many registered apps exist.
2. Client population: server-side confidential apps, browser SPAs, native/CLI apps, AI agents or MCP hosts? (public vs confidential changes token policy - see step 1)
3. Do apps act only on behalf of a signed-in user, or also with their own identity (bot/app tokens, org-wide access)? (drives scope-to-token-type binding and admin consent - see step 2)
4. Data-sensitivity ceiling: can any scope reach _other users'_ data, bulk data, or regulated financial/health data? (sets the verification tier - see step 3 - and whether step 4 ever matters)
5. Who approves an install: individual end users only, or org admins too? (see the consent-surface split below)
6. Build substrate and effort ceiling: authorization server from scratch, or on an identity product that already ships rotation/DPoP as configuration? When must this ship, and is it a one-off unblock (an enterprise deal demanding OAuth) or the foundation of an app ecosystem? The menus below diverge sharply on effort - re-rank them against these answers.

If your harness has persistent memory, store the core decisions: grant list, TTLs and rotation posture, scope naming convention and token-type binding, verification tiers and thresholds. A later run (a scope addition, a review-program escalation, a token-theft incident) then starts from the design instead of re-deriving it.

## Who consents: end user, org admin, or both

The B2B/B2C split that matters here is who stands between the app and the grant:

- **B2C / individual users**: one consent surface - the end user's screen. Consent copy quality and scope minimalism carry the entire trust decision.
- **B2B / org-owned workspaces**: two consent surfaces. The end user consents, and an org admin can gate installs before or above that (Slack admin-approved apps; Microsoft admin consent, where app-identity permissions can only ever be consented by an admin). Slack's developer guidance names the consequence: "Asking for more permissions than necessary can cause users or admins to reject the installation" - over-scoping is a conversion risk at a gate the end user's "yes" cannot pass for you.

The protocol core, token lifecycle, and plain-language consent-copy rules are identical for both - only the approval topology and the app-identity/org-wide scope tier differ. A B2B provider must design the admin surface (approval queue, org-wide grant review, app-identity permission tier) as a first-class part of steps 2-3, not bolt it on when the first enterprise customer asks.

## Workflow

Build in this order - each stage is load-bearing for the next, and the order is the staged sequence the cross-provider research converged on:

1. Protocol core.
2. Scope taxonomy and consent screen.
3. Registration and verification program.
4. Advanced hardening - only when justified.

## 1. Protocol core

Implement `authorization_code` with PKCE (S256, mandatory for every client - public and confidential alike), `client_credentials`, `refresh_token`, and `device_authorization` (RFC 8628, for CLIs and headless clients). Do not implement `implicit` or the password grant - both are removed under OAuth 2.1; a greenfield server has no reason to ever support them.

- Exact string matching on redirect URIs, with the single standard exception of the port number in native-app `localhost` redirects. RFC 9700's own justification: pattern matching "turned out to be more complex to implement and more error-prone", with "several successful attacks exploiting flaws in the pattern-matching implementation... observed in the wild" (RFC 9700).
- Never accept bearer tokens in URI query strings - Authorization header or POST body only; query strings leak into logs, history, and Referer headers.
- Authorization codes: single-use, maximum 10-minute lifetime (RFC 6749 §4.1, reinforced by RFC 9700).
- Access tokens: default ~1 hour - the cross-provider convergence point (GitHub App and Google tokens ~1h, Microsoft Entra 60-90 min; provider docs - full table in [references/protocol-baseline.md](references/protocol-baseline.md)). Keep JWT access tokens short specifically because a JWT can't be revoked mid-lifetime the way an opaque database-backed token can.

**Refresh-token posture** - three candidates, ranked:

- security value: `sender-constrained (DPoP) > rotation + reuse detection > non-rotating with a per-client cap`
- third-party client-library compatibility: `non-rotating > rotation > DPoP`
- effort: `DPoP > rotation > non-rotating`
- efficiency: `rotation + reuse detection > non-rotating > DPoP`

- **Default rung: rotation with token-family reuse detection.** Every refresh token belongs to a family traceable to the original grant; a consumed token presented again revokes the whole family (the Auth0/Keycloak/Django OAuth Toolkit convergent pattern). Ship a reuse grace window from day one - around 10-30 seconds - or multi-tab SPAs and network retries will read as attacks and log real users out. RFC 9700 §4.14 states what the tripwire buys: the server "cannot determine which party submitted the invalid refresh token, but it will revoke the active refresh token" - it's a tripwire, not an investigation tool.
- **DPoP is the starved option**: strongest posture, and near-zero ecosystem support - 0 of 18 mainstream issuers with a working discovery document advertised DPoP support (MojoAuth issuer survey). Promote it when tokens guard financial or health data (FAPI 2.0 accepts DPoP or mTLS as its baseline) or when long-lived AI-agent tokens widen the theft blast radius; Bluesky's mandatory-DPoP deployment proves it production-viable at scale. Otherwise it's a support burden few client libraries can even meet yet - Stage 4, not here.
- **Non-rotating survives only in two bounded forms:**

  - Confidential clients, whose client authentication already sender-constrains redemption (RFC 9449's own carve-out - this is why 2.1 mandates "sender-constrained or rotated" only for public clients).
  - Google's capped-population model (max 100 refresh tokens per account per client, oldest silently invalidated; Google docs - this figure moved from a cached "50", re-verify at implementation).

  An indefinitely-reusable refresh token for a public client is not a rung to demote - it's banned under 2.1; delete it from the menu.

- Cap refresh-token lifetime hardest for browser clients: Microsoft's SPA-specific 24-hour refresh-token lifetime, versus 90-day inactivity elsewhere (Microsoft Learn), is the reusable threshold - a refresh token in browser storage is a structurally higher-exposure asset.
- This ranking is a default, not a law - re-rank against question 6: a provider building on an identity product that ships DPoP as configuration (Keycloak 26.4 does; vendor docs) flips DPoP's effort line, and a retrofit with thousands of live integrations flips rotation's.

Endpoints, storage mechanics (hashed tokens vs the grace window's cleartext need), and the compliance-gate pattern: [references/protocol-baseline.md](references/protocol-baseline.md).

## 2. Scope taxonomy and consent screen

Two granularity rungs, ranked - the broad catch-all scope is deleted below, not ranked:

- blast-radius containment: `fine-grained per-resource permissions > resource:action scopes`
- integrating developer's effort: `fine-grained > resource:action` (more decisions per install)
- provider effort: `fine-grained > resource:action` (a permission taxonomy is a quarters-long project)
- efficiency: `resource:action > fine-grained`

- **Default rung: `resource:action` naming with a read/write split and read-only defaults** - the dominant idiom across Slack, Microsoft Graph, and Stripe Connect (provider docs; comparison table in [references/scope-taxonomy-archetypes.md](references/scope-taxonomy-archetypes.md)). Name scopes after the same resource nouns the API itself uses - `samber/developer-platform-skills@public-api-design-review` owns that naming consistency.
- **Fine-grained per-resource permissions are the starved option** (GitHub Apps: 50+ independent permissions, per-repository restriction, 1-hour installation tokens; GitHub docs) - promoted when org admins demand approval policies, a compliance regime is in scope, or scopes reach org-wide data. Budget for the documented cost: GitHub's own fine-grained model still "cannot accomplish every task" the classic broad model can (GitHub docs). Granular models lag broad ones in edge-case coverage through the whole transition, so keep the coarse tier alive during migration rather than forcing a lossy cutover.
- **Deleted anti-pattern, not a bottom rung: the single broad catch-all scope.** GitHub's classic `repo` grants read and write to everything with no way to request less. GitHub's own fine-grained-PAT launch post frames coarse scopes plus immortal tokens as a leading contributor to real breaches (GitHub blog, 2022). Designing one in 2026 is choosing the failure mode the entire field is migrating off.

Taxonomy rules that are cheap now and ruinous to retrofit:

- **Additive-only, forever**: never repurpose an existing scope's meaning - repurposing silently changes what already-granted consents authorize, and scope changes invalidate refresh tokens, forcing re-consent (Google docs).
- **Bind every scope to a token type on day one** if apps can hold both app-identity and user-delegated access. Slack's same `chat:write` posts as the app on a bot token and as the human on a user token, and its own migration guidance warns that changing that binding later "requires reauthorization from every user" (Slack docs). Microsoft expresses the identical split as delegated-vs-application permissions with admin-only consent on the application side.
- Accept scope explosion as granularity's price where the resource types genuinely differ (Slack: four separate history scopes across conversation types) - the alternative is a DM grant that silently includes private channels.

Consent screen:

- Translate every scope into a plain-language sentence, per-scope - never grouped buckets, never the raw scope string.
- Support partial grants, and **diff granted-vs-requested scopes server-side, always**. With granular consent, the response scope set may not match the request even when the user appears to approve everything - a provider that assumes they match ships a silent capability mismatch into every integrating app (Google's granular-permissions model documents this trap).
- Support incremental authorization (request a scope when the feature needs it, not upfront), and design the re-consent screen to show **only the net-new scope**. Google's own implementation re-displays everything already granted alongside the delta, a documented practitioner complaint (GMass) about making integrating developers "look sloppy".
- No rigorous consent-conversion study exists - circulating drop-off percentages trace to vendor blogs, not measurements. Argue consent design on security and UX grounds; never cite a conversion figure as fact.

Full archetypes and consent mechanics: [references/scope-taxonomy-archetypes.md](references/scope-taxonomy-archetypes.md), [references/consent-screen-design.md](references/consent-screen-design.md).

## 3. Registration and verification program

**Client registration** - two rungs, ranked by control retained per unit of onboarding friction; open dynamic registration is deleted below, not ranked:

- control over who integrates: `manual/reviewed registration > gated dynamic registration`
- integration friction for developers: `manual > gated dynamic`
- efficiency: `manual/reviewed > gated dynamic`

- **Default: manual developer-portal registration with a review step.** The negative finding is the argument: no major B2B SaaS platform offers ungated public dynamic client registration - GitHub, Slack, Stripe, and Salesforce all require portal registration plus review. When every legitimate client goes through onboarding anyway, DCR's core value (registering a client with no prior relationship) doesn't apply.
- **Promote to gated DCR (RFC 7591 behind an initial access token, rate-limited, all metadata treated as untrusted input)** only when deliberately serving an MCP/AI-agent ecosystem where clients discover servers dynamically - the one population that can't pre-register.
- **Open, ungated DCR is deleted, not demoted**: a May 2026 preprint probing 119 OAuth-enabled remote MCP servers found DCR flaws in 96.6% of them, and the MCP spec itself walked DCR from SHOULD to MAY to deprecated across 2025-2026 (MCP spec revisions). Self-asserted registration metadata can impersonate any legitimate app's name and logo unless cryptographically attested - an open endpoint is a phishing kit.

**Verification tiers** - rank review depth by trust bought per unit of developer friction, and scale it to data sensitivity, never one bar for all apps:

- trust signal bought: `third-party security assessment > self-attestation > publisher identity verification`
- developer friction: `third-party assessment > self-attestation > identity verification`
- efficiency: `identity verification > self-attestation > third-party assessment`

- **Default rung: publisher identity verification for every listed app** - Microsoft's model: domain plus verified partner account, no fee, "verified in minutes" for a prepared publisher (Microsoft Learn). It proves continuity of identity, not quality - and pair it with Microsoft's real enforcement lever: unverified publishers can't receive consent for permissions beyond basic profile in risk-managed tenants.
- **Self-attestation is the middle rung** - cheap (Microsoft: "most attestations can be completed in one hour or less") and explicitly not independently verified (Microsoft Learn). Keep the three signals visually distinct on your listing surface: Microsoft's own docs say the identity badge "doesn't imply or indicate quality criteria" - one undifferentiated checkmark for identity, attestation, and audit misleads every buyer reading it.
- **Mandatory third-party security assessment is the starved rung**: months of calendar time, plus a direct assessor fee the _developer_ pays rather than you. That fee runs roughly an order of magnitude higher for the full-pentest tier than for the scan tier (community-reported CASA pricing; figures in the reference). One bright-line condition every studied program draws promotes it: **the moment a scope grants access to other users' data or PII, not just the requesting user's own** (cross-provider review-program comparison), with annual recurrence on CASA's 12-month cadence.
- Give unverified apps a legitimate testing lane, modeled on Google's: bounded named test users, a visible warning screen, and hard ceilings - Google's is a 100-user lifetime cap that verification alone lifts (current Google docs; older sources say 50) plus 7-day test-token expiry. "Verify before you scale" only works when the ceiling is real.
- Enforce at runtime, not just at review: reject any scope request that isn't a subset of what was declared and approved at registration - Google's unverified-app warning fires exactly on that divergence, because declarations drift.

The cautionary tale to cite when this stage gets deprioritized: the August 2025 Salesloft Drift OAuth-token campaign hit 700+ organizations (Google Threat Intelligence/Mandiant). Salesforce's response, four mandatory OAuth controls including PKCE and rotation on all connected apps enforced May 11, 2026 with de-listing as the penalty (Salesforce ISV communications), is what a forced retrofit looks like. Full program comparison: [references/app-verification-program.md](references/app-verification-program.md).

## 4. Advanced hardening - when justified

- **DPoP/mTLS sender-constraining**: differentiation, not baseline - the same 18-issuer survey found exactly one (Microsoft Entra) advertising mTLS and zero advertising DPoP or PAR (MojoAuth survey). Runtime cost is not the blocker (~2-10ms per request for DPoP, ~5-15ms per connection for mTLS) - absent client-library support is. Adopt when question 4 answered financial/health data or long-lived agent tokens.
- **PAR (RFC 9126) + Rich Authorization Requests**: for transaction-level consent - rendering "transfer $500 to account ending 4321" instead of a generic payment scope - and keeping that payload out of browser URLs. Named mechanisms to reach for at FAPI-adjacent sensitivity; noise for a typical B2B SaaS consent flow.
- Gated DCR, if question 2 surfaced an MCP/agent ecosystem (see step 3).

## Failure modes

Each is a named, documented trap - not a hypothetical:

- Shipping the implicit or password grant on a new server, or prefix/pattern redirect matching - the exact holes 2.1 closed after in-the-wild exploitation.
- Rotation without a grace window: legitimate concurrent refreshes (multi-tab, retries) trip reuse detection and log users out.
- Hashing refresh tokens at rest without handling the grace window's need to recognize a just-consumed token - a documented design tension to resolve deliberately (Django OAuth Toolkit docs), not discover in production.
- Assuming the granted scope set equals the requested one - partial consent makes the diff mandatory.
- Re-consent screens that re-display every already-granted scope instead of the delta.
- Repurposing an existing scope's meaning instead of adding a new scope.
- Deferring the bot-vs-user token-type split, then paying Slack's documented price: reauthorization from every user.
- One visual trust badge conflating identity verification, self-attestation, and independent audit.
- An open dynamic-registration endpoint.
- A testing lane whose user cap isn't enforced, or verification checked only at review time while runtime scope requests drift.
- Citing a consent drop-off percentage as measured fact.

## Measurement

Gates - iterate until every one passes:

- Protocol: the discovery document and a conformance pass show no implicit/password grant, PKCE rejected-if-absent for every client, exact redirect matching - verified by test, not assertion.
- Token lifecycle: reuse detection demonstrated in staging (replayed refresh token revokes the family); a concurrent-refresh test passes without a false-positive logout.
- Consent: every scope has per-scope plain-language copy; a partial-grant integration test exercises the server-side diff.
- Verification: every scope is classified into a tier with a named review requirement; runtime subset-of-approved enforcement is live; the unverified-lane cap is enforced, not documented.

Trends to baseline from launch (no industry thresholds exist): median app-review turnaround, share of apps requesting scopes their traffic never uses, reuse-detection trip rate (a proxy for both attacks and a too-tight grace window).

## Invocation examples

- "We're adding 'Sign in with Acme' and a third-party app directory - design the OAuth provider side."
- "Define the scope taxonomy for our public API's OAuth apps - today we have one `full_access` scope."
- "Our consent screen is a wall of scary permissions and admins keep rejecting installs - redesign it."
- "Set token TTLs and the refresh rotation policy for our authorization server."
- "Design the app verification program before we open the integrations directory."

## References

See also, same collection:

- `samber/developer-platform-skills@api-auth-key-management` - the key-or-OAuth boundary decision and everything on the key side of it; this skill starts once that decision lands on OAuth for third-party apps.
- `samber/developer-platform-skills@mcp-server-offering` - the MCP resource-server posture that consumes the authorization server designed here.
- `samber/developer-platform-skills@api-versioning-policy` - deprecating a scope or sunsetting a grant type follows the same notice-window and sunset-signalling discipline as any breaking API change.
- `samber/developer-platform-skills@zapier-integration-playbook` - Zapier as an OAuth client: it refreshes tokens only on HTTP 401, and changing the auth scheme later forces a new integration major version.
- `samber/developer-platform-skills@app-marketplace-review` - the ongoing security/quality review process for listed apps; step 3 here designs the verification gate; that skill owns the standing review operation behind the marketplace.