OAuth 2.0 — 제3자 인증의 원리
이 토픽을 마치면
OAuth 2.0이 해결하는 문제, Authorization Code Flow의 전체 흐름, 그리고 Access Token과 Refresh Token의 역할을 이해할 수 있습니다.
비밀번호를 왜 안 받나
"구글로 로그인", "카카오로 로그인" 버튼을 누르면 우리 서비스에 비밀번호를 입력하지 않습니다. 구글 화면으로 이동해서 구글에 로그인하고, 다시 돌아옵니다.
왜 이렇게 복잡하게 만들었을까요? 간단합니다 — 비밀번호를 남의 서버에 넘기면 안 되기 때문입니다.
만약 어떤 서비스가 "구글 계정 이메일과 비밀번호를 입력하세요"라고 한다면, 그 서비스가 내 비밀번호를 저장할 수 있습니다. 유출되면 구글 계정이 통째로 뚫립니다. OAuth는 이 문제를 해결합니다 — 비밀번호 대신 "허가증"을 주는 것입니다.
4명의 등장인물
OAuth에는 역할이 4개 있습니다:
| 역할 | 의미 | 예시 |
|---|---|---|
| Resource Owner | 사용자 (데이터의 주인) | 나 |
| Client | 우리가 만든 서비스 | 우리 웹앱 |
| Authorization Server | 인증을 담당하는 서버 | 구글 인증 서버 |
| Resource Server | 사용자 데이터가 있는 서버 | 구글 프로필 API |
이름이 직관적이지 않습니다. "Client"가 사용자가 아니라 우리 서버라는 점을 주의하세요. OAuth 관점에서 우리 서비스가 구글에게 "데이터 좀 주세요"라고 요청하는 입장이라 Client입니다.
Authorization Code Flow
가장 많이 쓰이는 흐름입니다. 단계별로 보겠습니다:
1. 사용자가 "구글로 로그인" 클릭
2. 우리 서버가 구글 인증 서버로 리다이렉트
→ URL에 client_id, redirect_uri, scope 포함
3. 사용자가 구글에서 로그인 + "이 앱에 허용" 클릭
4. 구글이 redirect_uri로 "인가 코드(code)" 전달
5. 우리 서버가 인가 코드 + client_secret으로 구글에 요청
6. 구글이 Access Token 발급
7. 우리 서버가 Access Token으로 사용자 정보 요청핵심은 두 번 교환한다는 것입니다. 인가 코드(일회용)를 먼저 받고, 그걸로 진짜 토큰을 받습니다. 왜 한 번에 토큰을 안 줄까요?
4단계에서 인가 코드는 URL 파라미터로 전달됩니다. URL은 브라우저 히스토리에 남고, 로그에 찍힙니다. 만약 여기에 토큰이 들어있으면 누군가 볼 수 있습니다. 인가 코드는 일회용이고 짧은 시간 내에 만료되니까, 유출되어도 피해가 제한됩니다.
5단계에서 진짜 토큰 교환은 서버 간 통신(백채널)으로 일어납니다. client_secret이 포함되어 있고, 이건 절대 브라우저에 노출되지 않습니다.
Access Token과 Refresh Token
// 구글이 응답하는 토큰 구조 (예시)
{
"access_token": "ya29.a0AfH6SM...",
"expires_in": 3600, // 1시간
"refresh_token": "1//0eXyz...",
"token_type": "Bearer",
"scope": "email profile"
}| 토큰 | 수명 | 용도 |
|---|---|---|
| Access Token | 짧음 (보통 1시간) | API 호출에 사용 |
| Refresh Token | 김 (수주~수개월) | Access Token 재발급용 |
Access Token이 만료되면 Refresh Token으로 새 Access Token을 받습니다. 매번 사용자에게 "다시 로그인하세요"라고 하지 않아도 됩니다.
왜 Access Token을 길게 안 만들까요? 토큰이 유출되었을 때 피해 기간을 최소화하기 위해서입니다. 1시간짜리 토큰이 유출되면 1시간만 위험하지만, 1년짜리 토큰이 유출되면 1년간 위험합니다.
scope — 권한의 범위
https://accounts.google.com/o/oauth2/v2/auth?
client_id=OUR_CLIENT_ID&
redirect_uri=http://localhost:3000/callback&
scope=email+profile&
response_type=codescope는 어디까지 접근할 수 있는지 정하는 것입니다. email profile이면 이메일과 프로필 사진만 가져올 수 있고, 구글 드라이브 파일은 못 봅니다.
사용자가 "이 앱에 허용" 클릭할 때 보이는 화면에 "이 앱이 귀하의 이메일 주소를 확인합니다"라고 나오는 게 scope입니다.
최소 권한 원칙 — 필요한 것만 요청하세요. "모든 권한"을 요청하면 사용자가 겁먹고 거부합니다.
우리 서비스에서 구현할 때
실제 구현 시 알아야 할 것:
- Provider 등록: 구글/카카오 개발자 콘솔에서 앱을 등록하고
client_id와client_secret을 받습니다 - redirect_uri 설정: 인가 코드를 받을 콜백 URL을 등록합니다 — 등록되지 않은 URL로는 리다이렉트가 거부됩니다
- 상태값(state) 검증: CSRF 방지용 랜덤 문자열을 요청에 포함하고, 콜백에서 일치 여부를 확인합니다
// Express에서의 콜백 처리 (개념)
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// 1. state 검증 (CSRF 방지)
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state');
}
// 2. 인가 코드로 토큰 교환 (서버 → 구글, 백채널)
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
}),
});
const { access_token } = await tokenRes.json();
// 3. Access Token으로 사용자 정보 요청
const userRes = await fetch(
'https://www.googleapis.com/oauth2/v2/userinfo',
{ headers: { Authorization: `Bearer ${access_token}` } }
);
const user = await userRes.json();
// 4. 세션에 사용자 저장
req.session.user = user;
res.redirect('/');
});핵심
OAuth 2.0은 비밀번호 대신 토큰으로 권한을 위임하는 프로토콜입니다. Authorization Code Flow는 인가 코드(일회용) → Access Token(단기) → API 호출의 두 단계 교환입니다. Access Token은 짧게, Refresh Token으로 갱신 — 유출 시 피해를 최소화하는 설계입니다.
→ 다음 토픽에서 Passport.js + Google OAuth를 실제로 구현합니다.