[AWS Cognito] Solving SMS Failures: Integrate Domestic Custom SMS via Lambda Trigger in 5 Mins
2026-05-22T01:03:05.507Z
The Nightmare of AWS Cognito SMS Authentication
If you are an indie developer, a freelancer, or working at an early-stage startup, AWS Cognito is often the go-to solution for managing user authentication and authorization. It scales effortlessly and integrates perfectly into the AWS ecosystem. However, when you try to implement SMS authentication (OTP) for users in certain regions—especially in countries like South Korea (+82)—you immediately hit a massive roadblock.
By default, AWS Cognito relies on Amazon SNS (Simple Notification Service) to dispatch SMS text messages. Unfortunately, due to strict local telecom regulations and aggressive anti-spam filtering, SMS messages sent via AWS SNS are notoriously unreliable in these regions. Messages are frequently delayed by minutes, or completely blocked by telecom carriers. To make matters worse, the cost per SMS via SNS is remarkably high, draining the limited budget of a side project or a startup MVP.
The Barrier of Local SMS Providers
The logical solution seems simple: "I will just replace AWS SNS with a local domestic SMS provider API."
But as soon as you open the documentation for traditional local SMS vendors, you face an entirely different nightmare:
- Endless Paperwork: You are required to submit business registration certificates and telecommunication service usage proofs.
- Caller ID Pre-registration: Due to caller ID spoofing laws, you must pre-register your sending phone number, which requires a manual review process taking 3 to 5 business days.
- Legacy APIs: You often have to deal with SOAP APIs, XML payloads, or outdated authentication mechanisms.
For an indie developer or a startup trying to launch an MVP this weekend, spending a week exchanging emails for legal compliance just to send a 6-digit OTP code is a complete dealbreaker.
The 5-Minute Solution: Enter EasyAuth
This exact frustration is why EasyAuth was born. EasyAuth is an ultra-simple, developer-centric SMS authentication API designed to get out of your way.
- Zero Paperwork: No business licenses or compliance documents required. Anyone can use it.
- Instant Setup: Sign up, grab your API key, and integrate in under 5 minutes.
- Automatic Caller ID: No need to pre-register phone numbers. EasyAuth's system automatically handles compliant caller IDs on your behalf.
- Cost-Effective: Drastically cheaper than AWS SNS and traditional vendors, costing only about $0.011 to $0.018 (15~25 KRW) per message.
- Free Trial: You get 10 free SMS credits immediately upon signup to test your integration.
The true beauty of EasyAuth lies in its API design. You only need two endpoints:
POST /send- Sends the OTP to the user.POST /verify- Verifies the code the user entered.
EasyAuth handles the entire state management, OTP generation, and expiration logic. In this comprehensive guide, we will learn how to bypass AWS SNS completely and integrate EasyAuth into AWS Cognito using the Custom Authentication Challenge Lambda Triggers.
Architectural Decision: Custom SMS Sender vs. Custom Auth Challenge
AWS Cognito offers two primary ways to customize SMS delivery:
1. Custom SMS Sender Trigger
In this flow, Cognito generates the OTP code, encrypts it using an AWS KMS (Key Management Service) key, and triggers your Lambda function. Your Lambda must decrypt the code and then call a 3rd-party SMS API to send it.
The Problem: Managing AWS KMS keys adds unnecessary infrastructure complexity. Furthermore, this forces you to use Cognito's OTP state, which means you cannot fully utilize EasyAuth's elegant /verify endpoint.
2. Custom Authentication Challenge Trigger
In this flow, you take full control of the authentication state machine. You use a trio of Lambda functions (Define, Create, Verify). Cognito delegates the challenge entirely to you.
The Advantage: This is the perfect match for EasyAuth. We don't need AWS KMS. We can simply trigger EasyAuth's POST /send in the Create step, and trigger POST /verify in the Verify step. EasyAuth acts as the ultimate stateless OTP engine for our Cognito User Pool.
Let's implement the Custom Authentication Challenge flow step-by-step.
Step-by-Step Implementation Guide
We will write three separate Node.js Lambda functions. Ensure that your Lambda execution roles have basic execution permissions (CloudWatch logs), and add your EASYAUTH_API_KEY to the Lambda environment variables.
Step 1: Define Auth Challenge Lambda
This function acts as the orchestrator. It decides whether to present a challenge, issue JWT tokens (if successful), or deny access (if failed).
// defineAuthChallenge.js
exports.handler = async (event) => {
// Check if this is the first authentication attempt
if (event.request.session && event.request.session.length === 0) {
// Initiate our custom challenge
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
// Check if the user successfully answered the previous challenge
else if (
event.request.session &&
event.request.session.length > 0 &&
event.request.session.slice(-1)[0].challengeResult === true
) {
// The code was correct, issue JWT tokens!
event.response.issueTokens = true;
event.response.failAuthentication = false;
}
// Max attempts reached or invalid state
else {
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
return event;
};
Step 2: Create Auth Challenge Lambda
This function is invoked when Cognito needs to present the challenge to the user. This is where we call EasyAuth to dispatch the SMS.
// createAuthChallenge.js
exports.handler = async (event) => {
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
const phoneNumber = event.request.userAttributes.phone_number;
// Only send the SMS on the first attempt to prevent spamming
// if the user inputs the wrong code and retries.
if (event.request.session && event.request.session.length === 0) {
try {
// Call EasyAuth API to send the OTP
const response = await fetch('https://api.easyauth.co/v1/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EASYAUTH_API_KEY}`
},
body: JSON.stringify({ phone: phoneNumber })
});
if (!response.ok) {
const errorData = await response.text();
console.error("EasyAuth API Error:", errorData);
throw new Error('Failed to send SMS via EasyAuth');
}
} catch (error) {
console.error("Network/Execution Error:", error);
throw new Error("An error occurred while sending the verification code.");
}
}
// Public parameters are exposed to the client app (e.g., to show "Sent to +82 10-****-1234")
event.response.publicChallengeParameters = { phone: phoneNumber };
// Private parameters are securely passed to the Verify function later
event.response.privateChallengeParameters = { challenge: 'EASYAUTH_OTP' };
}
return event;
};
Step 3: Verify Auth Challenge Lambda
When the user types the 6-digit code into your frontend app and submits it, Cognito invokes this function. We will pass the code to EasyAuth for verification.
// verifyAuthChallenge.js
exports.handler = async (event) => {
if (event.request.privateChallengeParameters.challenge === 'EASYAUTH_OTP') {
const answer = event.request.challengeAnswer; // The code the user typed
const phoneNumber = event.request.userAttributes.phone_number;
try {
// Call EasyAuth API to verify the OTP
const response = await fetch('https://api.easyauth.co/v1/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EASYAUTH_API_KEY}`
},
body: JSON.stringify({
phone: phoneNumber,
code: answer
})
});
const data = await response.json();
// Inform Cognito whether the verification was successful
// EasyAuth returns { "success": true } if valid.
event.response.answerCorrect = data.success === true;
} catch (error) {
console.error("Verification Error:", error);
event.response.answerCorrect = false; // Deny on error
}
}
return event;
};
Frontend Integration (Next.js & AWS Amplify)
With your Cognito User Pool configured to use these three Lambda triggers, your frontend code becomes incredibly clean. If you are building with React or Next.js using AWS Amplify, the implementation looks like this:
import { Auth } from 'aws-amplify';
// 1. User enters their phone number to sign in/sign up
let cognitoUserReference;
async function handleSendOTP(phoneNumber) {
try {
// This triggers the Create Auth Challenge Lambda
cognitoUserReference = await Auth.signIn(phoneNumber);
console.log("OTP Sent! Please check your messages.");
// Transition your UI to the code input screen
} catch (error) {
console.error("Error initiating sign in:", error);
}
}
// 2. User enters the 6-digit code received via SMS
async function handleVerifyOTP(otpCode) {
try {
// This triggers the Verify Auth Challenge Lambda
await Auth.sendCustomChallengeAnswer(cognitoUserReference, otpCode);
console.log("Authentication successful! User is now logged in.");
// Redirect user to the dashboard
} catch (error) {
console.error("Verification failed (Invalid or expired code):", error);
}
}
Best Practices & Security Considerations for Production
Before deploying this to a production environment, keep the following tips in mind:
- Lambda Timeout Adjustment: By default, AWS sets Lambda timeouts to 3 seconds. Since our
CreateandVerifyfunctions are making external HTTP calls to the EasyAuth API, network latency can occasionally cause timeouts. Increase the timeout limit of these Lambdas to at least 10 seconds to ensure reliability. - Protecting PII in CloudWatch: Be very careful with
console.log(event)in your Lambda functions. Theeventobject contains the user's phone number. Logging this in plain text violates data protection standards (like GDPR or local privacy laws). Always mask phone numbers before logging. - API Key Management: Never hardcode your
EASYAUTH_API_KEY. Utilize AWS Secrets Manager or secure Lambda Environment Variables to inject the key at runtime. - Rate Limiting Mitigation: While EasyAuth scales wonderfully, you should implement WAF (Web Application Firewall) rules on your Cognito endpoints to prevent malicious bots from rapidly requesting SMS codes and racking up your billing.
Conclusion
Using AWS Cognito's default Amazon SNS for SMS authentication might be the path of least resistance initially, but for developers operating in strict telecom environments, it quickly becomes a bottleneck of failed deliveries, angry users, and high AWS bills.
Conversely, falling back to legacy domestic SMS vendors halts your development velocity entirely due to archaic paperwork requirements and slow manual approvals.
By leveraging the Cognito Custom Authentication Challenge and integrating EasyAuth, you achieve the holy grail of authentication for modern apps: an enterprise-grade AWS backend combined with a frictionless, zero-paperwork, and highly reliable local SMS provider.
Don't let legacy telecom regulations slow down your MVP launch. Sign up at EasyAuth today, claim your 10 free test credits, and implement this 5-minute serverless architecture. Focus your valuable engineering time on building your core product features, not wrestling with SMS delivery issues.
Start advertising on Bitbake
Contact Us