카카오 로그인은 비밀번호 대신 무엇을 전달할까

X Facebook

Written by

in

“카카오로 로그인” 버튼을 처음 붙일 때는 우리 서버가 카카오 아이디와 비밀번호를 받아 확인하는 줄 알았다. 하지만 문서에는 비밀번호를 받는 API 대신 브라우저를 카카오 로그인 화면으로 보내라고 적혀 있었다.

우리 서비스가 비밀번호를 보지 않는다면 무엇으로 사용자를 확인할까? 버튼을 누른 뒤 브라우저가 이동하는 경로를 따라가며 인증 코드와 토큰의 역할, 안전하게 전달하기 위한 절차를 살펴본다.

1. 비밀번호는 우리 서버를 거치지 않는다

로그인인데 우리 서비스는 사용자의 카카오 비밀번호를 한 번도 보지 않는다. 볼 방법 자체가 없다.

사용자는 비밀번호를 카카오한테만 준다. 카카오가 그 자리에서 “이 사람 맞다”를 확인하고, 우리에게는 확인했다는 증거만 넘겨준다. 우리 서버가 비밀번호를 저장할 일도, 잘못 저장해서 유출시킬 일도 없어진다는 뜻이다.

발렛파킹에 차를 맡길 때 집·트렁크까지 다 열리는 마스터키를 주지는 않는다. 시동과 주차만 되는 발렛 열쇠를 준다. 소셜 로그인에서 우리 앱이 받는 토큰이 이 발렛 열쇠다. 카카오 비밀번호(마스터키)는 못 보고, “프로필 조회 정도만 가능한 제한된 열쇠”만 받는다.

이 구조에 등장인물이 넷 있다. 이름이 딱딱해서 외우기는 쉽지 않은데, 각자 무슨 일을 하는지만 잡아 두면 뒤가 편하다.

사용자가 우리 앱에 로그인을 위임하고, 우리 앱이 인증 서버(카카오 로그인)에 인증을 요청해 토큰을 발급받은 뒤, 그 토큰으로 리소스 서버(카카오 API)를 호출하는 네 역할의 관계

역할 정체 하는 일
Resource Owner 사용자 자기 정보의 주인이다. 위임을 승인(동의)한다
Client 우리 앱 사용자 대신 권한을 요청하고 사용한다
Authorization Server 카카오·구글 인증 서버 로그인을 처리하고 토큰을 발급한다
Resource Server 카카오·구글 API 토큰을 검사하고 프로필 같은 걸 내준다

표 1. 소셜 로그인에 등장하는 네 역할

2. “너 누구야”와 “그거 해도 돼”는 다른 질문이다

용어 하나를 정리하고 가야 뒤가 안 꼬인다. 인증과 인가는 글자가 비슷해서 같은 말로 읽고 넘어가기 쉬운데, 둘은 묻는 질문이 아예 다르다.

인증(Authentication) 은 “너 누구야”를 확인하는 것이고, 인가(Authorization) 는 “그거 해도 돼”를 확인하는 것이다.

인증(Authentication) 인가(Authorization)
묻는 질문 “너 누구야?” “그거 해도 돼?”
확인하는 것 신원 — 로그인 여부, 토큰 유효성 권한 — 역할, 범위, 조건
실패했을 때 401 Unauthorized 403 Forbidden
순서 먼저 인증 다음에

표 2. 인증과 인가가 서로 다른 질문이라는 것

신원이 확인돼도 권한이 없으면 막힌다. 로그인은 멀쩡히 됐는데 관리자 페이지에서 403이 뜨는 화면이 그 경우다.

그런데 OAuth 2.0은 이름부터 authorization, 즉 원래 인가용으로 만들어진 규격이다. “이 앱한테 내 프로필 읽을 권한을 줄게”를 처리하려고 만든 것이지, “이 사람이 누구인가”를 알려주려고 만든 게 아니다. 로그인까지 하려면 그 위에 한 겹 얹은 OIDC(OpenID Connect) 가 필요하다. 우리가 “소셜 로그인”이라 부르는 건 사실상 OIDC 인데, 통칭이 굳어져서 그냥 OAuth로그인이라고 부른다.

3. 버튼을 누르면 화면은 어디로 가나

버튼 하나 눌렀는데 주소창이 kakao.com으로 바뀌었다가 다시 우리 사이트로 돌아온다. 이게 리다이렉트(redirect) 다.

서버가 응답으로 “그 페이지 여기 없고 저 주소로 가라”고 알려주면, 브라우저가 사용자에게 묻지 않고 알아서 그 주소로 이동한다. 사용자에게는 화면이 저절로 넘어가는 것으로 보인다. 로그인 도중 주소창이 잠깐 다른 도메인으로 바뀌는 화면이 이것이다. 소셜 로그인은 이 이동을 두 번 쓴다. 우리 사이트에서 카카오로 한 번, 카카오에서 우리 사이트로 다시 한 번.

전체 순서를 먼저 보자.

카카오 로그인 클릭부터 PKCE 값 생성, 카카오 리다이렉트와 동의, code 전달, code_verifier를 포함한 토큰 교환과 검증, access_token으로 프로필을 받아 우리 서비스 로그인을 마치기까지 ①~⑪ 단계의 순서

④ 에서 사용자가 비밀번호를 입력하는 화면은 카카오 도메인이다. 우리 코드가 아니라 카카오 코드다. 그래서 우리는 그 값을 볼 수 없고, 볼 수 없으니 흘릴 수도 없다.

⑤ 에서 돌아올 때 주소 뒤에 붙어 오는 code가 이 흐름의 핵심이다. 여기서 토큰이 바로 오지 않는다.

4. 왜 토큰을 바로 주지 않을까

⑤ 를 다시 보면, 카카오는 우리에게 access_token을 주지 않는다. code 라는 짧은 문자열 하나를 준다. 그걸 받아서 ⑦ 에서 한 번 더 교환해야 진짜 토큰이 나온다. 한 단계 더 거치는데 왜 이렇게 만들었을까.

⑤ 는 브라우저 주소창을 타고 오는 길이기 때문이다. 옛날 방식(Implicit)은 여기에 access_token을 그대로 실어 보냈다. 그러면 토큰이 주소창에 찍히고, 브라우저 방문 기록에 남고, 중간 서버들의 접속 로그에도 남는다. 사용자가 브라우저를 닫고 한참 뒤에도, 그 기록을 열어 본 사람은 토큰을 그대로 쓸 수 있다.

그래서 앞단(브라우저)에는 노출돼도 상대적으로 덜 위험한 일회용 code 만 흘리고, 진짜 토큰은 ⑦ 의 서버↔서버 통신으로 받는다. code는 수명이 초~분 단위로 아주 짧고, 한 번 쓰면 그걸로 끝이다. 주소창에 남아 있어도 이미 교환이 끝난 뒤라면 아무것도 못 연다.

교환 요청(⑦)에는 우리 앱만 아는 client_secret이 들어간다. 그러니까 code를 주운 사람이 있어도, client_secret이 없으면 토큰으로 바꿀 수 없다.

5. 브라우저 앱과 모바일 앱은 어떻게 하나

앞 문단의 안전장치는 서버가 있을 때 이야기다. 앞단만 있는 SPA 나 모바일 앱은 client_secret을 숨길 데가 없다. 코드에 박아 두면 브라우저 개발자 도구에서도, 앱 파일을 뜯어도 그대로 보인다.

그 자리를 메우는 게 PKCE(RFC 7636) 다.

② code_verifier = 랜덤 문자열 (앱만 앎)
   code_challenge = SHA256(code_verifier)   ← 이것만 ③에서 카카오에 보냄
⑦ 토큰 교환 때 원본 code_verifier 제출
⑧ 카카오: SHA256(code_verifier) == 아까 받은 code_challenge ?  → 맞아야 토큰 발급

요청을 시작할 때 앱이 랜덤 문자열(code_verifier)을 만들어 자기만 갖고 있고, 그걸 해시한 값(code_challenge)만 카카오에 미리 보내 둔다. 나중에 토큰을 교환할 때 원본을 제출하면, 카카오가 다시 해시해서 아까 받아 둔 값과 맞는지 대조한다.

공격자가 code를 중간에 가로채도 code_verifier를 모르니 토큰으로 바꿀 수 없다. 요즘은 서버가 있는 앱까지 포함해 모든 클라이언트에 PKCE를 권장한다. OAuth 2.1에서는 기본이다.

6. URL에 붙는 것들 — state는 왜 있나

③ 에서 브라우저를 실제로 어디로 보내는지 보면 이렇게 생겼다.

https://kauth.kakao.com/oauth/authorize
  ?response_type=code                      # 인가 코드 방식
  &client_id=YOUR_APP_KEY                   # 우리 앱 식별자
  &redirect_uri=https://myapp.com/callback  # 코드 받을 주소(사전 등록 필수)
  &scope=openid profile_nickname account_email  # 요청 권한 범위
  &state=xyz123                             # CSRF 방어용 랜덤값
  &code_challenge=E9Melhoa...               # PKCE
  &code_challenge_method=S256

돌아올 때는 이렇게 온다. https://myapp.com/callback?code=AUTH_CODE&state=xyz123

여기서 state는 쓰임새를 짚지 않은 채 아무 값이나 넣고 넘어가기 쉬운 자리다. 이 값이 CSRF(위조된 콜백) 방어용이다.

CSRF는 공격자가 사용자 브라우저를 시켜서 사용자 의사와 무관한 요청을 보내게 만드는 공격이다. 콜백 주소는 공개돼 있으니, 공격자가 자기 계정으로 받은 code를 사용자에게 들려 보내면 사용자가 공격자 계정으로 로그인되는 일이 생길 수 있다. 그래서 요청을 시작할 때 랜덤값을 만들어 state에 담아 보내고 우리 쪽에도 저장해 둔다. 돌아온 state가 저장해 둔 값과 다르면 우리가 시작한 요청이 아니라는 뜻이니 버린다. 자세한 건 쿠키가 자동으로 붙는데 왜 토큰을 또 보내나 쪽이다.

redirect_uri도 아무 주소나 적으면 되는 값이 아니다. 카카오 개발자 콘솔에 미리 등록한 주소와 완전히 일치해야만 허용된다. 이게 없으면 공격자가 자기 서버 주소를 넣어 code를 빼돌릴 수 있다.

OIDC에는 nonce도 있다. 요청할 때 심은 값이 발급된 신원 증명 안에 그대로 들어 있는지 확인해서, 예전에 받았던 응답을 재사용(replay)하는 걸 막는다.

7. 받은 토큰은 어떻게 실어 보내나

토큰을 받았다고 로그인이 끝나지는 않는다. 그걸 들고 “나 인증된 사용자야”를 매 요청마다 증명해야 한다. 표준 방식이 HTTP 요청 헤더에 실어 보내는 것이다.

GET /v2/users/me HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1Ni...

Bearer는 소지자라는 뜻이다. 말 그대로 이 토큰을 가진 사람이면 누구든 통과시킨다. 추가로 “정말 너 맞아?”를 되묻지 않는다. 편한 대신 탈취되면 그대로 사칭당한다는 뜻이라, HTTPS 필수·짧은 만료·재발급으로 위험을 줄인다.

쿠키와 비교하면 차이가 분명해진다. 쿠키는 브라우저가 조건에 맞으면 알아서 붙여 보낸다. 편하지만 사용자 의사와 무관하게 붙는다는 게 CSRF가 성립하는 조건이 된다. Bearer 헤더는 우리 코드가 명시적으로 붙여야 붙는다. 쿠키와 세션이 무엇인지는 로그인은 한 번뿐인데 서버는 계속 같은 사람으로 알아본다 에서 따로 다룬다. 서버가 로그인 상태를 기억하고 브라우저에는 그 방 번호만 쿠키로 주는 방식이다.

이 흐름에서 오가는 것들을 한 자리에 모으면 넷이다.

이름 용도 성격
authorization code 토큰으로 교환할 일회용 표 수명이 초~분으로 매우 짧다. 1회용
access_token 리소스 API 호출용 열쇠 짧은 수명. Authorization: Bearer로 전송
refresh_token access_token 재발급용 긴 수명. 서버에 보관
id_token (OIDC) “누가 로그인했나” 신원 증명 JWT. 우리 앱이 서명을 검증한다

표 3. 소셜 로그인 흐름에서 오가는 네 가지

access_token과 id_token은 둘 다 로그인하면 받는 문자열이라 같은 것으로 보기 쉬운데, 2절의 두 질문에 그대로 대응한다. access_token은 “무엇을 할 수 있나”(인가) 를, id_token은 “누구인가”(인증) 를 말한다. 소셜 “로그인”의 본질은 id_token 쪽이다. 이 구분만 잡고 가도 문서 읽기가 한결 수월해진다.

id_token의 실체는 JWT 다. 점 두 개로 나뉜 긴 문자열인데, 서명이 붙어 있어서 받은 쪽이 위조 여부를 자체적으로 검증할 수 있다. 구조는 JWT 는 왜 내용이 다 보이는데도 위조가 안 되나 에 있다.

받은 토큰을 어디에 두느냐도 남는다. localStorage에 넣으면 JS로 꺼내 쓰기 편하지만 XSS로 스크립트가 한 번 실행되면 그대로 읽힌다. HttpOnly 쿠키에 넣으면 JS가 못 읽어서 XSS에는 강한데, 브라우저가 자동으로 붙이니 CSRF 대비를 따로 해야 한다. 어느 쪽이든 내주는 게 하나씩 있다.

8. 도입 비용과 한계

여기까지만 보면 소셜 로그인은 안 쓸 이유가 없어 보인다. 비밀번호를 저장 안 해도 되고, 로그인 화면을 만들 필요도 없고, 사용자는 클릭 한 번으로 가입한다.

대신 내주는 것들이 있다.

우리 서비스의 로그인이 남의 서비스 상태에 묶인다. 카카오 인증 서버가 멈추면 우리 사용자도 로그인을 못 한다. 우리 서버는 멀쩡한데도 그렇다.

제공자가 정책을 바꾸면 따라가야 한다. 받을 수 있는 정보의 범위(scope)나 심사 기준은 제공자가 정한다. 어제까지 이메일을 주던 게 다음 정책 개정에서 별도 심사 대상이 되면, 우리 쪽 회원 식별 방식을 통째로 고쳐야 할 수도 있다.

사용자가 그 계정을 잃으면 우리 서비스도 같이 잃는다. 카카오 계정이 정지되거나 탈퇴하면, 우리 쪽에 데이터는 그대로 있는데 들어올 방법이 없어진다. 그래서 실무에서는 이메일 같은 별도 확인 수단을 하나 더 걸어 두거나, 소셜 로그인과 자체 로그인을 같이 열어 두는 경우가 많다.

제공자에게 우리 사용자의 활동이 보인다. 최소한 “이 사람이 우리 서비스에 언제 로그인했는가”는 제공자 쪽 기록에 남는다.

그래서 우리는 무엇을 받아 이 사람이 맞다고 판단하나

우리가 받는 건 비밀번호가 아니라 카카오가 확인해 준 증거다. “이 사람 맞다”는 신원 증명(id_token)과 “여기까지 해도 된다”는 제한된 열쇠(access_token)만 받는다.

그 증거를 안전하게 건네받으려고 한 단계를 더 둔 게 Authorization Code 다. 브라우저 주소창으로는 일회용 code만 오가고, 진짜 토큰은 서버끼리 교환한다. 여기에 state로 위조된 콜백을 막고, PKCE로 code가로채기를 막고, redirect_uri 정확 일치로 코드가 엉뚱한 주소로 새는 걸 막는다.

함께 읽기

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다