Skip to content

feat: 네이버 소셜 로그인 구현 (OAuth 2.0) - #34

Closed
kokkumong wants to merge 4 commits into
devfrom
feature/30-naver-login
Closed

kokkumong wants to merge 4 commits into
devfrom
feature/30-naver-login

Conversation

@kokkumong

Copy link
Copy Markdown
Collaborator

🚀 작업 내용 (What)

  • 네이버 OAuth 2.0 로그인 플로우 구현
  • GET /api/auth/naver/login — state 생성 후 네이버 인가 서버로 리다이렉트
  • GET /api/auth/naver/callback — state CSRF 검증, 토큰/유저 조회, DB upsert, JWT 발급
  • SocialAccount 모델 추가 (provider, providerUserId 기반 복합 unique)
  • User 모델 필드 optional 처리 (소셜 로그인은 비밀번호 불필요)
  • jose 기반 JWT 서명/검증 유틸 구현
  • 단위 테스트 24개 작성 (jwt, naver 클라이언트)

📣 핵심 변경 이유 (Why)

  • 네이버 아이디로 로그인 기능 제공으로 가입 허들 낮추기
  • SocialAccount 테이블 분리로 추후 카카오 등 소셜 로그인 확장 가능 구조

📸 스크린샷 (Visuals, 선택)

  • 해당 없음 (백엔드 API)

⚠️ 체크리스트 (Checklist)

  • 브랜치 컨벤션(feature/00-name)을 지켰나요?
  • 커밋 컨벤션(feat:, fix: 등)을 지켰나요?
  • 작업 전에 관련 이슈를 생성하고 연결했나요?
  • 내 코드가 팀의 기존 코드를 망가뜨리지 않았는지 확인했나요?

🔗 관련 이슈 (Issue)

Close #33

kokkumong and others added 3 commits May 28, 2026 14:10
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kokkumong, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 21 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ce99d5a-f7c1-419b-9e2f-4395801ede05

📥 Commits

Reviewing files that changed from the base of the PR and between 1776987 and d28f159.

📒 Files selected for processing (6)
  • src/app/api/auth/naver/callback/route.ts
  • src/app/api/auth/naver/login/route.ts
  • src/lib/auth/__tests__/jwt.test.ts
  • src/lib/auth/__tests__/naver.test.ts
  • src/lib/auth/jwt.ts
  • src/lib/auth/naver.ts

Walkthrough

User 스키마에 profileCompleted 필드와 SocialAccount 관계를 추가하고, 소셜 로그인용 필드를 nullable로 처리했다. JWT 서명/검증 유틸(jose 기반 HS256)을 구현하고, 네이버 OAuth 플로우 함수들(인가 URL, 토큰 교환, 사용자 정보 조회)을 작성했다. login과 callback API 라우트로 전체 OAuth 플로우를 구현하며, state 기반 CSRF 검증, 신규/기존 사용자 upsert, JWT 발급을 수행한다.

Changes

네이버 OAuth 소셜 로그인

Layer / File(s) Summary
데이터 스키마 및 마이그레이션
.env.example, prisma/schema.prisma, prisma/migrations/...
User 모델의 passwordHash, isAgeOver14, termsAgreedAt, privacyAgreedAt를 nullable 처리. profileCompleted 필드(기본값 true) 추가. SocialAccount 모델 신규 정의(userId 외래키, provider+providerUserId 복합 unique). SQLite 호환 마이그레이션 스크립트로 테이블 재구성.
JWT 인증 유틸
src/lib/auth/jwt.ts, src/lib/auth/__tests__/jwt.test.ts
JwtPayload 인터페이스(userId, nickname 필수, email/provider 선택), COOKIE_NAME/COOKIE_MAX_AGE 상수. getSecret() → JWT_SECRET을 Uint8Array로 변환, signAccessToken() → jose SignJWT로 HS256 서명, verifyAccessToken() → jwtVerify로 검증 후 payload 반환 또는 null. 라운드트립, 잘못된 토큰, 누락된 secret 케이스 테스트.
네이버 OAuth 유틸 함수
src/lib/auth/naver.ts, src/lib/auth/__tests__/naver.test.ts
getNaverAuthUrl(state) → 환경변수 검증 후 URLSearchParams로 인가 URL 생성. getNaverToken(code, state) → POST /token으로 access_token 발급, HTTP/error 필드 실패 시 예외 던짐. getNaverUser(naverAccessToken) → Bearer 토큰으로 사용자 정보 조회, resultcode="00" 검증. extractNaverUserInfo() → nickname ?? name ?? naver_{id} 규칙으로 닉네임 결정, email 추출. 환경변수 누락, fetch 실패, 응답 에러 케이스 테스트.
네이버 로그인/콜백 API 라우트
src/app/api/auth/naver/login/route.ts, src/app/api/auth/naver/callback/route.ts
login: dynamic="force-dynamic", state 생성, Naver 인가 URL로 리다이렉트, state를 httpOnly/secure/sameSite=lax 쿠키(10분)로 저장, 오류 시 /login?error=naver_login_failed로 리다이렉트. callback: code/state/error 파라미터 검증, naver_oauth_state 쿠키로 CSRF 방어, getNaverToken()과 getNaverUser()로 사용자 정보 조회, naverId 유효성 확인. naverId로 SocialAccount 검색 → 있으면 기존 유저 사용, 없으면 신규 유저(nickname 중복 시 naver_{naverId}, profileCompleted=false) + SocialAccount 생성. signAccessToken(userId, email, nickname, provider="naver")으로 JWT 발급, profileCompleted 여부에 따라 성공/추가정보 입력으로 리다이렉트, JWT와 state 쿠키(삭제) 처리, 예외 시 실패 리다이렉트.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Siul49/moim#29: User.profileCompleted 필드, SocialAccount 모델 및 Prisma 마이그레이션, JWT 유틸 공유.

Suggested labels

feature


필수 리뷰 체크포인트

1. JWT_SECRET 환경변수 강제

Why: 서명 없는 토큰 발급으로 인증 우회.
How: getSecret()에서 없으면 즉시 예외 던짐.
코드:

function getSecret(): Uint8Array {
  const secret = process.env.JWT_SECRET;
  if (!secret) throw new Error("JWT_SECRET not set");
  return new TextEncoder().encode(secret);
}

✅ 현재 구현 통과, 테스트 (range_edaf87c73507) 확인됨.


2. CSRF state 검증 필수성

Why: 공격자가 임의의 redirect_uri로 인가 코드 탈취.
How: callback에서 쿼리 state와 쿠키 state 일치 확인 필수.
코드 (range_b93973ad115e):

const stateCookie = req.cookies.get(STATE_COOKIE)?.value;
if (!stateCookie || stateCookie !== state) {
  return NextResponse.redirect(...failUrl);
}

✅ 구현됨, 쿠키 삭제(range_faea74365bea) 포함.


3. 신규 사용자 생성 시 닉네임 충돌 처리

Why: 기존 로컬 회원과 닉네임 충돌 → 에러 또는 덮어쓰기.
How: 충돌 시 naver_${naverId}로 폴백.
코드 (range_bbe6bddd8fb6):

const existingUser = await prisma.user.findUnique({
  where: { nickname: extractedUser.nickname }
});
if (existingUser) {
  newUser.nickname = `naver_${naverId}`;
}

⚠️ 문제: 폴백 닉네임도 충돌할 수 있음. naver_${naverId}유일성 보장 불가능 (같은 네이버 ID로 재시도 시 동일). DB unique 제약 위반 가능성. 권장: UUID 또는 카운터 추가 또는 unique 제약 없이 덮어쓰기 명시.


4. profileCompleted 상태 관리

Why: 소셜 로그인 사용자는 추가 정보(나이, 약관) 미입력 → 기능 제한.
How: 신규 유저는 profileCompleted=false, callback에서 상태 확인 후 리다이렉트.
코드 (range_bbe6bddd8fb6, range_faea74365bea):

// 신규 유저
user = await prisma.user.create({
  data: { profileCompleted: false, ... }
});
// callback에서
const redirectUrl = user.profileCompleted 
  ? successUrl 
  : additionalInfoUrl;

✅ 구현됨, 로직 명확.


5. JWT payload provider 필드 선택 처리

Why: provider 필드는 선택(provider?) 정의되지만 callback에서 provider: "naver" 항상 포함.
How: JwtPayload 인터페이스 확인 필요.
코드 (range_a9a0b9e83c3b):

export interface JwtPayload {
  userId: string;
  email?: string;
  nickname: string;
  provider?: string;
}

✅ 선택 필드 → callback에서 "naver" 지정 가능. 로컬 로그인 (provider 생략 또는 "local") 구분 가능.


6. Naver access_token 비노출

Why: access_token을 JWT에 포함하면 클라이언트에 노출 → 네이버 API 직접 호출 위험.
How: JWT payload에 userId, nickname만 포함, access_token은 서버 저장 또는 폐기.
코드 (range_faea74365bea):

const token = await signAccessToken({
  userId: user.id,
  email: user.email,
  nickname: user.nickname,
  provider: "naver"
});
// naverAccessToken은 포함 안 함

✅ 구현됨, 보안 통과.


7. 환경변수 누락 시 graceful error

Why: NAVER_CLIENT_ID, NAVER_REDIRECT_URI 누락 시 500 에러.
How: login/callback에서 try-catch, 콘솔 로그, /login?error=...로 리다이렉트.
코드 (range_39bb71753742):

try {
  const authUrl = getNaverAuthUrl(state);
  // ...
} catch (error) {
  console.error("Naver login error:", error);
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
  return NextResponse.redirect(`${baseUrl}/login?error=naver_login_failed`);
}

✅ 구현됨, 오류 처리 적절.


8. 테스트 커버리지 검증

범위별 검사:

유틸 테스트 파일 체크포인트
getNaverAuthUrl range_58e30d2e87fe ✅ 환경변수 검증, URL 구성 요소 검증
getNaverToken range_91a93f0196ac ✅ fetch 성공/실패, error 필드 처리
getNaverUser range_d129e2261b43 ✅ fetch 성공/실패, resultcode 검증
extractNaverUserInfo range_4d2580586aae ✅ nickname/name/id fallback, email undefined
signAccessToken range_434244285713 ✅ 라운드트립 (기본, 이메일 포함, 로컬)
verifyAccessToken range_434244285713 ✅ 잘못된 토큰, 빈 문자열, 다른 secret

⚠️ 누락: callback 라우트 통합 테스트 (신규/기존 사용자 upsert, state CSRF, JWT 쿠키 설정). 현재는 유틸 단위 테스트만 있음. 권장: e2e 또는 통합 테스트 추가 (별도 PR 또는 이후 작업).


9. HTTP 응답 상태 코드 검증

코드 (range_7bc8cab61933):

const response = await fetch(naverTokenEndpoint, { method: "POST", ... });
if (!response.ok) {
  throw new Error("Failed to get Naver token");
}
const data = await response.json();
if (data.error) {
  throw new Error(`Naver error: ${data.error}`);
}

✅ response.ok 및 JSON error 필드 검증, 명확한 에러 메시지.


10. State 쿠키 만료 시간

code: range_fe3068c4c3e4 & range_39bb71753742

maxAge: 10 * 60 // 10분

✅ OAuth 2.0 권장 범위(5~10분), 적절함.


최종 지적

항목 심각도 조치
폴백 닉네임 충돌 가능성 🔴 High naver_${naverId} 부족, UUID 또는 시퀀스 추가 필요
callback 통합 테스트 부재 🟡 Medium 유틸 테스트만 있음, 라우트 e2e 추가 권장
일부 환경변수 검증 누락 🟢 Low login/callback에서 getNaverAuthUrl/getNaverToken이 검증하므로 괜찮음

Approve 조건: 닉네임 충돌 처리 재설계 + callback 통합 테스트 추가.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 'feat:' 접두사로 시작하며, 네이버 소셜 로그인 구현이라는 PR의 핵심 변경사항을 명확히 반영하고 있습니다.
Description check ✅ Passed PR 설명이 OAuth 2.0 플로우, 모델 변경, 테스트 추가 등 실제 변경사항들과 일치하며, 작업 이유와 구현 범위를 구체적으로 기술하고 있습니다.
Linked Issues check ✅ Passed 모든 구현 요구사항이 충족됨: GET /api/auth/naver/login·callback 엔드포인트 구현, SocialAccount 모델 추가, User 필드 optional 처리, JWT 유틸 및 단위 테스트 완료.
Out of Scope Changes check ✅ Passed 모든 변경사항이 네이버 소셜 로그인 기능과 직접 연관되어 있으며, 범위 외 작업은 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/30-naver-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/auth/naver/callback/route.ts`:
- Around line 96-123: The current nickname uniqueness check using
prisma.user.findUnique then prisma.user.create can race and raise a Prisma
unique constraint error (P2002); update the user creation flow around
prisma.user.create to catch Prisma.PrismaClientKnownRequestError, detect
error.code === "P2002" related to the nickname, and then retry create with a
fallback nickname (e.g., `naver_${naverId}` or
`naver_${naverId}_${randomSuffix}`) before propagating the error; reference the
variables/fns finalNickname, prisma.user.findUnique, prisma.user.create and
ensure the P2002 branch produces a successful user or a deterministic fallback
redirect rather than letting the generic catch treat it as a fatal failure.

In `@src/app/api/auth/naver/login/route.ts`:
- Line 9: The GET handler currently builds error redirect URLs using
NEXT_PUBLIC_BASE_URL (in the GET function and the code around the other redirect
on lines 29-30); change it to derive the origin from the incoming request by
constructing new URL(request.url).origin and use that origin to build
same-origin redirect targets (e.g., new URL('/auth/naver/error?reason=...', new
URL(request.url).origin)). Replace any usage of NEXT_PUBLIC_BASE_URL in this
file with this request-based origin so error redirects are forced to the request
origin.

In `@src/lib/auth/__tests__/jwt.test.ts`:
- Around line 16-72: Add a boundary test for the JWT exp field by issuing a
token with signAccessToken, using jest fake timers (jest.useFakeTimers /
jest.setSystemTime or advanceTimersByTime) to simulate verification before and
just after the expiry, and assert verifyAccessToken returns the payload before
expiry and null after expiry; locate this new test alongside existing tests in
jwt.test.ts and ensure timers are restored (jest.useRealTimers) after the test.

In `@src/lib/auth/__tests__/naver.test.ts`:
- Around line 121-174: Add a new test in the getNaverUser suite that mocks
global.fetch to return ok: true and resultcode: "00" but with response.id
missing or empty, then call getNaverUser("access_token") and assert it rejects
with the same error you expect for missing id (e.g., "네이버 사용자 정보 조회 오류" or the
specific message thrown by getNaverUser). Use vi.spyOn(global,
"fetch").mockResolvedValueOnce to supply the mocked Response.json payload and
mirror the pattern used in the other tests so the edge-case of a success code
but missing response.id is covered.

In `@src/lib/auth/jwt.ts`:
- Around line 31-34: The current jwtVerify usage casts payload directly to
JwtPayload, which bypasses runtime checks and can allow tokens missing required
fields; update the verification in the function that calls jwtVerify(token,
getSecret()) to perform a runtime guard on the returned payload (check that
payload.userId is a string/number as expected and payload.nickname is a
non-empty string) and only return payload as JwtPayload when those checks pass,
otherwise return null; also ensure the catch block of the same try/catch returns
null (not throwing) so invalid tokens are closed out.

In `@src/lib/auth/naver.ts`:
- Around line 68-74: The fetch POST to "https://nid.naver.com/oauth2.0/token"
has no timeout or robust error handling; wrap this external call (and the
similar fetch at lines 94-99) with a shared timeout wrapper (e.g., a utility
like fetchWithTimeout) that aborts via AbortController after a configured short
timeout, and surface a clear error when aborted so callers can handle it; update
the token-exchange function(s) to use that wrapper and catch/translate
network/timeout errors into meaningful exceptions/logs instead of hanging
indefinitely.
- Around line 107-116: After verifying data.resultcode === "00", add a defensive
check that data.response and data.response.id exist and are non-empty before
returning; if missing, throw a clear exception (e.g., "네이버 사용자 정보 조회 오류: id 누락")
so callers of this function (the code that uses data.response.id for account
linking/upsert) never receive a user object without an identifier. Update the
logic around resultcode/data.response.id (referencing data.resultcode and
data.response.id in this file) to validate presence and throw immediately when
id is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2b962ed-87e3-4f5f-a16d-9d68c5e3e70f

📥 Commits

Reviewing files that changed from the base of the PR and between e40c252 and 1776987.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json, !package-lock.json
  • package.json is excluded by !**/*.json
📒 Files selected for processing (9)
  • .env.example
  • prisma/migrations/20260525000000_add_social_account/migration.sql
  • prisma/schema.prisma
  • src/app/api/auth/naver/callback/route.ts
  • src/app/api/auth/naver/login/route.ts
  • src/lib/auth/__tests__/jwt.test.ts
  • src/lib/auth/__tests__/naver.test.ts
  • src/lib/auth/jwt.ts
  • src/lib/auth/naver.ts

Comment thread src/app/api/auth/naver/callback/route.ts
Comment thread src/app/api/auth/naver/login/route.ts Outdated
Comment thread src/lib/auth/__tests__/jwt.test.ts
Comment thread src/lib/auth/__tests__/naver.test.ts
Comment thread src/lib/auth/jwt.ts
Comment thread src/lib/auth/naver.ts Outdated
Comment thread src/lib/auth/naver.ts
- naver.ts: fetchWithTimeout으로 외부 API 타임아웃 처리
- naver.ts: response.id 누락 시 명시적 에러 처리
- callback/route.ts: nickname P2002 race condition 방어 처리
- login/route.ts: 에러 리다이렉트를 NEXT_PUBLIC_BASE_URL 대신 request origin 기반으로 변경
- jwt.ts: verifyAccessToken 런타임 페이로드 필드 검증 추가
- jwt.test.ts: JWT 만료 경계 테스트 추가
- naver.test.ts: response.id 누락 케이스 테스트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Siul49 Siul49 mentioned this pull request May 30, 2026
11 tasks
@Siul49 Siul49 closed this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants