Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/cognito-prevent-user-enumeration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-blocks/bb-auth-cognito": patch
---

Enable `PreventUserExistenceErrors` on the Cognito user pool client. Sign-in and forgot-password responses now return a uniform error regardless of whether the username exists, closing the account-enumeration oracle that Cognito exposes by default (distinct `UserNotFoundException` vs. wrong-password errors).
2 changes: 2 additions & 0 deletions packages/bb-auth-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ Cognito scales automatically. Default quotas: 40 sign-ups/sec, 120 sign-ins/sec

AWS Blocks auth follows the BFF pattern: the browser sends `{username, password}` to the customer's Lambda over TLS; Lambda forwards to Cognito. The customer's Lambda is inside the user's trust boundary by design — same as `AuthBasic`, `AuthOIDC`, NextAuth, Devise, and every server-mediated auth library. Cognito tokens never reach the browser — instead, the BB issues an opaque HMAC-signed session cookie that maps to a server-side `SessionRecord` in a nested `KVStore`.

The user pool client sets `PreventUserExistenceErrors: ENABLED`, so sign-in and forgot-password responses return a uniform error whether or not the username exists — closing the account-enumeration oracle Cognito exposes by default.

See the auth-cognito technical design (see source repo) for the full architecture and mock-vs-AWS parity notes.

## Cookies and sessions
Expand Down
9 changes: 9 additions & 0 deletions packages/bb-auth-cognito/src/index.cdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ describe('AuthCognito (CDK) — user pool client', () => {
});
});

test('client enables PreventUserExistenceErrors (no username enumeration oracle)', () => {
const template = synth((stack) => {
new AuthCognito(scope(stack), 'auth');
});
template.hasResourceProperties('AWS::Cognito::UserPoolClient', {
PreventUserExistenceErrors: 'ENABLED',
});
});

test('hosted-UI / OAuth flows are disabled (no implicit grant, no placeholder callback)', () => {
const template = synth((stack) => {
new AuthCognito(scope(stack), 'auth');
Expand Down
6 changes: 6 additions & 0 deletions packages/bb-auth-cognito/src/index.cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
export class AuthCognito<const O extends AuthCognitoOptions = AuthCognitoOptions> extends Scope {
public readonly userPool: cognito.IUserPool;
public readonly userPoolClient: cognito.IUserPoolClient;
private readonly sessions: KVStore;

Check warning on line 70 in packages/bb-auth-cognito/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedPrivateClassMembers

This private class member is defined but never used.
/** Admin opt-in, captured for the IAM grant in `grantCognitoPermissions`. */
private readonly adminOptions?: AdminOptions;

Expand Down Expand Up @@ -238,6 +238,12 @@
this.userPoolClient = new cognito.UserPoolClient(this, 'client', {
userPool: this.userPool,
generateSecret: false,
// Return a uniform error for "user doesn't exist" and "wrong
// password" so sign-in / forgot-password responses can't be used to
// enumerate which usernames are registered. Without this, Cognito
// leaks a distinct UserNotFoundException, which is an account-
// enumeration oracle. Amazon's recommended posture is ENABLED.
preventUserExistenceErrors: true,
// SDK + session-cookie auth only; the hosted UI is never used.
// Off by default CDK would enable the implicit grant and a
// placeholder example.com callback — unused attack surface.
Expand Down
Loading