00_Inbox공부를 기다리는 친구들Kakao OAuth와 JWT 로그인 쉽게 이해하기

이 문서의 목표는 Kakao 로그인을 우리 서비스의 JWT 로그인으로 연결하는 과정을, OAuth를 처음 보는 사람도 따라올 수 있을 정도로 이해하는 것이다.

핵심 문장: Kakao에게 사용자를 확인받고, 백엔드는 그 결과를 바탕으로 우리 서비스 전용 로그인 토큰을 발급한다.


0. 가장 먼저 전체 그림 보기

온라인 쇼핑몰이나 게임에 로그인한다고 생각해 보자.

어떤 사이트가 우리에게 이렇게 말한다.

“네가 정말 이 사람인지 내가 직접 확인하기는 어려워. 대신 Kakao에게 확인해 볼게.”

사용자는 Kakao에서 로그인한다. Kakao는 사용자가 로그인에 성공했는지 알려 준다. 그러면 우리 서비스는 그 결과를 보고 우리 서비스의 출입증을 발급한다.

여기서 역할이 나뉜다.

Kakao
  └─ 사용자가 Kakao 계정의 주인인지 확인한다.

우리 백엔드
  └─ Kakao의 확인 결과를 보고 우리 서비스의 로그인 세션을 만든다.

우리 프론트엔드
  └─ 사용자를 Kakao 로그인 화면으로 안내하고, 로그인 완료 후 우리 API를 사용한다.

전체 흐름은 다음과 같다.

사용자
프론트엔드의 “Kakao로 로그인” 버튼
백엔드의 로그인 시작 API
Kakao 로그인 및 동의 화면
백엔드의 callback API
Kakao 사용자 정보 확인
우리 DB에서 사용자 찾기 또는 생성
우리 서비스 전용 JWT 발급
로그인 완료

중요한 결론은 이것이다.

Kakao의 토큰과 우리 서비스의 JWT는 서로 다른 토큰이다.

0.1 이 문서의 기본 설계와 큰 흐름

이 문서는 웹 브라우저가 백엔드의 로그인 시작 주소로 이동하고, 백엔드가 Kakao와 통신하는 구조를 기준으로 설명한다.

이 구조에서 중요한 경계는 다음과 같다.

  • 브라우저는 사용자를 Kakao 로그인 화면으로 이동시키는 User Agent다.
  • 우리 백엔드는 Kakao의 OAuth Client다. Client Secret을 보관하므로 Confidential Client라고도 부른다.
  • Kakao Authorization Server는 인가 코드와 Kakao 토큰을 발급한다.
  • Kakao Resource Server는 Kakao Access Token을 사용해 사용자 정보를 반환한다.
  • 우리 백엔드는 Kakao 사용자 정보를 확인한 뒤, 우리 서비스용 JWT를 발급한다.

시스템 전체 흐름

flowchart TB
    U["사용자"] -->|"로그인 버튼 클릭"| B["브라우저<br/>User Agent"]

    subgraph OUR["우리 서비스"]
        FE["프론트엔드<br/>로그인 화면"]
        BE["백엔드<br/>OAuth Client + JWT Issuer"]
        DB[(우리 DB<br/>users / oauth_accounts / auth_sessions)]
        API["우리 API<br/>우리 JWT 검증"]

        FE -->|"GET /auth/kakao/start"| BE
        BE -->|"사용자 계정 연결 및 세션 저장"| DB
    end

    subgraph KAKAO["Kakao"]
        AS["Authorization Server<br/>kauth.kakao.com<br/>authorize / token"]
        RS["Resource Server<br/>kapi.kakao.com<br/>/v2/user/me"]
    end

    B -->|"프론트엔드 화면"| FE
    BE -->|"302 Location: /oauth/authorize"| B
    B -->|"GET /oauth/authorize"| AS
    AS -->|"302 redirect_uri?code&state"| B
    B -->|"GET /auth/kakao/callback"| BE

    BE -->|"POST /oauth/token<br/>server-to-server"| AS
    AS -->|"Kakao access_token<br/>refresh_token optional<br/>id_token optional"| BE

    BE -->|"GET /v2/user/me<br/>Bearer Kakao access_token"| RS
    RS -->|"Kakao user id<br/>동의받은 사용자 정보"| BE

    BE -->|"Set-Cookie: 우리 토큰<br/>302 프론트엔드"| B
    FE -->|"GET /auth/me<br/>credentials: include"| API
    API -->|"우리 서비스 사용자 정보"| FE

이 그림에서 특히 화살표의 방향을 보자.

  1. 브라우저는 Kakao의 인가 화면으로 이동한다.
  2. Kakao가 돌려주는 code와 state는 브라우저를 거쳐 우리 백엔드 callback으로 들어온다.
  3. 인가 코드를 토큰으로 바꾸는 POST 요청은 백엔드가 Kakao로 직접 보낸다.
  4. Kakao 사용자 정보 조회도 백엔드가 Kakao Resource Server로 직접 보낸다.
  5. 프론트엔드는 Kakao Access Token을 받아 우리 API에 보내지 않는다.
  6. 백엔드는 Kakao의 결과를 우리 사용자와 연결한 뒤 우리 JWT를 발급한다.

1. 로그인, OAuth, JWT는 각각 무엇일까?

1.1 인증(Authentication)

인증은 “네가 누구인지 확인하는 것”이다.

예를 들어 학교에서 선생님이 학생증을 확인하는 것은 인증이다.

너는 정말 이 계정의 주인인가?

Kakao 로그인에서 Kakao가 주로 담당하는 일이 인증이다.

1.2 인가(Authorization)

인가는 “무엇을 할 수 있는지 허락하는 것”이다.

학생증이 있다고 해서 교무실이나 서버실에 마음대로 들어갈 수 있는 것은 아니다.

이 사용자가 이메일, 프로필, 친구 목록 등을 볼 수 있도록 허락했는가?

Kakao는 동의 화면을 통해 어떤 정보를 우리 서비스에 제공할지 사용자에게 묻는다.

1.3 OAuth 2.0

OAuth 2.0은 원래 사용자가 가진 리소스에 대한 권한을 다른 서비스에 제한적으로 위임하기 위한 프레임워크다. Kakao Login에서는 이 흐름을 사용해 우리 백엔드가 Kakao Resource Server API를 호출할 수 있는 Access Token을 받는다.

따라서 OAuth 2.0 자체를 곧바로 “로그인 프로토콜”이라고 부르는 것은 정확하지 않다. Kakao Login에서는 Kakao 사용자 정보 API 또는 OpenID Connect를 함께 사용해 사용자를 확인한다.

우리 서비스는 사용자의 Kakao 비밀번호를 알 필요가 없다. 사용자는 Kakao에서 직접 인증하고, 우리 백엔드는 Kakao가 발급한 결과만 검증하고 사용한다.

우리 서비스: “Kakao, 이 사용자가 로그인했는지 확인해 줘.”
Kakao: “확인했어. 이 요청을 시작한 서비스가 사용할 수 있는 임시 증표를 줄게.”

1.4 JWT

JWT는 정보를 담고 서명한 문자열이다.

쉽게 말하면 백엔드가 발급한 전자 출입증이다.

이 출입증은 우리 서비스가 발급했다.
주인은 user-123이다.
10분 뒤에는 만료된다.

JWT는 보통 다음 세 부분으로 이루어진다.

Header.Payload.Signature

JWT의 내용을 단순히 읽을 수 있다고 해서 진짜라는 뜻은 아니다. 백엔드는 반드시 서명을 검증해야 한다.

1.5 OAuth 2.0 Authorization Code Grant의 정확한 용어

이 로그인 방식의 정확한 이름은 OAuth 2.0 Authorization Code Grant, 한국어로는 OAuth 2.0 인가 코드 승인 방식이라고 이해하면 된다.

OAuth 용어이 시스템에서 실제 주체쉬운 설명
Resource Owner사용자자신의 Kakao 정보에 대한 주인
User Agent브라우저사용자를 로그인 화면으로 이동시키는 도구
OAuth Client우리 백엔드Kakao에 토큰을 요청하는 서비스
Authorization ServerKakao Authorization Server로그인, 동의, authorization code와 Kakao token 발급
Resource ServerKakao Resource ServerAccess Token으로 사용자 정보를 반환
JWT Issuer우리 백엔드우리 서비스용 JWT를 발급하는 역할
Our Resource Server우리 API우리 JWT를 검사하고 API 응답을 제공

OAuth, OIDC, JWT의 관계

세 용어는 서로 다른 종류의 개념이다.

OAuth 2.0
  = 권한 위임과 토큰 발급을 위한 흐름

OpenID Connect
  = OAuth 2.0 위에 사용자 신원 정보를 표준화한 계층
  = Kakao ID Token을 사용할 수 있게 함

JWT
  = 정보를 담고 서명하는 토큰 형식
  = OAuth 자체도 아니고, 로그인 흐름 자체도 아님

따라서 다음 문장은 정확하지 않다.

“JWT로 Kakao OAuth를 한다.”

더 정확한 표현은 다음과 같다.

“Kakao의 OAuth 2.0 인가 코드 흐름으로 사용자를 확인한 뒤,
우리 백엔드가 자체 JWT를 발급한다.”

기본 REST API 흐름에서는 Kakao Access Token으로 Kakao 사용자 정보 API를 호출하고 Kakao 회원번호를 확인한다. OpenID Connect를 사용할 때는 Kakao가 발급한 ID Token도 검증할 수 있다.


2. 등장인물 소개

이 문서에서는 다섯 명의 등장인물이 나온다.

등장인물쉬운 역할실제 역할
사용자로그인하려는 사람브라우저를 조작하는 사람
프론트엔드사용자를 안내하는 화면로그인 버튼, 화면, API 호출
우리 백엔드출입증을 발급하는 직원OAuth Client, DB 연결, JWT Issuer
Kakao Authorization ServerKakao의 인가 서버로그인, 동의, 인가 코드와 Kakao 토큰 발급
Kakao Resource ServerKakao의 리소스 서버Access Token으로 사용자 정보 조회

흐름을 그림으로 표현하면 다음과 같다.

사용자 → 브라우저 → 프론트엔드 → 우리 백엔드
                                      ├─ Kakao Authorization Server
                                      ├─ Kakao Resource Server
                                      ├─ 우리 DB
                                      └─ 우리 서비스용 JWT 발급

프론트엔드와 Kakao가 직접 모든 로그인을 처리하는 구조도 만들 수 있다. 하지만 백엔드 개발자가 자체 JWT를 발급할 계획이라면 백엔드가 OAuth callback과 토큰 교환을 담당하는 구조가 이해하기 쉽고 안전하다.


3. 토큰 다섯 가지를 구분하기

처음 OAuth를 배울 때 가장 많이 헷갈리는 부분이다.

이름누가 발급하나어디에 쓰나우리 API에서 로그인 토큰으로 사용하나?
인가 코드KakaoKakao 토큰으로 한 번 교환아니오
Kakao Access TokenKakaoKakao 사용자 정보 API 호출아니오
Kakao Refresh TokenKakaoKakao Access Token 갱신아니오
우리 Access JWT우리 백엔드우리 API 호출
우리 Refresh Token우리 백엔드우리 로그인 세션 연장

3.0 토큰이 어디에서 만들어지고 어디로 이동하는가

토큰은 모두 같은 토큰이 아니다. 아래 그림에서 Kakao 영역우리 서비스 영역을 분리해서 보자.

flowchart LR
    CODE["인가 코드<br/>Authorization Code<br/>일회용"] -->|"백엔드 POST /oauth/token"| KACCESS["Kakao Access Token"]
    KACCESS -->|"Bearer<br/>GET /v2/user/me"| KUSER["Kakao 사용자 정보<br/>Kakao user id"]
    KUSER -->|"transaction upsert"| INTERNAL["우리 내부 user_id"]

    INTERNAL -->|"서명"| ACCESS["우리 Access JWT<br/>짧은 만료"]
    INTERNAL -->|"랜덤 생성 + 해시 저장"| REFRESH["우리 Refresh Token<br/>Rotation 대상"]

    ACCESS -->|"쿠키 또는 Authorization"| OURAPI["우리 API"]
    REFRESH -->|"POST /auth/refresh"| ACCESS

    KREFRESH["Kakao Refresh Token<br/>Resource Server API를 계속 쓸 때만 보관"] -->|"POST /oauth/token"| KACCESS

여기서 두 가지 갱신 흐름을 구분해야 한다.

  • Kakao Refresh Token은 Kakao Access Token을 갱신한다.
  • 우리 Refresh Token은 우리 Access JWT를 갱신한다.
  • 우리 서비스의 일반 API 요청은 Kakao Access Token이 아니라 우리 Access JWT로 인증한다.
  • Kakao Resource Server API를 로그인 이후에도 호출해야 할 때만 Kakao Refresh Token 보관을 검토한다.

3.1 인가 코드

인가 코드는 Kakao가 callback 주소로 보내 주는 임시 번호표와 같다.

“이 사람이 Kakao 로그인에 성공했으니,
백엔드가 나에게 토큰을 요청해도 돼.”

보통 한 번만 사용할 수 있다. 프론트엔드가 이 값을 오래 보관하면 안 된다.

3.2 Kakao Access Token

Kakao Resource Server의 API를 호출할 때 쓰는 토큰이다.

Authorization: Bearer KAKAO_ACCESS_TOKEN

이 토큰은 Kakao Resource Server API에 대한 출입증이지, 우리 서비스에 대한 출입증이 아니다.

3.3 우리 Access JWT

우리 백엔드가 직접 발급한다.

이 사용자는 우리 서비스의 user-123이다.
우리 API를 10분 동안 사용할 수 있다.

3.4 우리 Refresh Token

Access JWT가 만료되었을 때 다시 로그인하지 않고 새 Access JWT를 발급받기 위한 토큰이다.

권장 방식은 Refresh Token을 랜덤 문자열로 만들고, DB에는 원문이 아닌 해시만 저장하는 것이다.

3.5 Kakao Refresh Token과 우리 Refresh Token은 다르다

이 둘은 이름만 비슷하다.

Kakao Refresh Token
  → Kakao Access Token을 새로 발급받는 용도

우리 Refresh Token
  → 우리 Access JWT를 새로 발급받는 용도

단순히 Kakao 로그인만 제공하고 Kakao Resource Server API를 계속 호출하지 않는다면, Kakao Refresh Token을 장기간 저장하지 않아도 된다.


4. 개발 전에 Kakao Developers에서 설정할 것

4.1 Kakao 앱 만들기

Kakao Developers에서 앱을 만든다.

필요한 값은 다음과 같다.

REST API 키
Client Secret

REST API 키는 Kakao 로그인 요청을 만들 때 사용한다. Client Secret은 백엔드가 Kakao에 토큰을 요청할 때 사용한다.

4.2 Kakao Login 활성화

Kakao Login 사용 설정을 켠다.

꺼져 있으면 로그인 시작 단계에서 오류가 발생한다.

4.3 Redirect URI 등록

Redirect URI는 Kakao 로그인 작업이 끝난 뒤 사용자를 돌려보낼 주소다.

예를 들어 백엔드가 8080 포트에서 실행된다면 다음처럼 등록할 수 있다.

http://localhost:8080/auth/kakao/callback

운영 환경은 다음과 같이 별도로 등록한다.

https://api.example.com/auth/kakao/callback

다음은 서로 다른 주소다.

http://localhost:8080/auth/kakao/callback
http://127.0.0.1:8080/auth/kakao/callback

개발 중에는 localhost와 127.0.0.1을 섞지 않는 것이 좋다. OAuth callback, 쿠키, CORS 설정이 서로 다른 주소로 갈라질 수 있다.

4.4 동의항목 설정

우리 서비스가 필요한 정보만 요청한다.

예를 들어 닉네임만 필요하다면 이메일, 생일, 성별까지 모두 요청할 필요가 없다.

사용자가 동의하지 않은 정보는 Kakao가 보내 주지 않을 수 있다.

따라서 다음처럼 생각해야 한다.

이메일이 항상 있을 것이다
→ 위험한 가정

이메일이 있을 수도 있고 없을 수도 있다
→ 안전한 처리

4.5 Client Secret 관리

Client Secret은 비밀번호처럼 취급한다.

프론트엔드 코드에 넣지 않는다.
Git에 올리지 않는다.
로그에 출력하지 않는다.
브라우저 Network 탭에서 보이면 안 된다.

환경 변수 예시는 다음과 같다.

KAKAO_REST_API_KEY=발급받은_REST_API_키
KAKAO_CLIENT_SECRET=발급받은_CLIENT_SECRET
KAKAO_REDIRECT_URI=http://localhost:8080/auth/kakao/callback
FRONTEND_URL=http://localhost:3000

5. 전체 로그인 흐름을 한 단계씩 보기

5.0 전체 시퀀스 다이어그램

아래 시퀀스에서 세로줄은 각 주체의 시간 흐름이고, 화살표는 실제 요청 또는 응답이다.

sequenceDiagram
    autonumber
    actor U as 사용자
    participant B as "브라우저 (User Agent)"
    participant FE as 프론트엔드
    participant BE as "우리 백엔드 (OAuth Client)"
    participant AS as "Kakao Authorization Server"
    participant RS as "Kakao Resource Server"
    participant DB as "우리 DB"

    U->>B: Kakao 로그인 버튼 클릭
    B->>FE: 로그인 화면 표시
    FE->>BE: GET /auth/kakao/start
    BE->>BE: state 생성 및 저장
    Note right of BE: state는 요청마다 새로 만들고<br/>짧은 TTL과 일회성으로 관리

    BE-->>B: 302 Location: Kakao authorize URL
    B->>AS: GET /oauth/authorize<br/>client_id, redirect_uri, state
    AS-->>B: Kakao 로그인 및 동의 화면 표시
    U->>AS: Kakao 로그인 + 동의

    alt 사용자가 취소하거나 Kakao 오류 발생
        AS-->>B: 302 callback?error&state
        B->>BE: GET /auth/kakao/callback?error&state
        BE->>BE: error 처리 및 state 검증
        BE-->>B: 302 프론트엔드 오류 화면
    else 로그인과 동의 성공
        AS-->>B: 302 callback?code&state
        B->>BE: GET /auth/kakao/callback?code&state
        BE->>BE: state 검증 후 일회성 소비

        BE->>AS: POST /oauth/token<br/>code + client_id + redirect_uri + client_secret
        AS-->>BE: Kakao access_token<br/>refresh_token optional<br/>id_token optional

        BE->>RS: GET /v2/user/me<br/>Authorization: Bearer access_token
        RS-->>BE: Kakao id + 동의받은 사용자 정보

        BE->>DB: oauth_accounts 조회 또는 생성
        DB-->>BE: 내부 user_id

        BE->>BE: 우리 Access JWT 서명
        BE->>DB: 우리 Refresh Session 저장 및 해시 관리
        BE-->>B: Set-Cookie + 302 프론트엔드 callback

        B->>FE: 로그인 완료 화면
        FE->>BE: GET /auth/me<br/>credentials: include
        BE-->>FE: 우리 서비스 사용자 정보
    end

요청이 실제로 어느 방향으로 가는가

순서요청요청을 보내는 주체받는 주체목적
1GET /auth/kakao/start프론트엔드우리 백엔드로그인 흐름 시작
2GET /oauth/authorize브라우저Kakao Authorization Server사용자 로그인과 동의
3GET /auth/kakao/callback?code&state브라우저우리 백엔드Kakao 결과 전달
4POST /oauth/token우리 백엔드Kakao Authorization Servercode를 Kakao token으로 교환
5GET /v2/user/me우리 백엔드Kakao Resource ServerKakao 사용자 정보 조회
6GET /auth/me프론트엔드우리 백엔드우리 로그인 상태 확인
7POST /auth/refresh브라우저우리 백엔드우리 Access JWT 갱신

2번과 3번은 브라우저 이동, 4번과 5번은 백엔드와 Kakao 서버 사이의 서버 간 통신이라는 차이가 핵심이다.

5.1 1단계: 사용자가 로그인 버튼을 누른다

프론트엔드는 Kakao Access Token을 직접 받으려고 하기보다 백엔드의 로그인 시작 API로 이동한다.

window.location.href =
  'http://localhost:8080/auth/kakao/start';

또는 버튼을 누르면 다음 주소로 이동하도록 한다.

GET /auth/kakao/start

이 API는 보통 JSON을 반환하지 않고 Kakao 로그인 화면으로 302 Redirect 한다.

5.2 2단계: 백엔드가 state를 만든다

백엔드는 로그인 요청마다 랜덤한 state를 만든다.

state는 놀이공원에서 받은 번호표와 비슷하다.

백엔드: “이번 로그인 요청의 번호표는 A123이야.”
Kakao: “로그인 후 A123을 다시 돌려줄게.”
백엔드: “돌아온 번호표가 내가 발급한 A123과 같네.”

백엔드는 다음 정보를 Redis 또는 DB에 짧게 저장한다.

{
  "stateHash": "저장할 state의 해시",
  "returnTo": "/dashboard",
  "createdAt": "2026-08-06T10:00:00Z",
  "expiresAt": "2026-08-06T10:05:00Z"
}

state는 다음 조건을 만족해야 한다.

  • 로그인 요청마다 새로 만든다.
  • 예측하기 어렵게 만든다.
  • 5~10분 후 만료시킨다.
  • 한 번 확인하면 폐기한다.
  • 로그인 요청을 시작한 브라우저와 연결한다.

5.3 3단계: 백엔드가 Kakao 로그인 주소를 만든다

백엔드는 Kakao Authorization Server의 authorization endpoint 주소를 만든다.

https://kauth.kakao.com/oauth/authorize
  ?response_type=code
  &client_id=REST_API_KEY
  &redirect_uri=등록한_CALLBACK_URL
  &state=랜덤한_STATE

각 파라미터의 의미는 다음과 같다.

파라미터의미
response_type=code로그인 결과로 인가 코드를 받겠다는 뜻
client_idKakao 앱의 REST API 키
redirect_uri로그인 완료 후 돌아올 백엔드 주소
state내가 시작한 로그인인지 확인하는 번호표

백엔드는 이 주소를 302 Redirect 응답으로 브라우저에 보낸다.

5.4 4단계: 사용자가 Kakao에서 로그인한다

브라우저가 Kakao Authorization Server의 로그인·동의 화면으로 이동한다.

사용자는 다음 과정을 수행할 수 있다.

1. Kakao 계정 로그인
2. 우리 서비스에 제공할 정보 확인
3. 동의하고 계속하기 선택

우리 백엔드는 사용자의 Kakao 비밀번호를 보지 못한다.

5.5 5단계: Kakao가 callback으로 돌아온다

로그인이 성공하면 Kakao는 등록해 둔 callback 주소로 이동시킨다.

GET /auth/kakao/callback
  ?code=일회용인가코드
  &state=아까의_STATE

사용자가 취소하면 다음처럼 돌아올 수 있다.

GET /auth/kakao/callback
  ?error=access_denied
  &error_description=User%20denied%20access
  &state=아까의_STATE

백엔드는 먼저 state를 확인해야 한다.

1. callback에 state가 있는가?
2. 저장해 둔 state와 같은가?
3. 아직 만료되지 않았는가?
4. 이미 사용한 state가 아닌가?

다르면 로그인 처리를 중단한다.

5.6 6단계: 백엔드가 인가 코드를 Kakao 토큰으로 바꾼다

인가 코드는 바로 사용자 정보가 아니다. 백엔드가 Kakao에 다시 요청해서 Access Token으로 교환해야 한다.

POST https://kauth.kakao.com/oauth/token
Content-Type: application/x-www-form-urlencoded;charset=utf-8

본문은 다음과 같다.

grant_type=authorization_code
client_id=REST_API_KEY
redirect_uri=같은_CALLBACK_URL
code=인가코드
client_secret=CLIENT_SECRET

예시 코드:

const tokenResponse = await fetch(
  'https://kauth.kakao.com/oauth/token',
  {
    method: 'POST',
    headers: {
      'Content-Type':
        'application/x-www-form-urlencoded;charset=utf-8'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: process.env.KAKAO_REST_API_KEY,
      redirect_uri: process.env.KAKAO_REDIRECT_URI,
      code,
      client_secret: process.env.KAKAO_CLIENT_SECRET
    })
  }
);

const kakaoToken = await tokenResponse.json();

응답에는 보통 다음과 같은 값이 들어 있다.

{
  "token_type": "bearer",
  "access_token": "Kakao가 발급한 토큰",
  "refresh_token": "Kakao가 발급한 갱신 토큰",
  "expires_in": 21599
}

여기서 받은 Kakao Access Token은 아직 우리 서비스 JWT가 아니다.

5.7 7단계: Kakao 사용자 정보를 조회한다

백엔드는 Kakao Access Token으로 Kakao Resource Server의 사용자 정보 API를 호출한다.

GET https://kapi.kakao.com/v2/user/me
Authorization: Bearer KAKAO_ACCESS_TOKEN

예시:

const meResponse = await fetch(
  'https://kapi.kakao.com/v2/user/me',
  {
    headers: {
      Authorization:
        'Bearer ' + kakaoToken.access_token
    }
  }
);

const kakaoUser = await meResponse.json();

응답 예시는 다음과 비슷하다.

{
  "id": 123456789,
  "kakao_account": {
    "email": "user@example.com",
    "profile": {
      "nickname": "철수",
      "profile_image_url": "https://..."
    }
  }
}

우리 서비스에서 가장 중요한 값은 id다.

const provider = 'kakao';
const providerUserId = String(kakaoUser.id);

이 값을 우리 DB의 Kakao 계정 식별자로 사용한다.

5.8 8단계: 우리 DB에서 사용자를 찾거나 만든다

DB에서 다음 조건으로 찾는다.

provider = kakao
provider_user_id = kakaoUser.id

이미 있으면 기존 사용자를 로그인시킨다.

없으면 다음 순서로 새 사용자를 만든다.

1. users 테이블에 서비스 사용자 생성
2. oauth_accounts 테이블에 Kakao 계정 연결 정보 생성
3. 두 작업을 하나의 트랜잭션으로 처리

추천 테이블 구조는 다음과 같다.

users
-----
id
created_at

oauth_accounts
--------------
id
user_id
provider              -- kakao
provider_user_id      -- Kakao의 id
email_at_login
created_at

반드시 다음 제약 조건을 둔다.

provider + provider_user_id는 유일해야 한다.

그래야 같은 Kakao 계정으로 로그인할 때 사용자가 매번 새로 생기지 않는다.

5.9 9단계: 우리 서비스용 JWT를 발급한다

DB 사용자 확인이 끝나면 백엔드가 우리 서비스 전용 Access JWT를 만든다.

{
  "iss": "https://api.example.com",
  "sub": "우리 서비스의 사용자 ID",
  "aud": "our-api",
  "iat": 1754432400,
  "exp": 1754433000,
  "jti": "토큰 고유 ID"
}

여기서 sub에는 Kakao ID보다 우리 DB의 사용자 ID를 넣는 것이 좋다.

Kakao ID: 123456789
우리 사용자 ID: user-abc-123

API는 Kakao를 직접 알 필요 없이 우리 사용자 ID만 사용하면 된다.

5.10 10단계: JWT를 쿠키에 담고 프론트로 돌아간다

웹 브라우저에서는 토큰을 URL에 넣지 않고 HttpOnly 쿠키에 넣는 방식을 권장한다.

Set-Cookie: access_token=...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/

Set-Cookie: refresh_token=...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/auth/refresh

그다음 프론트엔드로 Redirect 한다.

302 https://app.example.com/login/callback?success=true

프론트엔드는 Redirect된 뒤 우리 백엔드에 현재 사용자 정보를 요청한다.

const response = await fetch(
  'https://api.example.com/auth/me',
  {
    credentials: 'include'
  }
);

6. 백엔드 callback의 전체 의사코드

실제 프레임워크는 Express, Spring Boot, NestJS 등으로 달라질 수 있지만, 순서는 거의 같다.

async function kakaoCallback(request, response) {
  const code = request.query.code;
  const state = request.query.state;
  const error = request.query.error;

  // 1. 사용자가 Kakao 로그인 화면에서 취소했는지 확인
  if (error) {
    return redirectToLoginError(response, error);
  }

  // 2. 인가 코드와 state가 모두 있는지 확인
  if (!code || !state) {
    return redirectToLoginError(response, 'invalid_callback');
  }

  // 3. Redis/DB에서 state를 찾고 한 번에 소비한다.
  const oauthRequest =
    await oauthStateStore.consume(state);

  if (!oauthRequest) {
    return redirectToLoginError(response, 'invalid_state');
  }

  // 4. Kakao에 인가 코드를 보내 Access Token을 받는다.
  const kakaoToken =
    await exchangeCodeForKakaoToken(code);

  // 5. Kakao 사용자 정보를 조회한다.
  const kakaoUser =
    await getKakaoUser(kakaoToken.access_token);

  // 6. 우리 DB의 사용자와 연결한다.
  const user = await database.transaction(
    async (tx) &ge; {
      return upsertKakaoUser(tx, {
        provider: 'kakao',
        providerUserId: String(kakaoUser.id),
        email: kakaoUser.kakao_account?.email,
        nickname:
          kakaoUser.kakao_account?.profile?.nickname
      });
    }
  );

  // 7. 우리 서비스 Access JWT를 만든다.
  const accessToken =
    createAccessJwt(user.id);

  // 8. 우리 Refresh Token을 만들고 해시를 DB에 저장한다.
  const refreshToken =
    await createRefreshSession(user.id);

  // 9. 브라우저에 토큰을 HttpOnly 쿠키로 설정한다.
  setAuthCookies(response, {
    accessToken,
    refreshToken
  });

  // 10. 토큰을 URL에 넣지 않고 프론트로 이동시킨다.
  return response.redirect(
    'https://app.example.com/login/callback?success=true'
  );
}

실제 구현에서는 Kakao Access Token, Client Secret, 인가 코드가 로그에 출력되지 않도록 해야 한다.


7. 우리 JWT는 어떻게 설계할까?

7.1 Access JWT

Access JWT는 API를 사용할 때 제출하는 짧은 출입증이다.

권장 만료 시간: 5~15분

예를 들어 사용자가 게시글을 조회하면 백엔드는 JWT를 확인한다.

이 JWT의 서명이 진짜인가?
만료되지 않았는가?
우리 서비스가 발급한 것인가?
사용자 ID가 존재하는가?

7.2 Refresh Token

Access JWT가 만료되었을 때 새 Access JWT를 발급받는 긴 출입증이다.

권장 구조는 다음과 같다.

Access JWT
  - 짧은 만료 시간
  - API 요청에 사용
  - 탈취되어도 피해 시간을 줄임

Refresh Token
  - 상대적으로 긴 만료 시간
  - /auth/refresh에서만 사용
  - DB 세션과 연결
  - 매번 사용 후 교체

7.3 Refresh Token Rotation

Refresh Token을 사용할 때마다 새 Refresh Token으로 교체한다.

기존 Refresh Token A 사용
A 폐기
새 Refresh Token B 발급

이미 폐기된 A가 다시 사용되면 토큰 탈취를 의심하고 같은 세션 가족 전체를 폐기할 수 있다.

7.4 Refresh Token도 JWT로 만들 수 있을까?

가능하다. 하지만 JWT는 기본적으로 발급한 뒤 서버가 즉시 취소하기 어렵다.

Refresh JWT를 사용하려면 다음을 추가하는 것이 좋다.

- jti 저장
- 세션 ID 저장
- revoked_at 관리
- Rotation 적용
- 재사용 감지

그래서 실무에서는 다음 구성을 많이 사용한다.

Access Token = JWT
Refresh Token = 랜덤 문자열 + DB 세션

이것도 충분히 JWT 방식의 로그인 시스템이다.


8. 프론트엔드와 백엔드가 약속할 API

로그인 시작

GET /auth/kakao/start

응답:

302 Kakao 로그인 주소

로그인 callback

GET /auth/kakao/callback?code=...&state=...

백엔드 내부에서 다음을 처리한다.

state 확인
인가 코드 교환
사용자 정보 조회
DB upsert
우리 JWT 발급
쿠키 설정
프론트로 Redirect

현재 사용자 조회

GET /auth/me

성공:

{
  "id": "user-123",
  "nickname": "철수"
}

로그인되지 않았으면 다음처럼 응답할 수 있다.

401 Unauthorized

토큰 갱신

POST /auth/refresh

백엔드는 Refresh Token 쿠키를 확인하고 새 Access JWT와 새 Refresh Token을 발급한다.

로그아웃

POST /auth/logout

백엔드는 다음을 수행한다.

1. 현재 Refresh Session 폐기
2. Access Token 쿠키 삭제
3. Refresh Token 쿠키 삭제

9. 쿠키를 사용할 때 프론트와 백엔드가 확인할 것

프론트와 백엔드가 서로 다른 주소에 있다면 다음 설정이 필요하다.

fetch('https://api.example.com/auth/me', {
  credentials: 'include'
});

백엔드는 허용할 프론트 주소를 정확하게 지정해야 한다.

Access-Control-Allow-Origin:
https://app.example.com

Access-Control-Allow-Credentials:
true

다음처럼 모든 출처를 허용하면서 credentials를 함께 사용하는 것은 피한다.

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

쿠키 옵션은 환경에 따라 달라진다.

같은 사이트에 가까운 구조
  → SameSite=Lax 고려

서로 다른 사이트 간 쿠키
  → SameSite=None; Secure 필요
  → CSRF 방어 추가 필요

OAuth의 state는 로그인 요청을 보호하는 값이다. 일반 API의 모든 POST 요청을 보호하는 CSRF 토큰과는 역할이 다르다.


10. Kakao 로그아웃과 우리 로그아웃은 다르다

10.1 우리 서비스 로그아웃

가장 일반적인 로그아웃은 우리 서비스 세션만 끝내는 것이다.

우리 Refresh Session revoke
우리 쿠키 삭제

이 경우 사용자가 Kakao 사이트에 로그인한 상태는 유지될 수 있다.

10.2 Kakao 토큰 로그아웃

Kakao의 로그아웃 기능은 Kakao에서 발급한 토큰을 폐기하는 기능이다.

우리 서비스 Access JWT를 자동으로 폐기해 주는 것은 아니다.

10.3 Kakao 계정까지 로그아웃

Kakao 계정의 브라우저 로그인 세션까지 로그아웃하려면 Kakao가 제공하는 계정 로그아웃 흐름을 별도로 사용해야 한다.

10.4 연결 해제

연결 해제는 로그아웃보다 훨씬 강한 작업이다.

Kakao와 우리 앱의 연결 해제
동의 철회
Kakao 토큰 폐기

회원 탈퇴 기능과 연결할 때 신중하게 처리해야 한다.


11. OpenID Connect와 Kakao ID Token

Kakao Login은 기본 OAuth 흐름만 사용할 수도 있고, OpenID Connect를 추가로 사용할 수도 있다.

OAuth만 사용하는 경우

Kakao Access Token 발급
GET /v2/user/me
Kakao 사용자 id 확인

OpenID Connect를 사용하는 경우

Kakao Access Token 발급
Kakao ID Token도 발급
ID Token의 서명과 claims 검증
사용자 확인

ID Token은 JWT 모양이지만 우리 서비스 JWT가 아니다.

ID Token을 직접 사용한다면 다음을 반드시 검증한다.

서명
iss
aud
exp
nonce
sub

Kakao OIDC ID Token의 서명을 검증할 때 사용할 공개키 주소는 다음과 같다.

https://kauth.kakao.com/.well-known/jwks.json

ID Token을 단순히 Base64 Decode해서 믿으면 안 된다.


12. 자주 발생하는 오류

12.1 Redirect URI 오류

증상:

KOE006

확인할 것:

- Kakao Developers에 URI가 등록되어 있는가?
- 코드의 URI와 등록된 URI가 완전히 같은가?
- http와 https가 다른가?
- 포트가 다른가?
- 마지막 슬래시가 다른가?
- localhost와 127.0.0.1을 섞었는가?

12.2 Client Secret 오류

증상:

토큰 발급 단계에서 401 또는 invalid_client

확인할 것:

- Client Secret이 활성화되어 있는가?
- 백엔드 요청에 client_secret이 들어갔는가?
- 다른 Kakao 앱의 키를 사용하지 않았는가?
- 환경 변수를 잘못 읽고 있지 않은가?

12.3 인가 코드를 다시 사용함

증상:

invalid_grant

인가 코드는 일회용이므로 다음 상황에서 발생할 수 있다.

- callback 페이지 새로고침
- 같은 code를 재시도
- callback URL을 여러 번 호출

로그인 성공 후에는 프론트의 깨끗한 URL로 Redirect하도록 한다.

12.4 state 불일치

증상:

로그인은 성공했지만 백엔드가 invalid_state로 거부

확인할 것:

- 로그인 시작 시 저장한 state가 있는가?
- 여러 탭에서 동시에 로그인하지 않았는가?
- 쿠키가 callback 요청에 포함되는가?
- state를 너무 빨리 삭제하지 않았는가?

12.5 이메일이 없음

이메일 동의를 받지 않았거나 사용자가 동의하지 않으면 이메일이 없을 수 있다.

const email =
  kakaoUser.kakao_account?.email ?? null;

사용자 식별은 이메일이 아니라 Kakao id로 해야 한다.

12.6 로그인은 되었는데 프론트에서 401

확인할 것:

- fetch에 credentials: 'include'가 있는가?
- CORS에서 정확한 프론트 origin을 허용했는가?
- 쿠키의 Domain이 맞는가?
- Secure 쿠키를 http 개발 환경에서 사용하고 있지 않은가?
- SameSite 설정이 현재 주소 구조와 맞는가?
- API 요청 주소가 callback 주소와 다른 환경을 가리키지 않는가?

13. 절대 하지 말아야 할 것

13.1 Client Secret을 프론트에 넣기

// 금지
const clientSecret = 'Kakao Client Secret';

브라우저 코드는 사용자에게 모두 공개된다.

13.2 Kakao Access Token을 우리 API 토큰으로 사용하기

Kakao Access Token은 Kakao Resource Server API용이다.

우리 백엔드는 사용자 확인 후 자체 JWT를 발급해야 한다.

13.3 이메일만으로 사용자 식별하기

이메일은 없을 수 있고 변경될 수 있다.

provider + provider_user_id

를 유일 키로 사용한다.

13.4 JWT를 Decode만 하고 사용하기

Decode는 누구나 할 수 있다.

Decode 성공 = 진짜 토큰

가 아니다.

반드시 서명을 검증한다.

13.5 토큰을 URL에 넣기

/callback?access_token=...

토큰이 브라우저 기록과 로그에 남을 수 있다.

13.6 사용자가 보낸 프로필을 믿기

프론트가 보내는 닉네임과 이메일은 조작할 수 있다.

사용자 식별 정보는 백엔드가 Kakao Resource Server의 사용자 정보 API에서 직접 조회해야 한다.

13.7 임의의 returnTo 주소로 Redirect하기

다음과 같은 방식은 Open Redirect 취약점이 될 수 있다.

/auth/kakao/start?returnTo=https://evil.example.com

returnTo를 사용한다면 서버에서 허용된 내부 경로만 저장하고 사용한다.


14. 추천 DB 구조

users
  서비스에서 사용하는 사용자 본체

oauth_accounts
  Google, Kakao, GitHub 등 외부 계정 연결 정보

auth_sessions
  우리 서비스의 Refresh Token 세션

이 구조를 사용하면 나중에 Kakao 외에 Google 로그인을 추가하기 쉽다.

users 1 ─── N oauth_accounts
users 1 ─── N auth_sessions

예를 들어 한 사용자가 다음 계정을 모두 연결할 수 있다.

users.user-123
  ├─ kakao / 123456789
  ├─ google / google-sub-value
  └─ github / github-user-id

외부 로그인 제공자와 우리 서비스 사용자를 분리해 두면 인증 제공자를 바꾸거나 여러 로그인 수단을 연결하기 쉬워진다.


15. 개발 순서

한 번에 모든 기능을 만들지 말고 다음 순서로 확인한다.

1단계: Kakao 설정 확인

Kakao Login ON
Redirect URI 등록
REST API 키 확인
Client Secret 확인

2단계: 로그인 시작과 callback 만들기

먼저 callback으로 code와 state가 들어오는지만 확인한다.

단, 인가 코드와 Client Secret을 로그에 그대로 출력하지 않는다.

3단계: 토큰 교환 확인

백엔드에서 Kakao /oauth/token 호출이 성공하는지 확인한다.

4단계: 사용자 정보 조회 확인

/v2/user/me에서 Kakao id가 들어오는지 확인한다.

5단계: DB 연결

같은 Kakao 계정으로 두 번 로그인했을 때 사용자가 두 명 생기지 않는지 확인한다.

6단계: 자체 Access JWT 발급

우리 API가 자체 JWT를 검증하도록 만든다.

7단계: Refresh Token 추가

세션 저장, Rotation, 폐기, 재사용 감지를 추가한다.

8단계: 프론트 연결

쿠키, CORS, credentials: include를 연결한다.

9단계: 로그아웃과 탈퇴 분리

일반 로그아웃과 Kakao 연결 해제를 서로 다른 기능으로 구현한다.


16. 테스트 체크리스트

정상 흐름

  • Kakao 로그인 버튼을 누르면 Kakao 화면으로 이동하는가?
  • 로그인 후 callback으로 돌아오는가?
  • state 검증이 성공하는가?
  • Kakao Access Token을 발급받는가?
  • Kakao 사용자 정보를 조회하는가?
  • DB에 한 명의 사용자만 생성되는가?
  • 자체 JWT가 발급되는가?
  • /auth/me가 사용자 정보를 반환하는가?

실패 흐름

  • Kakao 로그인 취소를 처리하는가?
  • state가 다르면 거부하는가?
  • 만료된 state를 거부하는가?
  • 인가 코드 재사용을 처리하는가?
  • 이메일이 없어도 로그인 가능한가?
  • 잘못된 Client Secret을 안전하게 처리하는가?
  • Redirect URI가 다를 때 원인을 확인할 수 있는가?

세션 흐름

  • Access JWT 만료 후 refresh가 되는가?
  • Refresh Token이 Rotation 되는가?
  • 폐기된 Refresh Token을 다시 사용하면 차단되는가?
  • 로그아웃 후 /auth/me가 401을 반환하는가?
  • 브라우저 새로고침 후에도 세션이 유지되는가?
  • 다른 사용자의 user ID를 JWT에 넣어도 위조할 수 없는가?

보안 흐름

  • Client Secret이 프론트에 노출되지 않는가?
  • Kakao Access Token이 로그에 남지 않는가?
  • 우리 JWT 서명을 실제로 검증하는가?
  • 토큰을 URL에 넣지 않는가?
  • 허용하지 않은 returnTo로 Redirect하지 않는가?
  • CORS에서 모든 Origin을 허용하지 않는가?

17. 최종 요약

Kakao OAuth와 자체 JWT 로그인은 다음처럼 연결된다.

1. 프론트엔드가 백엔드 로그인 시작 API 호출
2. 백엔드가 state 생성
3. Kakao 로그인 페이지로 Redirect
4. 사용자가 Kakao 로그인과 동의 수행
5. Kakao가 callback에 code와 state 전달
6. 백엔드가 state 검증
7. 백엔드가 code를 Kakao Access Token으로 교환
8. 백엔드가 Kakao 사용자 정보 조회
9. Kakao id로 우리 DB 사용자 조회 또는 생성
10. 백엔드가 우리 서비스용 Access JWT 발급
11. 우리 Refresh Token 세션 저장
12. HttpOnly 쿠키 설정
13. 프론트엔드가 우리 API 사용

가장 중요한 한 문장만 다시 기억하면 된다.

Kakao는 사용자를 확인하고, 우리 백엔드는 우리 서비스의 로그인 토큰을 발급한다.


공식 문서

Built with LogoFlowershow