User Authentication with AWS Cognito
AWS Cognito is a managed authentication service. Instead of building user registration, login, password hashing, token management, and MFA yourself, you delegate all of it to Cognito.
It gives you:
- User registration with email/phone verification
- Secure password policies
- JWT-based sessions (Access Token, ID Token, Refresh Token)
- MFA support
- Social login (Google, Facebook, Apple) via Hosted UI
- Password reset flows out of the box
Cognito Concepts
| Term | What it is |
|---|---|
| User Pool | A directory of users. Handles registration, login, and token issuance. |
| App Client | A credential (Client ID + optionally a Client Secret) that your backend uses to call Cognito APIs. |
| Access Token | JWT used to authorize API requests. Short-lived (default 1 hour). |
| ID Token | JWT containing user identity claims (email, sub, custom attributes). |
| Refresh Token | Long-lived token used to get new Access/ID Tokens without re-login. |
Create a User Pool
- Go to AWS Console → Cognito → Create user pool
- Choose Email as the sign-in option
- Set a password policy (minimum length, required characters)
- Under App clients, create an app client — name it
my-app-backend - Choose Don't generate a client secret for a public app client (simpler)
- Note the User Pool ID and App Client ID
Add these to your .env:
.env
AWS_REGION=us-east-1
COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx
COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
Install
- npm
- Yarn
- pnpm
- Bun
npm install @aws-sdk/client-cognito-identity-provider aws-jwt-verify dotenv
yarn add @aws-sdk/client-cognito-identity-provider aws-jwt-verify dotenv
pnpm add @aws-sdk/client-cognito-identity-provider aws-jwt-verify dotenv
bun add @aws-sdk/client-cognito-identity-provider aws-jwt-verify dotenv
@aws-sdk/client-cognito-identity-provider— calls Cognito APIs (register, login, etc.)aws-jwt-verify— verifies Cognito JWTs locally without calling Cognito on every request
Cognito Client
config/cognito.js
import { CognitoIdentityProviderClient } from "@aws-sdk/client-cognito-identity-provider";
const cognito = new CognitoIdentityProviderClient({
region: process.env.AWS_REGION,
});
export default cognito;
Register a User
utils/auth.js
import {
SignUpCommand,
ConfirmSignUpCommand,
InitiateAuthCommand,
ForgotPasswordCommand,
ConfirmForgotPasswordCommand,
GlobalSignOutCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import cognito from "../config/cognito.js";
const CLIENT_ID = process.env.COGNITO_CLIENT_ID;
export async function signUp(email, password, name) {
const command = new SignUpCommand({
ClientId: CLIENT_ID,
Username: email,
Password: password,
UserAttributes: [
{ Name: "email", Value: email },
{ Name: "name", Value: name },
],
});
return cognito.send(command);
}
export async function confirmSignUp(email, code) {
const command = new ConfirmSignUpCommand({
ClientId: CLIENT_ID,
Username: email,
ConfirmationCode: code,
});
return cognito.send(command);
}
export async function signIn(email, password) {
const command = new InitiateAuthCommand({
ClientId: CLIENT_ID,
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: email,
PASSWORD: password,
},
});
const response = await cognito.send(command);
return response.AuthenticationResult;
}
export async function forgotPassword(email) {
const command = new ForgotPasswordCommand({
ClientId: CLIENT_ID,
Username: email,
});
return cognito.send(command);
}
export async function confirmForgotPassword(email, code, newPassword) {
const command = new ConfirmForgotPasswordCommand({
ClientId: CLIENT_ID,
Username: email,
ConfirmationCode: code,
Password: newPassword,
});
return cognito.send(command);
}
export async function signOut(accessToken) {
const command = new GlobalSignOutCommand({ AccessToken: accessToken });
return cognito.send(command);
}
Auth Routes
routes/auth.js
import express from "express";
import {
signUp,
confirmSignUp,
signIn,
forgotPassword,
confirmForgotPassword,
signOut,
} from "../utils/auth.js";
const router = express.Router();
router.post("/register", async (req, res) => {
const { email, password, name } = req.body;
if (!email || !password || !name) {
return res.status(400).json({ message: "email, password, and name are required" });
}
try {
await signUp(email, password, name);
res.status(201).json({ message: "Registration successful. Check your email for a verification code." });
} catch (err) {
res.status(400).json({ message: err.message });
}
});
router.post("/verify", async (req, res) => {
const { email, code } = req.body;
try {
await confirmSignUp(email, code);
res.json({ message: "Email verified. You can now log in." });
} catch (err) {
res.status(400).json({ message: err.message });
}
});
router.post("/login", async (req, res) => {
const { email, password } = req.body;
try {
const tokens = await signIn(email, password);
res.json({
accessToken: tokens.AccessToken,
idToken: tokens.IdToken,
refreshToken: tokens.RefreshToken,
expiresIn: tokens.ExpiresIn,
});
} catch (err) {
res.status(401).json({ message: err.message });
}
});
router.post("/forgot-password", async (req, res) => {
const { email } = req.body;
try {
await forgotPassword(email);
res.json({ message: "Password reset code sent to your email." });
} catch (err) {
res.status(400).json({ message: err.message });
}
});
router.post("/reset-password", async (req, res) => {
const { email, code, newPassword } = req.body;
try {
await confirmForgotPassword(email, code, newPassword);
res.json({ message: "Password reset successfully." });
} catch (err) {
res.status(400).json({ message: err.message });
}
});
router.post("/logout", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ message: "No token provided" });
try {
await signOut(token);
res.json({ message: "Logged out successfully." });
} catch (err) {
res.status(400).json({ message: err.message });
}
});
export default router;
Key Takeaways
- Cognito handles all the hard parts of auth: secure storage, hashing, verification, token rotation
- After login, the client receives three tokens — store only the Access Token in memory (or a secure cookie), not localStorage
- The Access Token is what you send in
Authorization: Bearer <token>headers - Next, see the Cognito Middleware doc to protect your routes using JWT verification