ts-otp-aws
Skill3 starsChanged 8 months agoCC0-1.0View on GitHub
Implement OTP and passwordless authentication on AWS for TypeScript projects using Cognito CUSTOM_AUTH triggers (default) or a custom DynamoDB-backed flow, with SES (email) and SNS (SMS) delivery. Use when the user mentions OTP, one-time password, passwordless login, magic link, Cognito custom auth, DefineAuthChallenge, CreateAuthChallenge, VerifyAuthChallengeResponse, SES verification email, SNS SMS code, or MFA over email/SMS. Covers architecture decision (Cognito vs custom), Lambda trigger handlers, SES/SNS notifiers, DynamoDB schema with TTL, rate limiting, constant-time comparison, threat model (enumeration, replay, brute force), and aws-sdk-client-mock testing.
In this repository48 other files it gives its agents.
- 3d-printingSkill
- app-securitySkill
- architecture-designSkill
- aws-genai-lensSkill
- aws-well-architected-frameworkSkill
- aws-well-architectedSkill
- azure-well-architectedSkill
- blog-post-with-diagramsSkill
- clean-architectureSkill
- clean-codeSkill
- databricks-genie-spaces-best-practicesSkill
- data-visualization-guideSkill
- design-an-interfaceSkill
- design-patternsSkill
- domain-driven-designSkill
- dry-principleSkill
- edit-articleSkill
- event-driven-designSkill
- git-best-practicesSkill
- git-guardrails-claude-codeSkill
- grill-meSkill
- hexagonal-architectureSkill
- improve-codebase-architectureSkill
- language-conventions-source-en-docs-brSkill
- learn-electronicsSkill
- linked-dataSkill
- microservices-skillSkill
- object-oriented-programmingSkill
- oop-design-patternsSkill
- oreilly-design-referenceSkill
- organize-docs-from-zipSkill
- prd-to-issuesSkill
- prd-to-planSkill
- project-context-syncSkill
- request-refactor-planSkill
- rfc2119Skill
- scaffold-exercisesSkill
- semverSkill
- setup-pre-commitSkill
- solid-principlesSkill
- tddSkill
- todoSkill
- triage-issueSkill
- ubiquitous-languageSkill
- ux-design-principlesSkill
- write-a-prdSkill
- write-a-skillSkill
- yagni-principleSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 67
No user pool, single-purpose API, or you already issue your own JWTs → custom DynamoDB (jump to references/custom-dynamodb-otp.md).
- Very specific: line 137
When Cognito is not an option, follow references/custom-dynamodb-otp.md.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
ts-otp-aws
Implement OTP / passwordless authentication on AWS for TypeScript projects.
When to use
- Adding email or SMS verification codes to a TypeScript app on AWS Lambda.
- Wiring or debugging Cognito
CUSTOM_AUTHtriggers (DefineAuthChallenge,CreateAuthChallenge,VerifyAuthChallengeResponse,PreSignUp). - Standing up a non-Cognito OTP flow on DynamoDB with TTL.
Default approach
Cognito User Pool with CUSTOM_AUTH flow + four Lambda triggers, SESv2 for email, SNS for SMS. The user pool stays the source of truth; the four triggers handle OTP generation, delivery, and verification. Drop-in handlers and Terraform live in assets/. The custom-DynamoDB alternate is documented in references/custom-dynamodb-otp.md — pick it only when you can't use Cognito.
Cognito vs custom — quick decision
| If you need… | Pick |
| Managed user directory, JWTs, MFA toggle | Cognito CUSTOM_AUTH |
| Standalone API, no user pool, full control over JWT | custom DynamoDB |
| Federation (Google/Apple) alongside OTP | Cognito (out of scope here) |
| Lowest latency, no cold-start triggers | custom DynamoDB |
Full matrix + threat model: references/architecture-decision.md.
Architecture at a glance
┌────────┐ InitiateAuth(CUSTOM_AUTH) ┌──────────────┐ Define→Create ┌──────────────────┐
│ client │ ───────────────────────────▶ │ Cognito │ ───────────────▶ │ auth-triggers λ │
│ (Next/ │ ◀── session + masked hint ── │ User Pool │ │ ─ create-chall │
│ Astro) │ │ │ │ ─ define-chall │
│ │ Respond(answer=otp) │ │ │ ─ verify-chall │
│ │ ───────────────────────────▶ │ │ │ ─ pre-signup │
│ │ ◀── id/access/refresh ────── │ │ └────┬──────┬──────┘
└────────┘ └──────────────┘ │ │
SESv2│ │SNS
┌───────▼──┐ ┌─▼──────┐
│ email │ │ SMS │
└──────────┘ └────────┘Custom path swaps Cognito + triggers for one Lambda + a DynamoDB OTP table (PK/SK + ttl).
Step 1 — Audit the project
scripts/audit-project.sh /path/to/repoReports which patterns the repo already has (Cognito CUSTOM_AUTH, custom DDB, client-only) and what's missing. Exits non-zero on missing rg. See scripts/README.md for sample output.
Step 2 — Pick the path
- Existing Cognito user pool, or you want managed JWT issuance → Cognito CUSTOM_AUTH (continue to Step 3).
- No user pool, single-purpose API, or you already issue your own JWTs → custom DynamoDB (jump to
references/custom-dynamodb-otp.md).
Step 3 — Drop in handlers (Cognito path)
Copy the five trigger handlers and two notifiers verbatim:
assets/handlers/ → lambda/auth-triggers/src/handlers/
assets/notifiers/ → lambda/auth-triggers/src/notifiers/Required env vars on the Lambda:
| Var | Purpose |
SES_FROM | Verified SES sender (e.g. noreply@yourdomain.com) |
SMS_SENDER_ID | 3–11 alphanumeric SMS sender (default OTP) |
OTP_DEV_BYPASS | Set to 1 only in non-prod to accept 000000 |
APP_NAME | Brand string used in email/SMS body |
The dispatcher (handler.ts) routes by event.triggerSource. Walkthrough of each trigger and the phantom-user (prevent_user_existence_errors) path: references/cognito-custom-auth.md.
Step 4 — Wire infrastructure (Terraform)
Minimum viable set under assets/terraform/:
cognito-userpool.tf— pool + 4 trigger ARN bindings,prevent_user_existence_errors=ENABLED, no MFA (OTP is the auth).auth-triggers-lambda.tf— Node 22 ESM Lambda, log retention, IAM role.ses-identity.tf— domain identity + DKIM + custom MAIL FROM.sns-sms.tf—MonthlySpendLimit,DefaultSMSType=Transactional, opt-out preferences.dynamodb-otp-table.tf— only for the custom path (PK/SK +ttl+ PITR + deletion protection).
IAM least-privilege policies: assets/iam/auth-trigger-policy.json and assets/iam/custom-otp-policy.json.
Step 5 — Frontend integration
Server-side adapter calling InitiateAuthCommand + RespondToAuthChallengeCommand lives at assets/frontend/cognito-server-snippet.ts. Key points:
AuthFlow: "CUSTOM_AUTH", passClientMetadata: { channel: "email" | "sms" }.- For phone identifiers,
ListUserswithphone_number = "..."filter; pass through unknown identifiers unchanged so Cognito's phantom-user path stays opaque. - Error mapping:
NotAuthorizedException→ session expired; fresh challenge response → wrong code.
Step 6 — Security must-haves
- Constant-time OTP comparison (
crypto.timingSafeEqual); reject early if not 6 ASCII digits. - 3-attempt cap inside
define-challenge.ts; surface asfailAuthentication=true. - 5-minute OTP TTL; never log the code.
- Phantom-user safety: never throw on
userNotFound=true— silently return a synthetic challenge or you'll leak existence. - SES out of sandbox, MAIL FROM domain set, DKIM aligned. SNS spend limit + transactional + sender-ID per region.
- Mask channel hint (
***1234, not+5511915551234).
Full checklist: references/security-checklist.md.
Step 7 — Test
aws-sdk-client-mock v4, one mock per service. Inline minimum:
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
import { mockClient } from "aws-sdk-client-mock";
const ses = mockClient(SESv2Client);
ses.on(SendEmailCommand).resolves({});
// run handler, then:
expect(ses.commandCalls(SendEmailCommand)).toHaveLength(1);State-machine and phantom-user assertions: references/testing-patterns.md.
Custom DynamoDB alternate + second factor
When Cognito is not an option, follow references/custom-dynamodb-otp.md. For layering TOTP authenticator apps or WebAuthn passkeys on top of OTP, see references/second-factor.md — kept brief on purpose.
Channel deliverability (SES sandbox/DKIM, SNS 10DLC/DLT/origination): references/messaging-channels.md. Source attributions: references/citations.md.
Validation
skills-ref validate /home/kayaman/Projects/ts-otp-aws-skill(frontmatter + naming).scripts/audit-project.sh ~/Projects/ai-assisted-devshould say "Cognito CUSTOM_AUTH — complete".wc -l SKILL.md< 200; eachreferences/*.md< 500.
Out of scope
- Pinpoint marketing campaigns (this is transactional auth).
- Cognito IdP federation (Google/Apple).
- Full WebAuthn/passkey or HOTP/TOTP authenticator-app implementations.
- UI component libraries (an
OtpCodeInputis referenced but not bundled). - Generic IAM/Lambda/Terraform fundamentals.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

