Twilio를 이용한 SMS 인증 구현-토큰 검증

Odyssey·2025년 6월 27일
0

Next.js_study

목록 보기
47/58
post-thumbnail

2025.6.27 금요일의 공부기록

이번 글에서는 Next.js 애플리케이션에서 SMS 인증 토큰 검증 기능을 구현하는 방법에 대해 다룬다. 인증번호의 존재 여부 확인, 사용자 로그인 처리, 그리고 세션 관리까지이다.


인증 토큰 검증 로직 구현

사용자가 받은 SMS 인증 토큰을 입력하면 다음의 과정을 통해 유효성을 검증하고 사용자를 로그인 처리한다.

과정 요약:

  1. 입력된 토큰의 유효성 검증 (존재 여부 및 형식)
  2. 유효한 토큰이 있을 경우 사용자 세션 생성 및 로그인 처리
  3. 사용한 인증 토큰을 DB에서 삭제하여 보안성 유지

token verification

코드 (app/sms/actions.ts)

"use server";

import { z } from "zod";
import validator from "validator";
import db from "@/lib/db";
import crypto from "crypto";
import { getSession } from "@/lib/session";
import { redirect } from "next/navigation";

const phoneSchema = z
  .string()
  .trim()
  .refine(
    (phone) => validator.isMobilePhone(phone, "ko-KR"),
    "Wrong phone number format"
  );

async function tokenExists(token: number) {
  const exists = await db.sMSToken.findUnique({
    where: {
      token: token.toString(),
    },
    select: {
      id: true,
    },
  });
  return Boolean(exists);
}

const tokenSchema = z.coerce
  .number()
  .min(100000)
  .max(999999)
  .refine(tokenExists, "This token does not exist");

interface ActionState {
  token: boolean;
}

async function getToken() {
  const token = crypto.randomInt(100000, 999999).toString();
  const existingToken = await db.sMSToken.findUnique({
    where: {
      token,
    },
    select: {
      id: true,
    },
  });
  if (existingToken) {
    return getToken();
  }
  return token;
}

export async function smsLogIn(prevState: ActionState, formData: FormData) {
  const phone = formData.get("phone");
  const token = formData.get("token");

  if (!prevState.token) {
    const result = phoneSchema.safeParse(phone);
    if (!result.success) {
      return { token: false, error: result.error.flatten() };
    } else {
      // delete previous token
      await db.sMSToken.deleteMany({
        where: {
          user: {
            phone: result.data,
          },
        },
      });
      // create new token

      const token = await getToken();
      await db.sMSToken.create({
        data: {
          token,
          user: {
            connectOrCreate: {
              where: {
                phone: result.data,
              },
              create: {
                phone: result.data,
                username: crypto.randomBytes(10).toString("hex"),
              },
            },
          },
        },
      });
      // send token to phone using twilio
      return { token: true };
    }
  } else {
    const result = await tokenSchema.safeParseAsync(token);
    if (!result.success) {
      return {
        token: true,
        error: result.error.flatten(),
      };
    } else {
      const token = await db.sMSToken.findUnique({
        where: {
          token: result.data.toString(),
        },
        select: {
          id: true,
          user: true,
        },
      });
      const session = await getSession();
      session.userId = token!.user.id;
      await session.save();
      await db.sMSToken.delete({
        where: {
          id: token!.id,
        },
      });
      redirect("/profile");
    }
  }
}

흐름

토큰 존재 여부 확인 (tokenExists 함수)

  • 입력받은 토큰이 실제 DB에 존재하는지 여부를 비동기적으로 확인한다.
  • 존재하지 않으면 유효성 검사 오류가 발생하여 사용자에게 알림을 제공한다.
async function tokenExists(token: number) {
  const exists = await db.sMSToken.findUnique({
    where: {
      token: token.toString(),
    },
    select: {
      id: true,
    },
  });
  return Boolean(exists);
}

토큰 검증 스키마 (tokenSchema)

  • Zod를 이용해 토큰의 형식과 범위를 검증한다.
  • 토큰의 최소값(100000)과 최대값(999999)을 설정하여 6자리 숫자인지 확인한다.
  • 위에서 작성한 tokenExists 함수로 토큰이 실제 존재하는지 검증한다.
const tokenSchema = z.coerce
  .number()
  .min(100000)
  .max(999999)
  .refine(tokenExists, "This token does not exist");

최종 인증 처리 및 로그인 로직

  • 토큰 검증 성공 시 사용자 세션을 생성하고 로그인 처리를 한다.
  • 사용한 토큰은 즉시 삭제하여 보안을 유지한다.
  • 마지막으로 사용자를 프로필 페이지로 리다이렉트 한다.
const result = await tokenSchema.safeParseAsync(token);
    if (!result.success) {
      return {
        token: true,
        error: result.error.flatten(),
      };
    } else {
      const token = await db.sMSToken.findUnique({
        where: {
          token: result.data.toString(),
        },
        select: {
          id: true,
          user: true,
        },
      });
      const session = await getSession();
      session.userId = token!.user.id;
      await session.save();
      await db.sMSToken.delete({
        where: {
          id: token!.id,
        },
      });
      redirect("/profile");
    }

인증 플로우 요약

단계작업목적
1전화번호 제출 시 SMS 토큰 생성 및 발송사용자 인증을 위한 토큰 제공
2사용자 토큰 입력인증 요청
3토큰 형식 및 DB 존재 여부 확인유효한 인증 번호인지 검사
4사용자 세션 생성 및 로그인 처리인증 성공 후 사용자 접근 권한 부여
5인증 완료 후 토큰 삭제재사용 방지 및 보안 강화

0개의 댓글