"구글로 로그인" 버튼을 누르면 내 비밀번호를 넘기지 않고도 어떻게 로그인이 될까? — 위임 인가(delegated authorization)의 핵심 프로토콜
한 줄 요약
OAuth 2.0은 비밀번호를 공유하지 않고, 제3의 앱에게 "내 계정의 일부 권한만" 잠깐 빌려주는 표준이다.
사용자는 신뢰하는 인가 서버(구글 등)에서만 로그인하고, 앱은 그 대가로 짧은 수명의 액세스 토큰을 받는다.
1. 등장 인물 4명
Resource Owner사용자 (나)보호된 자원(내 프로필, 사진)의 주인. 권한을 위임할지 결정한다.
Client제3자 앱내 자원을 쓰고 싶어하는 서비스. 예: 사진 인쇄 사이트가 구글 포토에 접근.
Authorization Server인가 서버사용자를 인증하고 동의를 받아 인가 코드와 토큰을 발급. 예: accounts.google.com
Resource Server자원 서버실제 API. 액세스 토큰을 검사해 데이터를 내준다. 예: Google Photos API
2. 인가 코드 흐름 — 단계별 시뮬레이터
아래 "다음 단계" 버튼을 눌러 브라우저 리다이렉트가 어떻게 이어지는지 따라가 보자.
파란 테두리는 지금 요청을 받는 쪽이다.
사용자 브라우저
Client 앱
인가 서버
자원 서버
STEP 0 / 7
사용자가 Client 앱에서 "구글로 로그인" 버튼을 누르기 직전. 시작하려면 "다음 단계"를 누르세요.
3. 왜 "코드"를 한 번 더 교환할까?
인가 서버는 토큰을 브라우저 주소창(리다이렉트 URL)에 바로 실어 보내지 않고,
일회용 인가 코드만 보낸다. 그 코드를 Client의 백엔드 서버가
자신의 client_secret과 함께 뒤에서(back-channel) 토큰으로 바꾼다.
주소창·브라우저 히스토리·리퍼러 로그에 토큰이 남지 않는다.
코드는 수명이 짧고(보통 10분 이내) 1회만 사용 가능하다.
토큰 교환에는 client_secret이 필요해, 코드를 훔쳐도 공격자가 쓰기 어렵다.
PKCE (Proof Key for Code Exchange) — 모바일 앱·SPA처럼 client_secret을 숨길 수 없는
"공개 클라이언트"는 대신 매 요청마다 임의의 code_verifier를 만들고 그 해시(code_challenge)를
1단계에 보낸다. 토큰 교환 때 원본 code_verifier를 제시해야만 하므로, 코드를 가로채도 소용이 없다.
오늘날 PKCE는 모든 클라이언트에 권장된다.
# PKCE 값 생성 (브라우저에서)
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"# 43~128자 랜덤
code_challenge = BASE64URL( SHA256(code_verifier) )
= "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"# 1단계 인가 요청에 code_challenge 를 실어 보냄# 5단계 토큰 교환에 code_verifier 원본을 제시 → 서버가 해시 비교
4. 실제 HTTP 요청/응답
1단계 — 인가 요청 (브라우저 리다이렉트)
GET https://accounts.google.com/o/oauth2/v2/auth?
response_type=code
&client_id=1234.apps.googleusercontent.com
&redirect_uri=https://printphotos.com/callback
&scope=openid%20profile%20photos.readonly
&state=xyz-CSRF-랜덤값
&code_challenge=E9Melhoa2Owv...w-cM
&code_challenge_method=S256
5단계 — 코드를 토큰으로 교환 (Client 백엔드 → 인가 서버)
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=4/0AeaYSH... (일회용)
&redirect_uri=https://printphotos.com/callback
&client_id=1234.apps.googleusercontent.com
&client_secret=GOCSPX-••••••••
&code_verifier=dBjftJeZ4CVP-mB92K27uh...── 응답 ──
{
"access_token": "ya29.a0Af...", // 자원 서버에 제시, 수명 ~1시간"expires_in": 3600,
"refresh_token": "1//0gEo...", // 만료 시 새 토큰 발급용, 장수명"scope": "openid profile photos.readonly",
"token_type": "Bearer"
}
6단계 — API 호출 (Client → 자원 서버)
GET https://photoslibrary.googleapis.com/v1/albums
Authorization: Bearer ya29.a0Af...
5. 토큰의 두 종류
액세스 토큰 (Access Token)
리프레시 토큰 (Refresh Token)
용도
API를 부를 때마다 제시
액세스 토큰이 만료되면 새로 발급
수명
짧다 (수 분~1시간)
길다 (수일~무기한, 철회 전까지)
노출 위험
자주 오가지만 금방 만료
거의 안 움직이고 서버에 안전 보관
탈취 시
피해가 만료까지로 제한
치명적 → 반드시 서버 측 저장·회전
6. 흔한 오해 정리
오해 OAuth는 로그인(인증) 프로토콜이다 →
사실 OAuth 2.0은 인가(authorization)용. "로그인"은 그 위에 얹은
OpenID Connect가 id_token(JWT)으로 담당한다. → JWT 학습
오해state 파라미터는 선택이다 →
사실 콜백이 진짜 내가 시작한 요청인지 확인하는 CSRF 방어. 필수. → CSRF 학습
오해 Implicit Flow(토큰을 바로 받기)를 쓰면 된다 →
사실 2.1 초안에서 폐기. 지금은 SPA도 PKCE + 인가 코드 흐름을 쓴다.
오해scope는 형식적이다 →
사실최소 권한 원칙. 사진 읽기만 필요하면 쓰기·삭제 scope를 요구하지 않는다.
핵심 정리
사용자는 신뢰하는 인가 서버에서만 비밀번호를 입력한다. Client는 비밀번호를 절대 못 본다.
브라우저(front-channel)로는 일회용 코드만, 서버(back-channel)로는 비밀과 함께 토큰을 교환.