비트베이크

Implementing Mobile SMS Auth for Sign-ups in Next.js in 5 Minutes (Zero Paperwork)

2026-05-07T01:02:34.616Z

Professional, modern tech imagery with focus on code, data, or secure interactions, suitable for text overlay on developer/authentication content. Example image (not fetched by AI) of hands typing on a glowing keyboard representing secure coding or digital authentication. Search query: developer authentication secure

The Hidden Challenge of Building SMS Authentication

If you've ever tried building a Minimum Viable Product (MVP) or scaling a side project, you know that user authentication is a non-negotiable feature. To prevent spam, block fake accounts, and maintain a secure ecosystem, verifying a user's mobile phone number through an SMS One-Time Password (OTP) remains the gold standard.

However, developers quickly realize that writing the code is the easy part. The real nightmare begins when you try to get access to an SMS API. Traditional SMS providers often act as institutional gatekeepers, demanding an exhausting list of requirements before letting you send a single message:

  • Official Business Registration Certificates
  • Telecommunication Service Provider Licenses
  • Mandatory pre-registration of Sender IDs (often requiring weeks of verification)
  • Minimum monthly volume commitments

If you're an indie hacker, a weekend hackathon participant, or a startup founder in the pre-incorporation phase, this paperwork is more than an inconvenience—it's a massive roadblock that can completely derail your launch timeline.

In this comprehensive guide, we will bypass this red tape completely. We'll walk step-by-step through implementing a production-ready, highly secure SMS OTP authentication flow in Next.js (App Router) using React Server Actions. The best part? You'll go from zero to a fully functioning verification system in under 5 minutes, with absolutely zero paperwork.


Solution Overview: What We Are Building

To achieve a seamless and secure architecture, we will leverage the modern capabilities of Next.js 14/15.

Here is a breakdown of what we will cover:

  1. Environment Configuration: Securely storing our API credentials.
  2. Server Actions Architecture: Building backend functions to communicate safely with the SMS provider without exposing credentials to the client.
  3. Stateful UI Implementation: Crafting a React component with robust state management (handling phone number input, countdown timers, loading states, and error messaging).
  4. Best Practices & Security: Understanding rate limiting, input sanitization, and timing attack mitigation.

We will utilize EasyAuth, a developer-first SMS API designed specifically to eliminate the paperwork hurdle while providing a clean, RESTful architecture.


Step 1: Setting up the Environment

First, you need an API key. We are using EasyAuth because it requires no business registration and grants instant API access upon signup.

Navigate to the root directory of your Next.js project and open (or create) your .env.local file. Add your secure API key here:

# .env.local
EASYAUTH_API_KEY=your_production_api_key_here

> Security Note: Notice that we do not use the NEXT_PUBLIC_ prefix. This ensures the environment variable remains strictly on the server-side, preventing malicious actors from scraping your API key from the client's browser bundle.


Step 2: Constructing Next.js Server Actions

In older versions of React and Next.js, you had to spin up dedicated API routes (e.g., /api/send-sms) to handle secrets. With the Next.js App Router, we can use Server Actions. This allows us to write server-side logic directly inside our application that can be invoked seamlessly from client components.

Create a new file at app/actions/auth.ts.

'use server';

// app/actions/auth.ts

interface ActionResponse {
  success: boolean;
  message?: string;
}

/**
 * Action 1: Requests an OTP to be sent to the user's phone.
 * @param phoneNumber The user's unformatted or formatted phone number.
 */
export async function sendVerificationSMS(phoneNumber: string): Promise {
  try {
    // Sanitization: Strip all non-numeric characters (e.g., dashes, spaces, parentheses)
    const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');
    
    // Basic validation to prevent unnecessary API calls
    if (cleanPhone.length < 10 || cleanPhone.length > 15) {
      return { success: false, message: 'Please enter a valid phone number.' };
    }

    // Communicate with EasyAuth's /send endpoint
    const response = await fetch('https://api.easyauth.kr/send', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.EASYAUTH_API_KEY}`
      },
      body: JSON.stringify({ to: cleanPhone })
    });

    if (!response.ok) {
      // In production, parse response.json() for specific error logs
      throw new Error('Failed to dispatch SMS through provider.');
    }

    return { success: true };
  } catch (error) {
    console.error('[SMS_SEND_ERROR]:', error);
    return { success: false, message: 'Internal server error. Please try again later.' };
  }
}

/**
 * Action 2: Verifies the OTP code input by the user.
 * @param phoneNumber The phone number originally used.
 * @param code The 6-digit OTP code provided by the user.
 */
export async function verifySMSCode(phoneNumber: string, code: string): Promise {
  try {
    const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');

    // Communicate with EasyAuth's /verify endpoint
    const response = await fetch('https://api.easyauth.kr/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.EASYAUTH_API_KEY}`
      },
      body: JSON.stringify({ to: cleanPhone, code })
    });

    if (!response.ok) {
      return { success: false, message: 'Invalid or expired verification code.' };
    }

    return { success: true };
  } catch (error) {
    console.error('[SMS_VERIFY_ERROR]:', error);
    return { success: false, message: 'An error occurred during verification.' };
  }
}

Why this architecture is powerful:

  1. Encapsulation: The EasyAuth API integration details (endpoints, keys) live entirely on your server.
  2. Type Safety: By sharing the ActionResponse interface, your client code intrinsically knows what to expect when calling these functions.
  3. Sanitization at the Edge: We process the regex replace(/[^0-9]/g, '') on the server so that even if a user bypasses client validation, the API call remains clean.

Step 3: Building the Interactive Client UI

A great user experience is crucial during authentication. Users need clear feedback when a message is sent, a countdown timer to create urgency (and define the OTP validity window), and distinct error messaging.

Create the UI component at app/components/SmsAuth.tsx.

'use client';

import { useState, useEffect } from 'react';
import { sendVerificationSMS, verifySMSCode } from '../actions/auth';

export default function SmsAuth() {
  // Form states
  const [phone, setPhone] = useState('');
  const [code, setCode] = useState('');
  
  // Application flow states
  const [step, setStep] = useState<'IDLE' | 'SENT' | 'VERIFIED'>('IDLE');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  
  // 180 seconds = 3 minutes standard OTP window
  const [timeLeft, setTimeLeft] = useState(180); 

  // Countdown Timer Effect
  useEffect(() => {
    if (step === 'SENT' && timeLeft > 0) {
      const timerId = setInterval(() => {
        setTimeLeft((prev) => prev - 1);
      }, 1000);
      return () => clearInterval(timerId); // Cleanup on unmount
    }
  }, [step, timeLeft]);

  // Helper to format seconds into MM:SS
  const formatTime = (seconds: number) => {
    const m = Math.floor(seconds / 60);
    const s = seconds % 60;
    return `${m}:${s.toString().padStart(2, '0')}`;
  };

  // Handle SMS Request
  const handleSend = async () => {
    setLoading(true);
    setError('');
    
    const res = await sendVerificationSMS(phone);
    
    if (res.success) {
      setStep('SENT');
      setTimeLeft(180); // Reset timer on successful send/resend
    } else {
      setError(res.message || 'Failed to send SMS.');
    }
    setLoading(false);
  };

  // Handle OTP Verification
  const handleVerify = async () => {
    if (timeLeft === 0) {
      setError('The verification code has expired. Please request a new one.');
      return;
    }

    setLoading(true);
    setError('');

    const res = await verifySMSCode(phone, code);
    
    if (res.success) {
      setStep('VERIFIED');
      // Proceed to sign-up completion or user routing here
    } else {
      setError(res.message || 'Verification failed.');
    }
    setLoading(false);
  };

  return (
    <div>
      <h2>Mobile Verification</h2>

      {/* Persistent Error Display */}
      {error &amp;&amp; (
        <div>
          {error}
        </div>
      )}

      {step === 'VERIFIED' ? (
        <div>
          
            
          
          Authentication Successful!
        </div>
      ) : (
        <div>
          {/* Phone Number Input Section */}
          <div>
            Phone Number
            <div>
               setPhone(e.target.value)}
                placeholder="e.g. 01012345678"
                disabled={step === 'SENT' &amp;&amp; timeLeft &gt; 0}
                className="flex-1 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
              /&gt;
              
                {step === 'SENT' ? 'Resend' : 'Send OTP'}
              
            </div>
          </div>

          {/* OTP Code Input Section */}
          {step === 'SENT' &amp;&amp; (
            <div>
              
                <span>Authentication Code</span>
                <span>
                  {formatTime(timeLeft)}
                </span>
              
              <div>
                 setCode(e.target.value.replace(/[^0-9]/g, ''))}
                  placeholder="6-digit code"
                  maxLength={6}
                  className="flex-1 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-center tracking-widest text-lg"
                /&gt;
                
                  Verify
                
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Advanced Tips & Best Practices for Production

While the code above works perfectly, running an authentication service in a live environment requires anticipating malicious behavior and edge cases. Here are essential architectural considerations:

1. Protect Your Wallet with Rate Limiting

One of the most common attacks on modern web applications is SMS Toll Fraud (or SMS Pumping). Attackers use automated bots to repeatedly request SMS messages to premium-rate numbers, draining your funds. Because you are exposing a public-facing /send action, you must implement rate limiting before going to production. In a Next.js environment, integrating @upstash/ratelimit paired with Redis is the industry standard. Limit users to a strict threshold, such as 3 to 5 SMS requests per IP address per 24-hour window.

2. Double-Layer Input Sanitization

Never trust client-side validation alone. Users will input phone numbers in wildly unpredictable formats—adding country codes, hyphens, spaces, and brackets. In the provided code, we apply .replace(/[^0-9]/g, '') on both the React client state and inside the Server Action. This robust double-layer sanitization ensures that backend logic always receives clean, predictable numerical strings, severely reducing database formatting errors or API rejections.

3. Enforcing the 3-Minute Validity Window

Providing a visual countdown timer on the frontend is excellent for User Experience (UX), but it provides zero real security. A malicious user could easily bypass the frontend logic using tools like Postman or cURL. Security relies entirely on the backend. When using a reliable API service, the provider enforces a strict Time-to-Live (TTL) on the OTP. Any verification requests hitting the API after the 3-minute window will automatically be rejected by the provider, ensuring mathematical security regardless of frontend tampering.


Conclusion: The Easiest Choice for Developers

We've successfully built a complete, highly-responsive SMS authentication flow using Next.js Server Actions. The architecture is modular, secure, and provides an excellent user experience.

However, the real magic behind this 5-minute implementation isn't just the Next.js framework—it's having the right infrastructure partner. If you are tired of bureaucratic red tape and slow onboarding, EasyAuth (이지어스) is the definitive solution tailored for modern developers.

Why developers are switching to EasyAuth:

  • Absolute Zero Paperwork: Say goodbye to submitting Business Licenses and Telecommunication Certificates. Start instantly.
  • No Sender ID Registration Needed: EasyAuth utilizes a pre-verified automated routing system, meaning you don't have to wait weeks for telecommunication companies to approve your sending number.
  • Incredible Cost Efficiency: Traditional legacy APIs charge roughly 30-50 KRW per message. EasyAuth slashes this down to an ultra-competitive 15-25 KRW ($0.01-$0.02) per message.
  • Generous Free Tier: Every new account is instantly credited with 10 free messages, allowing you to test out the API and perfect your MVP's user flow with zero upfront financial commitment.

With just two intuitive endpoints (POST /send and POST /verify), implementing mobile security has never been more straightforward. Stop writing legal paperwork, and get back to writing code. Try integrating EasyAuth into your Next.js application today!

비트베이크에서 광고를 시작해보세요

광고 문의하기

다른 글 보기

2026-08-06T06:01:33.120Z

2026 GTX 개통 임박! A/B/C 노선 수혜지역 투자 가이드

2026년 GTX A/B/C 노선 개통이 임박하며 수도권 부동산 시장이 들썩이고 있습니다. GTX 노선별 개통 현황과 함께, 주요 수혜지역을 심층 분석하고 실거주 및 투자를 위한 현명한 전략과 유의점을 제시하여 성공적인 아파트 투자를 돕는 가이드입니다.

2026-08-05T06:01:33.825Z

2026 하반기 재건축 투자: 규제 완화 속 핵심 전략

2026년 하반기, 규제 완화 기대감 속 재건축 투자의 핵심 전략을 알아봅니다. 정부 정책 변화 분석, 유망 지역 선정 기준, 주의할 점, 그리고 성공적인 투자를 위한 전문가들의 조언까지, 2026 부동산 시장에서 기회를 잡을 방법을 제시합니다.

2026-08-04T06:01:37.246Z

2026 하반기 청약, 대출 금리 변화 활용 내집마련 필승 전략

2026년 하반기 청약 시장은 변화하는 대출 금리와 정책, 지역별 수급 상황에 따라 기회와 도전이 공존합니다. 이 글에서는 부동산 시장 동향과 주택담보대출 전략, 인기 청약 단지 분석, 청약 가점 및 특별공급 활용 팁 등 내 집 마련을 위한 필승 전략을 제시합니다. 철저한 준비와 현명한 판단으로 2026년 내 집 마련의 꿈을 이루세요.

2026-08-04T01:01:36.795Z

2026년 청약 성공 전략: 무주택자 내집마련 필승 가이드

2026년 무주택자의 내집마련 꿈을 위한 필승 청약 전략 가이드입니다. 청약 가점부터 특별공급 활용법, 현명한 대출 전략, 유망 단지 분석, 그리고 제도 변화까지 2026년 청약 성공을 위한 모든 정보를 담았습니다.

서비스

피드자주 묻는 질문고객센터

문의

비트베이크

레임스튜디오 | 사업자 등록번호 : 542-40-01042

경기도 남양주시 와부읍 수례로 116번길 16, 4층 402-제이270호

트위터인스타그램네이버 블로그