1
0
forked from baron/baron-sso

SMS 발송 및 Redis 기반 인증 코드 검증, JWT 발급 기능 구현

This commit is contained in:
2026-01-06 16:32:43 +09:00
parent 362b6b60d4
commit 659ccfbe53
7 changed files with 170 additions and 9 deletions

View File

@@ -75,6 +75,7 @@ func main() {
auth.Post("/enchanted-link/poll", authHandler.PollEnchantedLink)
auth.Post("/magic-link/verify", authHandler.VerifyMagicLink)
auth.Post("/sms", authHandler.SendSms)
auth.Post("/verify-sms", authHandler.VerifySms)
// Client Logging Route (For Debugging)
api.Post("/client-log", func(c *fiber.Ctx) error {

View File

@@ -11,8 +11,11 @@ require (
github.com/ClickHouse/ch-go v0.69.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect

View File

@@ -9,13 +9,19 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=

View File

@@ -33,3 +33,9 @@ type NaverSmsResponse struct {
type SmsRequest struct {
PhoneNumber string `json:"phoneNumber"`
}
// SmsVerifyRequest represents the request body for verifying an SMS code.
type SmsVerifyRequest struct {
PhoneNumber string `json:"phoneNumber"`
Code string `json:"code"`
}

View File

@@ -15,11 +15,13 @@ import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v4"
)
type AuthHandler struct {
ProjectID string
SmsService domain.SmsService
ProjectID string
SmsService domain.SmsService
RedisService *service.RedisService
}
func NewAuthHandler() *AuthHandler {
@@ -28,9 +30,15 @@ func NewAuthHandler() *AuthHandler {
// Fallback for dev if not set
pid = "P37DsGepBT6uDWb5TYYpb5RxUPuq"
}
redisService, err := service.NewRedisService()
if err != nil {
log.Fatalf("Failed to connect to Redis: %v", err)
}
return &AuthHandler{
ProjectID: pid,
SmsService: service.NewSmsService(),
ProjectID: pid,
SmsService: service.NewSmsService(),
RedisService: redisService,
}
}
@@ -41,24 +49,94 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
}
log.Printf("[SMS 발송 시작] 요청된 번호: %s", req.PhoneNumber)
// Sanitize phone number: remove dashes
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
log.Printf("[SMS 발송] 번호 정제 완료: %s", sanitizedPhone)
// Generate a 6-digit verification code
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
content := fmt.Sprintf("[Baron SSO] Your verification code is %s", code)
log.Printf("[SMS 발송] 인증 코드 생성 완료: %s", code)
// Store the code in Redis before sending
if err := h.RedisService.StoreVerificationCode(sanitizedPhone, code); err != nil {
log.Printf("[SMS 발송 실패] Redis에 코드 저장 실패: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to process request"})
}
log.Printf("[SMS 발송] Redis에 인증 코드 저장 성공 (키: sms_verify:%s)", sanitizedPhone)
if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil {
log.Printf("Error sending SMS: %v", err)
log.Printf("[SMS 발송 실패] SENS API 호출 실패: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
}
// TODO: Store the verification code for later verification
log.Printf("[SMS 발송 성공] SENS API를 통해 SMS 발송 완료")
return c.JSON(fiber.Map{"message": "SMS sent successfully"})
}
// VerifySms verifies the provided SMS code.
func (h *AuthHandler) VerifySms(c *fiber.Ctx) error {
var req domain.SmsVerifyRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
}
log.Printf("[SMS 검증 시작] 요청된 번호: %s, 코드: %s", req.PhoneNumber, req.Code)
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
log.Printf("[SMS 검증] 번호 정제 완료: %s", sanitizedPhone)
storedCode, err := h.RedisService.GetVerificationCode(sanitizedPhone)
if err != nil {
log.Printf("[SMS 검증 실패] Redis에서 코드 조회 실패: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal server error"})
}
log.Printf("[SMS 검증] Redis에서 코드 조회 완료. 저장된 코드: '%s'", storedCode)
if storedCode == "" || storedCode != req.Code {
log.Printf("[SMS 검증 실패] 코드가 일치하지 않거나 만료됨 (요청된 코드: %s, 저장된 코드: %s)", req.Code, storedCode)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"})
}
log.Printf("[SMS 검증] 코드 일치 확인")
// Code is correct, delete it to prevent reuse
if err := h.RedisService.DeleteVerificationCode(sanitizedPhone); err != nil {
// Log the error but don't fail the request as the code was already verified
log.Printf("[SMS 검증] 경고: Redis에서 코드 삭제 실패 (하지만 검증은 성공으로 처리됨): %v", err)
} else {
log.Printf("[SMS 검증] Redis에서 사용된 코드 삭제 완료")
}
// Generate JWT token
claims := jwt.MapClaims{
"sub": sanitizedPhone, // Subject (user identifier)
"exp": time.Now().Add(time.Hour * 24).Unix(), // Expiration time (24 hours)
"iat": time.Now().Unix(), // Issued at
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
log.Printf("[SMS 검증] JWT 클레임 생성 완료")
// Sign the token with the secret key
secretKey := os.Getenv("COOKIE_SECRET")
if secretKey == "" {
log.Println("Warning: COOKIE_SECRET is not set. Using a default, insecure key.")
secretKey = "default-insecure-secret-key-for-dev"
}
signedToken, err := token.SignedString([]byte(secretKey))
if err != nil {
log.Printf("[SMS 검증 실패] JWT 토큰 서명 실패: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate token"})
}
log.Printf("[SMS 검증 성공] JWT 토큰 발급 완료")
return c.JSON(fiber.Map{"token": signedToken})
}
// getBaseURL extracts the region code from Project ID if present (e.g., P37... -> api.37ds.descope.com)
// Default is api.descope.com
func (h *AuthHandler) getBaseURL() string {

View File

@@ -0,0 +1,62 @@
package service
import (
"context"
"os"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
type RedisService struct {
Client *redis.Client
}
// NewRedisService creates and returns a new RedisService
func NewRedisService() (*RedisService, error) {
redisAddr := os.Getenv("REDIS_ADDR")
if redisAddr == "" {
redisAddr = "localhost:6379" // Fallback for local dev without Docker
}
rdb := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
// Ping the server to check the connection
if _, err := rdb.Ping(ctx).Result(); err != nil {
return nil, err
}
return &RedisService{Client: rdb}, nil
}
// StoreVerificationCode saves the SMS verification code with a 3-minute expiration
func (s *RedisService) StoreVerificationCode(phone, code string) error {
// Key format: "sms_verify:01012345678"
key := "sms_verify:" + phone
expiration := 3 * time.Minute
err := s.Client.Set(ctx, key, code, expiration).Err()
return err
}
// GetVerificationCode retrieves the SMS verification code
func (s *RedisService) GetVerificationCode(phone string) (string, error) {
key := "sms_verify:" + phone
code, err := s.Client.Get(ctx, key).Result()
if err == redis.Nil {
// Key does not exist (expired or incorrect phone number)
return "", nil
} else if err != nil {
return "", err
}
return code, nil
}
// DeleteVerificationCode removes the verification code after successful verification
func (s *RedisService) DeleteVerificationCode(phone string) error {
key := "sms_verify:" + phone
return s.Client.Del(ctx, key).Err()
}

View File

@@ -12,11 +12,11 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"baron-sso-backend/internal/domain"
)
type SmsServiceImpl struct {
accessKey string
secretKey string
@@ -25,11 +25,16 @@ type SmsServiceImpl struct {
}
func NewSmsService() domain.SmsService {
// Sanitize sender phone number right after reading from env
rawSenderPhone := os.Getenv("NAVER_SENDER_PHONE_NUMBER")
sanitizedSenderPhone := strings.ReplaceAll(rawSenderPhone, "-", "")
log.Printf("[서비스 초기화] 발신자 번호 처리: 원본='%s', 정제 후='%s'", rawSenderPhone, sanitizedSenderPhone)
return &SmsServiceImpl{
accessKey: os.Getenv("NAVER_CLOUD_ACCESS_KEY"),
secretKey: os.Getenv("NAVER_CLOUD_SECRET_KEY"),
serviceID: os.Getenv("NAVER_CLOUD_SERVICE_ID"),
senderPhone: os.Getenv("NAVER_SENDER_PHONE_NUMBER"),
senderPhone: sanitizedSenderPhone,
}
}