Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bfe18c1
docs(tech-design): add AuthCognitoAdmin building block design
harsh62 Jun 18, 2026
bc17a65
docs(tech-design): adopt in-package auth.admin handle for AuthCognito…
harsh62 Jun 29, 2026
61d8d62
docs(tech-design): expand auth.admin plan with compiler-verified type…
harsh62 Jun 29, 2026
02d4f11
docs(tech-design): add task list and unit/integration verify lists to…
harsh62 Jun 29, 2026
1e6f997
feat(bb-auth-cognito): implement opt-in auth.admin handle (types + al…
harsh62 Jun 30, 2026
7bc20b3
feat(bb-auth-cognito)!: make AuthCognito generic a const type parameter
harsh62 Jun 30, 2026
a77aa90
test(bb-auth-cognito): unit tests for auth.admin (mock behavior + CDK…
harsh62 Jun 30, 2026
c64f681
docs(bb-auth-cognito): document the auth.admin opt-in surface in README
harsh62 Jun 30, 2026
a339472
test(comprehensive): live e2e suite for the auth.admin surface
harsh62 Jun 30, 2026
89d8076
test(comprehensive): integrate auth.admin sandbox suite into e2e harness
harsh62 Jun 30, 2026
91c5745
chore(bb-auth-cognito): update API report + add changeset for auth.admin
harsh62 Jun 30, 2026
7941c3e
chore(bb-agent): regenerate stale API report
harsh62 Jun 30, 2026
cea63ec
chore: add changeset for bb-agent API report regeneration
harsh62 Jun 30, 2026
d0f8949
Merge branch 'main' into docs/bb-auth-cognito-admin-design
harsh62 Jul 2, 2026
de658bd
Merge branch 'main' into docs/bb-auth-cognito-admin-design
harsh62 Jul 3, 2026
7c4f4de
docs+fix(bb-auth-cognito): address PR review findings
harsh62 Jul 3, 2026
46c4d0e
Merge branch 'main' into docs/bb-auth-cognito-admin-design
harsh62 Jul 7, 2026
397d784
Merge branch 'main' into docs/bb-auth-cognito-admin-design
harsh62 Jul 9, 2026
5398ce8
fix(test-apps): stop declaring the standard 'email' attr in native-bi…
harsh62 Jul 9, 2026
8d2027b
feat(bb-auth-cognito): close auth.admin type-safety & ergonomics gaps
harsh62 Jul 9, 2026
1be77a9
fix(bb-auth-cognito): admin createUser attr-prefix + getUser groups (…
harsh62 Jul 16, 2026
0d62d88
fix(bb-auth-cognito): lifecycle slice self-sufficient for getUser gro…
harsh62 Jul 16, 2026
475353c
Merge remote-tracking branch 'origin/main' into docs/bb-auth-cognito-…
harsh62 Jul 16, 2026
36e8591
Merge branch 'main' into docs/bb-auth-cognito-admin-design
harsh62 Jul 21, 2026
5745210
chore(bb-auth-cognito): release auth.admin as patch, not minor
harsh62 Jul 22, 2026
e0c6fcb
chore(bb-auth-cognito): soften const O note (not a breaking change)
harsh62 Jul 22, 2026
90f0340
docs(bb-auth-cognito): fix README/DESIGN to match compile-time action…
harsh62 Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/auth-cognito-admin-handle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@aws-blocks/bb-auth-cognito": patch
---

Add an opt-in `auth.admin` handle to `AuthCognito` for server-side group-membership and user-lifecycle administration.

Enable it by passing an `admin` options object; `admin.actions` scopes both the granted `Admin*` / `List*` IAM **and** the compile-time method surface. Without it, `auth.admin` is a compile error and no admin IAM is granted (unchanged default).

```ts
const auth = new AuthCognito(scope, 'auth', { groups: ['admins'], admin: { actions: ['groups'] } });
await auth.admin.addUserToGroup('alice', 'admins');
```

The admin surface is fully typed by the pool config `O`:

- **Action gating:** calling a method whose action group wasn't granted (e.g. `deleteUser` under `actions: ['groups']`) is a compile error, and fast-fails at runtime with a clear message instead of a cryptic AWS `AccessDenied`.
- **Typed reads:** `getUser` / `scan` / `listUsersInGroup` return `AdminUser<O>` — `groups` narrows to the configured group union and `attributes` keys to the declared attributes, matching the client-side `CognitoUser`.
- **Typed writes:** `createUser`'s `attributes` narrow to the declared keys (catches typos like `signUp` does).
- **`scan(filter?)`** accepts a server-side `AdminUserFilter` mapped to Cognito's `ListUsers` `Filter`.
- **`setUserPassword(username, password, { permanent })`** takes a named options object instead of a bare boolean.

The `AuthCognito` class generic is now a `const` type parameter, so inline options literals narrow without `as const`.

Note: `const O` narrows the params of `requireRole`, `updateUserAttribute`, and `updateMFAPreference` for inline-literal options. Callers passing widened `string` variables to these may need a cast or literal arguments.
5 changes: 5 additions & 0 deletions .changeset/bb-agent-api-report-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-blocks/bb-agent": patch
---

Regenerate the API report to match the current `BedrockModels` source (the committed `API.md` had drifted from the model-id constants). No source or runtime change.
5 changes: 5 additions & 0 deletions .changeset/blocks-admin-types-reexport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-blocks/blocks": patch
---

Re-export the `auth.admin` types (`AdminOptions`, `AdminUser`, `AdminCreateInit`, `GroupAdmin`, `LifecycleAdmin`, `AdminSurface`, `AdminGetterOf`, `AdminDisabled`) from the umbrella package so consumers using `@aws-blocks/blocks` can name the values `auth.admin` returns and annotate `admin` options.
346 changes: 346 additions & 0 deletions docs/tech-design/BB-auth-cognito-admin-implementation-plan.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/tech-design/BB-auth-cognito-admin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# BB: AuthCognitoAdmin — SUPERSEDED

> **⚠️ SUPERSEDED.** The original design proposed a separate `@aws-blocks/bb-auth-cognito-admin` package. We instead ship the admin surface as an **opt-in `auth.admin` handle** on the existing `AuthCognito` class.
>
> The authoritative design and implementation plan is **[BB-auth-cognito-admin-implementation-plan.md](./BB-auth-cognito-admin-implementation-plan.md)**. For the shipped API and usage, see the [`bb-auth-cognito` README](../../packages/bb-auth-cognito/README.md) (*Admin surface*) and [DESIGN.md](../../packages/bb-auth-cognito/DESIGN.md).

## Why the direction changed

The separate-package design carried a second construct, a `/internal` subpath for sharing mock state across instances, and cross-construct wiring — all to preserve an admin/client separation that, on the shared backend-Lambda model, is enforced by API surface + lint rather than IAM anyway. The in-package handle preserves that same separation (plus a compile-time opt-in gate) at a fraction of the cost. The full trade study, type-safety analysis (including the `AuthCognito<O>` variance constraint that makes `admin.actions` scope the IAM grant rather than the typed method set), task list, and verification plan live in the implementation-plan doc.

The original 755-line design doc (API surface, error model, mock-parity research) was retired here to avoid drift: its code snippets predated the rebrand and its package/wiring model no longer matches shipped code. Its still-relevant content was folded into the implementation plan and the package README/DESIGN.
103 changes: 102 additions & 1 deletion packages/bb-auth-cognito/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,65 @@ import type { ChildLogger } from '@aws-blocks/bb-logger';
import { Scope } from '@aws-blocks/core';
import type { ScopeParent } from '@aws-blocks/core';

// @public
export type AdminAction = 'groups' | 'lifecycle';

// @public
export type AdminActionGate<O extends AuthCognitoOptions, A extends AdminAction> = AdminGrants<O, A> extends true ? [] : [ERROR_admin_action_not_granted: never];

// @public
export interface AdminCreateInit<O extends AuthCognitoOptions = AuthCognitoOptions> {
attributes?: Partial<Record<AttrOf<O>, string>>;
suppressInvite?: boolean;
temporaryPassword?: string;
}

// @public
export type AdminDisabled = {
readonly __adminNotEnabled: 'construct AuthCognito with { admin: {} }';
};

// @public
export type AdminGetterOf<O extends AuthCognitoOptions> = O extends {
admin: object;
} ? AdminSurface<O> : AdminDisabled;

// @public
export type AdminGrants<O extends AuthCognitoOptions, A extends AdminAction> = O extends {
admin: {
actions: infer L extends readonly string[];
};
} ? (A extends L[number] ? true : false) : true;

// @public
export interface AdminOptions {
actions?: readonly AdminAction[];
}

// @public
export type AdminSurface<O extends AuthCognitoOptions = AuthCognitoOptions> = GroupAdmin<O> & LifecycleAdmin<O>;

// @public
export interface AdminUser<O extends AuthCognitoOptions = AuthCognitoOptions> {
// (undocumented)
attributes: Partial<Record<ReadAttrOf<O>, string>>;
// (undocumented)
enabled: boolean;
// (undocumented)
groups?: GroupOf<O>[];
// (undocumented)
username: string;
// (undocumented)
userSub: string;
}

// @public
export interface AdminUserFilter {
attribute: string;
match: 'startsWith' | 'equals';
value: string;
}

// Warning: (ae-incompatible-release-tags) The symbol "AttrOf" is marked as @public, but its signature references "CustomAttrNames" which is marked as @internal
//
// @public
Expand All @@ -20,8 +79,9 @@ export type AttrOf<O extends AuthCognitoOptions> = O extends {
} ? StandardUserAttributeKey | CustomAttrNames<O> | `custom:${CustomAttrNames<O>}` : string;

// @public
export class AuthCognito<O extends AuthCognitoMockOptions = AuthCognitoMockOptions> extends Scope implements BlocksAuth {
export class AuthCognito<const O extends AuthCognitoMockOptions = AuthCognitoMockOptions> extends Scope implements BlocksAuth {
constructor(scope: ScopeParent, id: string, options?: O);
get admin(): AdminGetterOf<O>;
autoSignIn(context: BlocksContext): Promise<SignInResult<O>>;
checkAuth(context: BlocksContext): Promise<boolean>;
completePasskeyRegistration(context: BlocksContext, credential: string): Promise<CompletePasskeyRegistrationResult>;
Expand Down Expand Up @@ -114,6 +174,7 @@ export interface AuthCognitoMockOptions extends AuthCognitoOptions {

// @public
export interface AuthCognitoOptions {
admin?: AdminOptions;
authFlowType?: AuthFlowType;
crossDomain?: boolean;
deviceTracking?: {
Expand Down Expand Up @@ -270,6 +331,18 @@ export interface FetchAuthSessionOptions {
forceRefresh?: boolean;
}

// @public
export interface GroupAdmin<O extends AuthCognitoOptions = AuthCognitoOptions> {
// (undocumented)
addUserToGroup(username: string, group: GroupOf<O>, ...gate: AdminActionGate<O, 'groups'>): Promise<void>;
// (undocumented)
listGroupsForUser(username: string, ...gate: AdminActionGate<O, 'groups'>): Promise<GroupOf<O>[]>;
// (undocumented)
listUsersInGroup(group: GroupOf<O>, ...gate: AdminActionGate<O, 'groups'>): Promise<AdminUser<O>[]>;
// (undocumented)
removeUserFromGroup(username: string, group: GroupOf<O>, ...gate: AdminActionGate<O, 'groups'>): Promise<void>;
}

// @public
export type GroupOf<O extends AuthCognitoOptions> = O extends {
groups: readonly [unknown, ...unknown[]];
Expand All @@ -292,6 +365,28 @@ export interface JWT {
toString(): string;
}

// @public
export interface LifecycleAdmin<O extends AuthCognitoOptions = AuthCognitoOptions> {
// (undocumented)
createUser(username: string, init?: AdminCreateInit<O>, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<AdminUser<O>>;
// (undocumented)
deleteUser(username: string, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<void>;
// (undocumented)
disableUser(username: string, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<void>;
// (undocumented)
enableUser(username: string, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<void>;
// (undocumented)
getUser(username: string, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<AdminUser<O> | null>;
// (undocumented)
resetUserPassword(username: string, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<void>;
// (undocumented)
revokeUserSessions(username: string, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<void>;
// (undocumented)
scan(filter?: AdminUserFilter, ...gate: AdminActionGate<O, 'lifecycle'>): AsyncIterable<AdminUser<O>>;
// (undocumented)
setUserPassword(username: string, password: string, options?: SetPasswordOptions, ...gate: AdminActionGate<O, 'lifecycle'>): Promise<void>;
}

// @public
export function makeExternalUserPoolRef(userPoolId: string, clientId?: string): ExternalUserPoolRef;

Expand Down Expand Up @@ -376,12 +471,18 @@ export interface SessionRecord {
export class SessionStore {
constructor(scope: ScopeParent, id?: string);
createSession(record: SessionRecord): Promise<string>;
deleteByUsername(username: string): Promise<number>;
deleteSession(sessionId: string): Promise<void>;
lookupSession(sessionId: string): Promise<SessionRecord | null>;
// (undocumented)
updateSession(sessionId: string, update: Partial<SessionRecord>): Promise<void>;
}

// @public
export interface SetPasswordOptions {
permanent?: boolean;
}

// @public
export type SignInNextStep = {
name: 'CONFIRM_SIGN_IN_WITH_SMS_CODE';
Expand Down
14 changes: 12 additions & 2 deletions packages/bb-auth-cognito/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Design document. For usage, see [README.md](./README.md).
- Device tracking (list, forget).
- Password reset via verification code.
- Provider-agnostic state machine driving the same `<Authenticator>` UI as every other Blocks auth BB.
- **Admin user-lifecycle + group-membership APIs** (`createUser`, `deleteUser`, `addUserToGroup`, `listUsersInGroup`, etc.) — exposed as an **opt-in `auth.admin` handle** on this class, gated by the `admin` options object. Off by default (no handle, no `Admin*` IAM). See *Admin surface* below and [BB-auth-cognito-admin-implementation-plan.md](../../docs/tech-design/BB-auth-cognito-admin-implementation-plan.md) for the rationale.

**Out (for this BB):**

- **Admin user-lifecycle APIs** (`createUser`, `deleteUser`, `addUserToGroup`, `listUsers`, etc.) — deliberately excluded. Those operate on *any* user and can't self-gate from a `context`, so they don't belong on the same class that exposes `signIn`/`signUp`. They are provided by a separate admin Building Block.
- **HostedUI + federated sign-in** (Google / Facebook / LoginWithAmazon / Apple / OIDC / SAML) — not supported.
- **Cognito Lambda triggers** (`preSignUp`, `postConfirmation`, `preAuthentication`, …) — customers who need them can attach against the underlying `userPool` construct directly.
- **Auth flows other than `USER_PASSWORD_AUTH`** — widened typing only.
Expand All @@ -47,7 +47,7 @@ Creates under the construct's scope:
- **Nested `AppSetting(this, 'session-secret', { secret: true })`** — SSM SecureString HMAC for signing session-ID cookies. `AppSetting` handles the custom-resource wiring (CF can't create SecureString natively) and grants `ssm:GetParameter` + `kms:Decrypt` to `this.handler` automatically. Both the CDK layer and AWS runtime instantiate an `AppSetting` at the same scope path, so each side derives the same SSM parameter name without a dedicated env var.
- **Nested `KVStore(this, 'sessions', { removalPolicy })`** — DynamoDB table holding server-side session records.

The Lambda handler receives env vars `BLOCKS_AUTH_COGNITO_<UPPER_FULLID>_{USER_POOL_ID, CLIENT_ID, REGION}` so the AWS runtime can discover the pool/client IDs without CfnOutput round-trips. IAM grants are a **single** policy statement scoped to the pool ARNthe 17 client-facing Cognito actions (sign-up, sign-in, MFA, devices, self-service password/attribute mutations, `GlobalSignOut`). No `cognito-idp:Admin*` or `cognito-idp:List*` actions are granted; those belong to a separate admin Building Block.
The Lambda handler receives env vars `BLOCKS_AUTH_COGNITO_<UPPER_FULLID>_{USER_POOL_ID, CLIENT_ID, REGION}` so the AWS runtime can discover the pool/client IDs without CfnOutput round-trips. IAM grants are scoped to the pool ARN: a base policy statement covering the 17 client-facing Cognito actions (sign-up, sign-in, MFA, devices, self-service password/attribute mutations, `GlobalSignOut`) is always attached. `cognito-idp:Admin*` / `cognito-idp:List*` actions are granted **only** when the `admin` option is set — a second statement whose action set is scoped by `admin.actions` (`'groups'` and/or `'lifecycle'`). Omit `admin` and the synthesized role is byte-identical to the client-only grant (least privilege by default). See *Admin surface*.

`userPoolName` is `this.fullId`; the construct throws at synth time if the ID exceeds Cognito's 128-char limit.

Expand Down Expand Up @@ -81,6 +81,15 @@ Everything else (`username`, `userSub`, `groups`, `attributes`) is derived on re

**Challenge envelopes.** The challenge token returned by `signIn` on MFA / `NEW_PASSWORD_REQUIRED` is a small HMAC-signed envelope the client echoes back in `confirmSignIn`. Cognito's raw session token never reaches the browser; the envelope carries enough state to reconstruct `ChallengeResponses` on the server.

## Admin surface (`auth.admin`)

Server-side group-membership and user-lifecycle administration is exposed as an **opt-in handle on this class**, not a separate Building Block. Enable it by passing an `admin` options object; the handle grants the matching `Admin*` / `List*` IAM (scoped by `admin.actions`). This keeps the admin/client API distinction structural while avoiding a second package, its `/internal` mock-state-sharing port, and cross-construct wiring — the handle mutates the same live `state`/pool the client methods use. See [BB-auth-cognito-admin-implementation-plan.md](../../docs/tech-design/BB-auth-cognito-admin-implementation-plan.md) for the full rationale and the package-vs-handle trade study.

- **Compile-time gate.** The getter's type is `AdminGetterOf<O> = O extends { admin: object } ? AdminSurface<O> : AdminDisabled`. Without an `admin` object, `auth.admin` is `AdminDisabled` and any access is a compile error whose message names the fix; the getter also throws at runtime for untyped JS callers. Group names on the methods narrow via `GroupOf<O>`.
- **`actions` scopes both the IAM grant and the compile-time method surface.** Each method on `GroupAdmin<O>` / `LifecycleAdmin<O>` carries a trailing `...gate: AdminActionGate<O, A>` parameter: when `actions` grants action group `A` the gate is an empty tuple (callable normally), otherwise it demands an extra `never` argument, so an ungranted call is a **compile error**. Crucially the gate lives in a *parameter* position, not a conditional over the surface *shape* — so it does **not** make `AuthCognito<O>` invariant (a shape-narrowing conditional over `O` did, breaking assignability across the codebase — verified). `actions` therefore scopes both the CDK grant and the typed surface. Untyped JS callers that reach past the gate additionally fast-fail at runtime with a clear error.
- **Not an access boundary.** Like every block on the shared backend Lambda, client and admin run under one role; separation is by API surface + lint, not IAM. Gate every admin route behind `requireRole`.
- **Session freshness.** `revokeUserSessions` revokes Cognito refresh tokens (AWS: `AdminUserGlobalSignOut`; mock: deletes session records). On AWS an already-issued access token stays valid until it expires, so `checkAuth` does not flip immediately — a known mock-vs-AWS parity difference. Group changes likewise apply on the next sign-in / `fetchAuthSession({ forceRefresh: true })`, not to a live session.

## Options Split: `AuthCognitoOptions` vs `AuthCognitoMockOptions`

`AuthCognitoOptions` is the cross-runtime option type — CDK, AWS, and mock all accept it. The mock entry accepts a widened type:
Expand Down Expand Up @@ -134,3 +143,4 @@ Resolved by the MFA_SETUP + USER_AUTH work:
4. **Mock + AWS runtimes share the session/cookie code path.** `SessionStore`, cookie helpers, JWT decode — all shared. The only divergence is that mock issues placeholder-signature JWTs and never talks to Cognito.
5. **Error constants mirror Cognito's wire-format exception names.** Every value equals the name Cognito puts on the wire, so `isBlocksError(e, AuthCognitoErrors.X)` works identically for errors thrown by the mock (matched name directly) and errors propagated from the AWS SDK (name preserved when re-thrown as `ApiError`).
6. **`GroupNotFound` maps to `ResourceNotFoundException`.** Real Cognito returns `ResourceNotFoundException` (not the intuitive `GroupNotFoundException`) for a missing group. The constant name preserves the semantic label while the value matches what Cognito actually returns.
7. **Admin surface as an opt-in `auth.admin` handle, not a separate Building Block.** The admin/client distinction is preserved by the handle (and the `admin` opt-in gate) rather than by a second package. This removes a whole package, an `/internal` subpath for sharing mock state, and cross-construct wiring, at the cost of independent versioning (not needed). `admin.actions` scopes both the IAM grant and the compile-time method surface via a parameter-position `AdminActionGate` (ungranted calls are compile errors); the gate is a method *parameter* rather than a conditional over the surface shape, so `AuthCognito<O>` stays covariant in `O`. Full trade study in [BB-auth-cognito-admin-implementation-plan.md](../../docs/tech-design/BB-auth-cognito-admin-implementation-plan.md).
Loading
Loading