We need two Lambda functions. Create them one at a time.
This function receives email and password, authenticates against Cognito, and returns a JWT token.
loginFunctionlambda-workshop-role
| Key | Value |
|---|---|
CLIENT_ID | Your Cognito App client ID |
CLIENT_SECRET | Your Cognito App client secret |

import {
CognitoIdentityProviderClient,
InitiateAuthCommand
} from "@aws-sdk/client-cognito-identity-provider";
import crypto from "crypto";
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const cognito = new CognitoIdentityProviderClient({});
function generateSecretHash(username) {
return crypto
.createHmac("sha256", CLIENT_SECRET)
.update(username + CLIENT_ID)
.digest("base64");
}
export const handler = async (event) => {
try {
const body = JSON.parse(event.body || "{}");
const { email, password } = body;
if (!email || !password) {
return {
statusCode: 400,
body: JSON.stringify({ message: "email and password are required" })
};
}
const result = await cognito.send(new InitiateAuthCommand({
AuthFlow: "USER_PASSWORD_AUTH",
ClientId: CLIENT_ID,
AuthParameters: {
USERNAME: email,
PASSWORD: password,
SECRET_HASH: generateSecretHash(email)
}
}));
const tokens = result.AuthenticationResult;
return {
statusCode: 200,
body: JSON.stringify({
accessToken: tokens.AccessToken,
idToken: tokens.IdToken,
refreshToken: tokens.RefreshToken
})
};
} catch (err) {
if (err.name === "NotAuthorizedException") {
return {
statusCode: 401,
body: JSON.stringify({ message: "Incorrect email or password" })
};
}
return {
statusCode: 500,
body: JSON.stringify({ message: "Internal server error" })
};
}
};
We return idToken because that is the token API Gateway’s JWT Authorizer will validate. The accessToken is for calling AWS services directly.
This function reads all songs from DynamoDB and returns them. It will be protected by a JWT Authorizer — Lambda itself does not need to verify the token.
getSongsFunctionlambda-workshop-role
| Key | Value |
|---|---|
SONGS_TABLE | songs |
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.SONGS_TABLE;
export const handler = async (event) => {
try {
const result = await docClient.send(new ScanCommand({
TableName: TABLE
}));
return {
statusCode: 200,
body: JSON.stringify({
songs: result.Items,
count: result.Count
})
};
} catch (err) {
return {
statusCode: 500,
body: JSON.stringify({ message: "Internal server error" })
};
}
};
We use Scan here to keep things simple. In a real app with many songs, you would use Query with an index instead.