Skip to content

feat(bb-auth-cognito): opt-in auth.admin handle (group + user-lifecycle admin)#38

Merged
harsh62 merged 27 commits into
aws-devtools-labs:mainfrom
harsh62:docs/bb-auth-cognito-admin-design
Jul 23, 2026
Merged

feat(bb-auth-cognito): opt-in auth.admin handle (group + user-lifecycle admin)#38
harsh62 merged 27 commits into
aws-devtools-labs:mainfrom
harsh62:docs/bb-auth-cognito-admin-design

Conversation

@harsh62

@harsh62 harsh62 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the opt-in auth.admin handle on AuthCognito — server-side group-membership and user-lifecycle admin operations — adopting the in-package counter-proposal instead of a separate @aws-blocks/bb-auth-cognito-admin package.

const auth = new AuthCognito(scope, 'auth', {
  groups: ['admins'],
  admin: { actions: ['groups'] },        // opt-in: enables auth.admin + grants matching IAM
});
await auth.requireRole(ctx, 'admins');               // gate the route
await auth.admin.addUserToGroup('alice', 'admins');  // group narrowed via GroupOf<O>

Without an admin options object, auth.admin is a compile error and the pool gets no Admin* IAM grant (least privilege, byte-identical to today's role).

What's included

  • Types + gate (types.ts): AdminOptions, AdminSurface, AdminGetterOf, GroupAdmin/LifecycleAdmin, plus admin? on AuthCognitoOptions.
  • All four runtimes: mock (mutates live state + flushToDisk), AWS (Admin* SDK commands), browser stub, CDK (second PolicyStatement scoped by actions).
  • const O class generic so inline option literals narrow without as const (separate commit — see breaking-change note).
  • Tests: 13 mock unit + 4 CDK grant + a compile-is-the-test type proof + a live sandbox e2e suite.
  • Docs: README admin section + the superseded original design doc retained for its research.

Design decision surfaced during implementation

The compiler caught a variance regression: narrowing the method set by actions via a conditional over O forced AuthCognito<O> invariant and broke 14 existing call sites. Resolved: actions scopes the IAM grant only; the typed surface is always the full set (group names still narrow). An ungranted call fails at runtime with IAM AccessDenied.

Verification

  • Unit / type / CDK: 224 pass, 0 fail; tsc --build clean across all entries; conditional-exports parity 20/20.
  • Live integration (real Cognito, us-west-2): 5/5 pass — createUser→setPassword→signIn, addUserToGroup→requireRole on a fresh token, disable/enable gates signIn, deleteUser, revokeUserSessions. Stack deployed and torn down clean.
  • Live run surfaced and corrected a mock-vs-AWS parity point: AdminUserGlobalSignOut revokes refresh tokens but does not invalidate an already-issued access token, so checkAuth does not flip immediately on AWS (mock deletes the session record and does). Test + README corrected.

⚠️ Breaking change (pre-release)

const O narrows requireRole / updateUserAttribute / updateMFAPreference params for inline-literal options. Callers passing widened string variables now need a cast or literal args (fixed the 2 affected test-app call sites here).

Unrelated pre-existing issue noted

The full test:e2e:sandbox run hits WebSocket is not defined in bb-realtime on Node 20 (needs ≥22 or a polyfill) — independent of this change; worth a separate ticket.

@harsh62
harsh62 requested a review from a team as a code owner June 18, 2026 15:37
@harsh62
harsh62 force-pushed the docs/bb-auth-cognito-admin-design branch from 5b8215f to b9c225b Compare June 29, 2026 21:33
@harsh62 harsh62 changed the title docs(tech-design): AuthCognitoAdmin building block design feat(bb-auth-cognito): opt-in auth.admin handle (group + user-lifecycle admin) Jun 30, 2026
harsh62 added 12 commits June 30, 2026 11:15
Implementation-ready design for the AuthCognitoAdmin building block — the
server-side admin surface (user lifecycle + group membership) that AuthCognito
deliberately omits from its self-gating client-facing class. Covers API
surface, error model, per-runtime implementation, and mock parity.

Migrated from private staging repo (squashed). Internal cross-references to
docs not present in this repo have been de-linked.
…Admin

Add implementation plan for the in-package `auth.admin` handle approach
(opt-in via `admin` options object) instead of a separate
`@aws-blocks/bb-auth-cognito-admin` package, per the reviewed counter-proposal.
Mark the original separate-package design as superseded; retain it for the
API-surface/error-model/mock-parity research the handle reuses.
… design

Detail the full type-safety design for the in-package admin handle: the
AdminOptions/AdminSurface/AdminGetterOf gate, GroupAdmin/LifecycleAdmin
interfaces, the single safe getter cast, and per-runtime + CDK + const-O
migration steps. Every conditional type was compiled under tsc --strict with
positive and @ts-expect-error negative cases (Appendices A/B); the proof lands
in-repo as admin.types-test.ts.
… auth.admin plan

11 ordered, independently-committable tasks (T1-T11) with dependencies, plus a
verify list spanning type tests, mock behavior, CDK grant assertions, and a
sandbox-gated integration suite (incl. negative least-privilege).
…l runtimes)

Adds the server-side admin surface as an opt-in handle on AuthCognito (T1-T4,
T6-T7): admin types + compile-time gate, mock runtime (group/lifecycle mutators
on live state), AWS runtime (Admin* SDK commands), browser stub getter, and the
CDK IAM grant scoped by admin.actions. Includes admin.types-test.ts.

Type-safety finding: narrowing the method set by actions via a conditional over
O forces AuthCognito<O> invariant and regressed 14 existing call sites. Resolved
by always exposing the full surface (group names still narrow via GroupOf<O>);
actions scopes the IAM grant only. Full package build + existing 117 tests green.

Remaining: T5 const O, T8/T9 unit tests, T10 integration, T11 README.
Change AuthCognito<O> to AuthCognito<const O> across all four entries so inline
options literals narrow without `as const`. This narrows five projections by
default — GroupOf (requireRole), AttrOf (updateUserAttribute family), ReadAttrOf
(fetchUserAttributes), MfaTypeOf (confirmSignIn/updateMFAPreference), and
CustomAttrNames.

Fixes the call sites that pass wire strings to now-narrowed params by casting at
the boundary (runtime validates regardless): comprehensive authCRequireRole +
authCMfaUpdateMFAPreference, native-bindings cognitoRequireRole. Tightens
admin.types-test case 5 to assert group narrowing without `as const`.

BREAKING CHANGE: callers passing widened variables to requireRole /
updateUserAttribute / updateMFAPreference now need a cast or literal args.
Acceptable pre-release.
… grant)

admin.test.ts (13): runtime gate throw, group add/remove + GroupNotFound +
UserNotFound, listGroups/listUsersInGroup, lifecycle create/get/delete (+ group
cleanup), disable/enable (asserts existing signIn disabled-guard), setUserPassword,
scan, revokeUserSessions.

index.cdk.test.ts (+4): no-admin grants zero Admin* actions (least privilege);
admin:{} grants both slices; actions:['groups'|'lifecycle'] grant only that slice.

Full package suite: 224 tests, 0 fail.
Add an 'Admin surface' section: opt-in via the admin options object, the
compile-time gate, actions scoping the IAM grant (not the typed method set),
the not-an-access-boundary caveat, group + lifecycle method tables, and the
session-freshness note (revokeUserSessions for immediate effect). Update the
generic-narrowing section for the const O type parameter (inline literals
narrow without as const).
Wire authC with admin: {} plus authCAdmin* routes (createUser, setPassword,
addToGroup, listGroupsForUser, disable/enable, delete, revokeSessions), and add
sandbox-admin-surface-e2e.ts driving them through the deployed Lambda over JSON-RPC.

Covers: createUser, setUserPassword→signIn, addUserToGroup→requireRole on a fresh
token, revokeUserSessions drops the live session, disable/enable gates signIn,
deleteUser. Requires a deployed bb-test-* sandbox (sandbox-gated; not in the unit
run). Typechecks clean.
Convert the admin e2e to the (getApi) => void suite pattern wired into
e2e.test.ts (auth-cognito-admin-sandbox.test.ts), replacing the standalone
script so it runs under the unified deploy/teardown harness.

Verified live against real Cognito (us-west-2): createUser→setPassword→signIn,
addUserToGroup→requireRole on a fresh token, disable/enable gates signIn,
deleteUser, and revokeUserSessions all pass (5/5).

Live run surfaced a real mock-vs-AWS parity point: AdminUserGlobalSignOut
revokes refresh tokens but does NOT invalidate an already-issued access token,
so checkAuth does not flip immediately on AWS (the mock deletes the session
record and does). Corrected the test assertion and the README accordingly.
Regenerate API.md to include the admin surface (AdminOptions, AdminSurface,
GroupAdmin/LifecycleAdmin, AdminGetterOf, AdminUser, AdminCreateInit) and the
const O class generic. Add a minor changeset documenting the feature + the
const O breaking change.
bb-agent's committed API.md is out of date with its BedrockModels source on
main (model-id renames + DEFAULT/BALANCED/SMART reordering). The repo-wide
check:api gate regenerates and diffs ALL packages, so this pre-existing
staleness blocks any PR. Regenerated to match source — no source change.
Isolated in its own commit so it can be dropped if handled separately.
@harsh62
harsh62 force-pushed the docs/bb-auth-cognito-admin-design branch from f9a0232 to 7941c3e Compare June 30, 2026 15:40
harsh62 and others added 12 commits June 30, 2026 11:45
The repo-wide changeset-coverage gate requires a changeset for any package
with file changes; the bb-agent API.md regen (stale-on-main fix) needs a
patch entry.
- DESIGN.md (must-fix): move the admin surface from Out to In (opt-in auth.admin
  handle), update the IAM section to the opt-in grant model, add an Admin surface
  section + Key Design Decision aws-devtools-labs#7, cross-link the implementation plan. Resolves
  the shipped-doc-vs-code contradiction.
- @aws-blocks/blocks (should-fix): re-export the admin types so umbrella consumers
  can name AdminOptions/AdminUser/GroupAdmin/etc.; regenerate blocks API.md; add
  changeset.
- Superseded design doc (minor): reduce the 755-line BB-auth-cognito-admin.md to a
  redirect stub, removing KitContext/KIT_*/isKitError rebrand residue.
- AWS admin scan() (minor): pin Limit: 60 to match the documented page-size contract
  and the other paginators.

Full package suite: 224 pass, 0 fail.
…ndings

'email' is a built-in Cognito standard attribute; declaring it in
userAttributes is invalid per DESIGN.md (built-ins are implicit). Harmless
under the old wide generic, but once AuthCognito<const O> narrowed the literal,
CognitoUser.attributes emitted BOTH 'email' and 'custom:email' in the generated
OpenRPC spec — which the Dart codegen turned into a duplicate field / colon-in-
identifier, breaking 'dart analyze' with error-level diagnostics (Native SDK
E2E / Dart E2E). Removing the redundant declaration restores the open-map
attributes shape. Root cause of the Dart E2E regression since the const-O
commit; Swift was unaffected because its generator maps the shape to a Map.

Verified locally: regenerated spec has 0 custom:email; dart analyze on the
regenerated client reports 0 errors (infos only, which analyze tolerates).
Addresses reviewer-identified gaps in the admin surface:

- Gap 1 (typed reads): AdminUser is now AdminUser<O> — getUser/scan/listUsersInGroup
  return narrowed groups (GroupOf<O>) and attributes (ReadAttrOf<O>), matching the
  client-side CognitoUser. createUser attributes narrow via AttrOf<O> too, catching
  typos the way signUp does.
- Gap 3 (ungranted-action feedback): a new AdminActionGate rest-param makes calling
  an ungranted method (e.g. deleteUser under actions:['groups']) a COMPILE error —
  variance-safe because the gate is in parameter position, not the surface shape
  (verified against the call sites the earlier shape-narrowing attempt regressed).
  A runtime guard also fast-fails with a clear message instead of AWS AccessDenied.
- Gap 4 (scan filter): scan(filter?) accepts an AdminUserFilter mapped to Cognito's
  ListUsers Filter (startsWith/equals); mock filters in memory.
- Gap 5 (readability): setUserPassword(username, password, { permanent }) uses a
  named options object instead of a bare boolean.

Umbrella @aws-blocks/blocks re-exports the new public types. API reports + changeset
updated. Package suite: 228 pass, 0 fail; full type-test incl. gate + variance guard.
…AWS parity)

Live sandbox e2e for the new admin runtime paths surfaced two mock-vs-AWS
parity bugs the unit tests couldn't:

- AWS admin.createUser passed custom attributes unprefixed, so Cognito rejected
  them ('attribute department is not defined in schema'). Now runs them through
  prefixCustomAttrs like signUp and the mock do.
- AWS admin.getUser never populated AdminUser.groups (AdminGetUser doesn't return
  memberships), so the typed groups field was always empty on AWS. Now fetches
  them via AdminListGroupsForUser, matching the mock.

Adds sandbox e2e coverage for the new surface: getUser attribute+group round-trip
(Gap 1) and scan() with a Cognito ListUsers Filter (Gap 4), plus the backend
routes they drive. Verified live (us-west-2): all 7 admin e2e tests pass.
…up read

getUser is lifecycle-gated but reports group memberships via
AdminListGroupsForUser, which was only in the 'groups' IAM slice. A pool with
admin: { actions: ['lifecycle'] } would grant AdminGetUser but not
AdminListGroupsForUser, so getUser 500'd with IAM AccessDenied only in the
deployed AWS runtime (mock reads groups from memory; the admin:{} sandbox e2e
grants everything — both hid it).

- Add AdminListGroupsForUser to the lifecycle IAM slice (shared with groups).
- Make getUser's group fetch best-effort: swallow AccessDenied and return
  groups: undefined rather than failing the whole read under a hand-narrowed policy.
- Regression tests: CDK lifecycle-only self-sufficiency (AdminGetUser +
  AdminListGroupsForUser both granted) and a mock lifecycle-only getUser path.

Caught in PR review; a self-inflicted gap from the earlier getUser groups fix.
pranavosu
pranavosu previously approved these changes Jul 22, 2026
Reserve minor bumps for real API changes during preview; the
const O breaking-change condition is unlikely to hit in practice
(only literal-variable call sites, 2 found repo-wide).
@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 90f0340

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@aws-blocks/bb-auth-cognito Patch
@aws-blocks/bb-agent Patch
@aws-blocks/blocks Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

harsh62 added 2 commits July 22, 2026 12:41
… gate

The shipped AdminActionGate (types.ts) gates each admin method at
compile time via a trailing rest parameter, so an ungranted call is a
compile error (proven by admin.types-test.ts) — not merely a runtime
AccessDenied. The parameter-position gate keeps AuthCognito<O>
covariant, so the 'not possible without breaking variance' framing is
also wrong. Update README:170 + DESIGN admin-surface bullet and Key
Design Decision aws-devtools-labs#7 accordingly, and fix the stale setUserPassword
signature (options object, not a bare permanent boolean).
@harsh62
harsh62 merged commit feb5be4 into aws-devtools-labs:main Jul 23, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants