@davidwells/cogneato
v0.6.0
Published
low level implementation lib for use with aws cognito srp login
Readme
@davidwells/cogneato
Low-level TypeScript helpers for AWS Cognito SRP login, refresh tokens, remembered devices, MFA challenges, sign-up, account operations, and identity-pool credential exchange.
npm install @davidwells/cogneatoTL;DR
The Problem: Cognito's SRP auth flow is secure, but wiring it by hand means implementing SRP math, InitiateAuth, challenge responses, MFA loops, device confirmation, refresh tokens, and user account operations against low-level AWS JSON APIs.
The Solution: Cogneato wraps those Cognito calls in small TypeScript functions while keeping the flow explicit. srpLogin() is an async generator, so your UI or service decides how to handle NEW_PASSWORD_REQUIRED, SOFTWARE_MFA_REQUIRED, SMS_MFA_REQUIRED, successful tokens, and errors.
Why Use Cogneato?
| Feature | What It Does |
|---------|--------------|
| SRP login generator | Starts USER_SRP_AUTH, responds to password verifier challenges, yields MFA/new-password steps, and returns tokens. |
| Remembered devices | Confirms new devices, stores reusable device credentials, and can reuse known devices on later logins. |
| MFA helpers | Supports software-token MFA, SMS MFA, software-token association, token verification, and user MFA preference updates. |
| User operations | Wraps sign-up, confirmation, password reset, password change, attribute verification, sign-out, and token revocation. |
| Identity pools | Calls Cognito Identity GetId and GetCredentialsForIdentity for identity-pool credentials. |
| Runtime-light design | Uses direct Cognito JSON requests instead of requiring the full AWS SDK at runtime. |
Quick Example
import { srpLogin, refresh, changePassword } from "@davidwells/cogneato";
const login = srpLogin({
region: "us-west-2",
userPoolId: "us-west-2_abc123",
clientId: "4exampleclientid",
username: "[email protected]",
password: "CorrectHorseBatteryStaple1!",
device: undefined,
autoConfirmDevice: true,
autoRememberDevice: "remembered",
});
let step = await login.next();
if (step.value.code === "SOFTWARE_MFA_REQUIRED") {
step = await login.next("123456");
}
if (step.value.code === "NEW_PASSWORD_REQUIRED") {
step = await login.next("NewPassword1!");
}
if (step.value.code === "ERROR") {
throw step.value.error;
}
const auth = step.value.response!;
await refresh({
region: "us-west-2",
clientId: "4exampleclientid",
refreshToken: auth.tokens.refreshToken,
deviceKey: auth.newDevice?.key,
});
await changePassword({
region: "us-west-2",
accessToken: auth.tokens.accessToken,
previousPassword: "CorrectHorseBatteryStaple1!",
proposedPassword: "NewPassword1!",
});Design Philosophy
Keep Cognito Explicit
Cogneato does not hide Cognito behind a large session manager. It exposes the flow as functions and return values you can store, inspect, and test:
const step = await login.next();
switch (step.value.code) {
case "SOFTWARE_MFA_REQUIRED":
case "SMS_MFA_REQUIRED":
case "NEW_PASSWORD_REQUIRED":
case "TOKENS":
case "ERROR":
break;
}Make Interactive Auth Natural
SRP login is not always a single request. Users may need to enter a new password, a TOTP code, or an SMS code. The async generator shape lets the caller provide those values when Cognito asks for them.
Store Only What You Need
When Cognito returns NewDeviceMetadata, Cogneato can confirm the device and return the reusable device key, group key, and generated device password:
const device = auth.newDevice && {
key: auth.newDevice.key,
groupKey: auth.newDevice.groupKey,
password: auth.newDevice.password!,
};Use Small Operation Wrappers
Account operations map closely to Cognito API names, so tests and callers can reason about the exact request being made:
await updateUserAttributes({
region,
accessToken,
userAttributes: [{ Name: "custom:tenantId", Value: "tenant-123" }],
});How Cogneato Compares
| Capability | Cogneato | AWS Amplify Auth | AWS SDK CognitoIdentityProvider | Hand-rolled Cognito calls | |------------|----------|------------------|----------------------------------|---------------------------| | SRP math included | Yes | Yes | No | You write it | | Async challenge flow | Explicit generator | Managed internally | Manual | Manual | | Remembered device support | Built in | Framework-managed | Manual | Manual | | Direct Cognito operations | Yes | Partly abstracted | Yes | Yes | | Runtime size/control | Small focused package | Larger framework | Larger SDK client | Depends on your code | | Hosted UI/OAuth abstraction | No | Yes | No | No | | Best fit | Custom auth flows needing precise control | App teams using Amplify conventions | Backend services already using AWS SDK | Specialized experiments |
Use Cogneato when:
- You need Cognito SRP without adopting Amplify.
- You want to own UI state for MFA, forced password changes, and device prompts.
- You want small wrappers around Cognito user-pool and identity-pool operations.
Cogneato may not be ideal when:
- You want Cognito Hosted UI, OAuth redirects, or social-provider flows.
- Your app already standardizes on Amplify Auth.
- You need admin-only Cognito APIs such as
AdminCreateUserorAdminInitiateAuth.
Installation
npm
npm install @davidwells/cogneatopnpm
pnpm add @davidwells/cogneatoyarn
yarn add @davidwells/cogneatoFrom Source
git clone https://github.com/DavidWells/saaslayer.git
cd saaslayer/packages/cogneato
npm install
npm run buildThe package publishes CommonJS output and TypeScript declarations from dist/.
Quick Start
1. Configure your app client
Use a Cognito user-pool app client that supports USER_SRP_AUTH. Collect these values from your infrastructure outputs:
const cognito = {
region: "us-west-2",
userPoolId: "us-west-2_abc123",
clientId: "4exampleclientid",
};2. Run SRP login
import { srpLogin } from "@davidwells/cogneato";
const login = srpLogin({
...cognito,
username: "[email protected]",
password: "CorrectHorseBatteryStaple1!",
device: undefined,
autoConfirmDevice: true,
autoRememberDevice: "remembered",
clientMetadata: { source: "web" },
});3. Handle challenge steps
let result = await login.next();
while (!result.done) {
if (result.value.code === "SOFTWARE_MFA_REQUIRED") {
result = await login.next(await promptForTotpCode());
continue;
}
if (result.value.code === "SMS_MFA_REQUIRED") {
console.log(`Code sent to ${result.value.hint}`);
result = await login.next(await promptForSmsCode());
continue;
}
if (result.value.code === "NEW_PASSWORD_REQUIRED") {
result = await login.next(await promptForNewPassword());
continue;
}
throw result.value.error;
}
if (result.value.code === "ERROR") {
throw result.value.error;
}
const auth = result.value.response!;4. Persist remembered-device metadata
if (auth.newDevice?.password) {
await saveDeviceForUser(auth.username, {
key: auth.newDevice.key,
groupKey: auth.newDevice.groupKey,
password: auth.newDevice.password,
});
}5. Reuse the device on the next login
const savedDevice = await loadDeviceForUser("[email protected]");
const login = srpLogin({
...cognito,
username: "[email protected]",
password: "CorrectHorseBatteryStaple1!",
device: savedDevice,
autoConfirmDevice: true,
autoRememberDevice: "remembered",
});API Reference
srpLogin(params)
Runs Cognito USER_SRP_AUTH as an async generator.
const login = srpLogin({
region: "us-west-2",
userPoolId: "us-west-2_abc123",
clientId: "4exampleclientid",
username: "[email protected]",
password: "CorrectHorseBatteryStaple1!",
device: undefined,
autoConfirmDevice: true,
autoRememberDevice: "remembered",
clientMetadata: { source: "web" },
debugTracing: false,
});Generator step codes:
| Code | Meaning | Caller Response |
|------|---------|-----------------|
| TOKENS | Login completed. | Read step.value.response. |
| SOFTWARE_MFA_REQUIRED | Cognito requires a six-digit TOTP code. | Call login.next("123456"). |
| SMS_MFA_REQUIRED | Cognito requires a six-digit SMS code. | Call login.next("123456"). |
| NEW_PASSWORD_REQUIRED | Cognito requires a password reset during login. | Call login.next("NewPassword1!"). |
| ERROR | Login failed. | Read or throw step.value.error. |
Successful response shape:
type TAuthResponse = {
username: string;
tokens: {
accessToken: string;
refreshToken: string;
idToken: string;
tokenType: string;
expiresIn: number;
};
newDevice?: {
key: string;
groupKey: string;
password?: string;
deviceAutoConfirmed: boolean;
deviceAutoRemembered?: "remembered" | "not_remembered";
userConfirmationNecessary?: boolean;
};
};refresh(params)
Refreshes access and ID tokens with a refresh token.
import { refresh } from "@davidwells/cogneato";
const tokens = await refresh({
region: "us-west-2",
clientId: "4exampleclientid",
refreshToken: "refresh-token",
deviceKey: "device-key",
});Sign-Up and Confirmation
import {
signUp,
confirmSignUp,
resendConfirmationCode,
} from "@davidwells/cogneato";
await signUp({
region,
clientId,
username: "[email protected]",
password: "S3cret!pass",
userAttributes: [
{ Name: "email", Value: "[email protected]" },
{ Name: "custom:orgId", Value: "org-123" },
],
clientMetadata: { source: "web" },
});
await confirmSignUp({
region,
clientId,
username: "[email protected]",
confirmationCode: "123456",
});
await resendConfirmationCode({
region,
clientId,
username: "[email protected]",
});Password Reset and Password Change
import {
forgotPassword,
confirmForgotPassword,
changePassword,
} from "@davidwells/cogneato";
await forgotPassword({
region,
clientId,
username: "[email protected]",
});
await confirmForgotPassword({
region,
clientId,
username: "[email protected]",
confirmationCode: "123456",
password: "NewPassword1!",
});
await changePassword({
region,
accessToken,
previousPassword: "OldPassword1!",
proposedPassword: "NewPassword1!",
});User Profile and Account Operations
import {
getUser,
updateUserAttributes,
getUserAttributeVerificationCode,
verifyUserAttribute,
globalSignOut,
revokeToken,
} from "@davidwells/cogneato";
const user = await getUser({ region, accessToken });
await updateUserAttributes({
region,
accessToken,
userAttributes: [{ Name: "email", Value: "[email protected]" }],
clientMetadata: { source: "settings" },
});
await getUserAttributeVerificationCode({
region,
accessToken,
attributeName: "email",
});
await verifyUserAttribute({
region,
accessToken,
attributeName: "email",
code: "123456",
});
await globalSignOut({ region, accessToken });
await revokeToken({
region,
clientId,
token: refreshToken,
clientSecret: "optional-client-secret",
});Software Token MFA
import {
associateSoftwareToken,
verifySoftwareToken,
setUserMfaPreference,
} from "@davidwells/cogneato";
const association = await associateSoftwareToken({
region,
accessToken,
});
await verifySoftwareToken({
region,
accessToken,
userCode: "123456",
friendlyDeviceName: "Work phone",
});
await setUserMfaPreference({
region,
accessToken,
softwareTokenMfaSettings: { Enabled: true, PreferredMfa: true },
smsMfaSettings: { Enabled: false, PreferredMfa: false },
emailMfaSettings: { Enabled: false, PreferredMfa: false },
webAuthnMfaSettings: { Enabled: false },
});associateSoftwareToken() and verifySoftwareToken() also accept a challenge session instead of an accessToken when Cognito returns one during auth setup.
Identity Pool Credentials
import {
getIdentityId,
getCredentialsForIdentity,
} from "@davidwells/cogneato";
const { IdentityId } = await getIdentityId({
region,
identityPoolId: "us-west-2:identity-pool-id",
logins: {
"cognito-idp.us-west-2.amazonaws.com/us-west-2_abc123": idToken,
},
});
const credentials = await getCredentialsForIdentity({
region,
identityId: IdentityId,
logins: {
"cognito-idp.us-west-2.amazonaws.com/us-west-2_abc123": idToken,
},
});Low-Level Cognito Operations
Cogneato also exports lower-level auth operation helpers used by srpLogin():
| Export | Cognito Operation |
|--------|-------------------|
| initiateUserSRPAuth | InitiateAuth with USER_SRP_AUTH |
| respondPasswordVerifier | RespondToAuthChallenge with PASSWORD_VERIFIER |
| respondSoftwareTokenMfa | RespondToAuthChallenge with SOFTWARE_TOKEN_MFA |
| respondSmsMfa | RespondToAuthChallenge with SMS_MFA |
| respondNewPasswordRequired | RespondToAuthChallenge with NEW_PASSWORD_REQUIRED |
| respondDeviceSRPAuth | RespondToAuthChallenge with DEVICE_SRP_AUTH |
| confirmDevice | ConfirmDevice plus optional device remembered status |
Prefer srpLogin() unless you need to assemble a custom Cognito challenge flow.
Configuration
Cogneato does not require a config file. Most apps keep Cognito deployment outputs in environment variables or generated service config.
export const authConfig = {
region: process.env.COGNITO_REGION!,
userPoolId: process.env.COGNITO_USER_POOL_ID!,
clientId: process.env.COGNITO_CLIENT_ID!,
identityPoolId: process.env.COGNITO_IDENTITY_POOL_ID,
};Example .env values:
COGNITO_REGION=us-west-2
COGNITO_USER_POOL_ID=us-west-2_abc123
COGNITO_CLIENT_ID=4exampleclientid
COGNITO_IDENTITY_POOL_ID=us-west-2:00000000-0000-0000-0000-000000000000For SaaSLayer services, prefer generated deployer outputs or module manifests over hard-coded stack assumptions.
Architecture
Application UI / Service
|
| imports @davidwells/cogneato
v
+-----------------------------+
| High-Level Auth Helpers |
| - srpLogin generator |
| - refresh |
| - account operations |
| - MFA setup helpers |
+-----------------------------+
|
v
+-----------------------------+
| Cognito Operation Wrappers |
| - InitiateAuth |
| - RespondToAuthChallenge |
| - ConfirmDevice |
| - SignUp / ConfirmSignUp |
| - GetId / Credentials |
+-----------------------------+
|
v
+-----------------------------+
| SRP and Platform Layer |
| - SRP math |
| - device verifier |
| - fetch / crypto wrappers |
| - Cognito JSON request |
+-----------------------------+
|
v
AWS Cognito User Pools and Identity PoolsTroubleshooting
Incorrect username or password.
Cognito returns this for bad credentials and, depending on app-client settings, may also return it when the user does not exist.
const result = await login.next();
if (result.value.code === "ERROR") {
console.error(result.value.error?.message);
}Expected 6 digit MFA code
srpLogin() validates MFA input before sending it to Cognito. Pass a string containing exactly six digits.
await login.next("123456");NEW_PASSWORD_REQUIRED never completes
The generator expects the next value to be the new password string.
let step = await login.next();
if (step.value.code === "NEW_PASSWORD_REQUIRED") {
step = await login.next("NewPassword1!");
}Device reuse fails with Missing deviceParams
If you pass a remembered device, include all three persisted fields.
const device = {
key: saved.key,
groupKey: saved.groupKey,
password: saved.password,
};refresh() succeeds but does not return a refresh token
Cognito refresh-token auth usually returns a new access token and ID token, not a new refresh token. Keep the original refresh token until your app signs out or revokes it.
Browser build cannot find Node globals
This package targets ES2017/CommonJS and uses platform wrappers for crypto and request behavior. If your bundler complains about Node globals, check your bundler's CommonJS handling and runtime crypto support.
Limitations
| Capability | Current State | Workaround | |------------|---------------|------------| | Hosted UI and OAuth redirects | Not included | Use Cognito Hosted UI, Amplify Auth, or your OAuth client. | | Admin Cognito APIs | Not included | Use AWS SDK admin APIs from trusted backend services. | | Full session manager | Not included | Store tokens/device metadata in your application layer. | | WebAuthn authentication flow | Preference settings are typed, but auth flow helpers are not implemented here. | Use Cognito-supported WebAuthn flows separately. | | SecretHash app clients | Not modeled across all operations. | Prefer public app clients for SRP flows or add server-side signing. |
FAQ
Is this a replacement for AWS Amplify Auth?
No. It is a focused Cognito helper package for teams that want direct control over SRP login and Cognito operations without adopting Amplify's broader framework.
Does Cogneato store tokens?
No. It returns tokens and device metadata to the caller. Your app chooses where and how to store them.
Can I use it in a browser?
The package exposes browser-compatible entry metadata and platform wrappers, but you should verify your bundler and runtime crypto support in your target browsers.
Does it support MFA?
Yes. srpLogin() handles software-token MFA and SMS MFA challenges. The package also includes helpers for associating, verifying, and preferring software-token MFA.
Does it support remembered devices?
Yes. Set autoConfirmDevice: true and autoRememberDevice: "remembered" to confirm and remember new devices, then persist the returned newDevice metadata for the next login.
Why is login an async generator?
Cognito auth is interactive. A single login may require MFA, a forced password update, or a device challenge. An async generator lets your app pause for user input and resume the same auth flow.
Where does this package live now?
The canonical source is the SaaSLayer monorepo:
/packages/cogneatoRepository: https://github.com/DavidWells/saaslayer/tree/master/packages/cogneato
What is the project history?
Cogneato began as a fork of the franken-srp package, then grew into a broader set of Cognito SRP login, device, MFA, account-operation, and identity-pool helpers.
The old standalone repository at https://github.com/DavidWells/cogneato is retained as a historical pointer only.
Development
npm install
npm run build
npm testIntegration-test helpers under src/test/cdk can deploy Cognito resources for live Cognito testing:
npm run cdkdeploy
npm run createTestUsers
npm test
npm run cdkdestroyLicense
MIT. See LICENSE.
