스토리텔링 형식으로 업데이트되었습니다! 실무 사례와 함께 더 쉽게 이해할 수 있어요.
이미지 로딩 중...
AI Generated
2025. 11. 25. · 9 Views
OAuth 2.0과 소셜 로그인 완벽 가이드
OAuth 2.0의 핵심 개념부터 구글, 카카오 소셜 로그인 구현까지 초급 개발자를 위해 쉽게 설명합니다. 인증과 인가의 차이점, 다양한 Flow의 특징, 그리고 보안 고려사항까지 실무에 바로 적용할 수 있는 내용을 다룹니다.
목차
- OAuth 2.0이란?
- Authorization Code Flow
- Implicit Flow vs PKCE
- Access Token과 Scope
- 소셜 로그인 구현 (구글, 카카오)
- OAuth 보안 고려사항
1. OAuth 2.0이란?
김개발 씨는 새로운 웹 서비스를 만들고 있습니다. 회원가입 기능을 구현하려는데, 요즘 누가 아이디와 비밀번호를 일일이 만들어서 가입하나 싶었습니다.
"구글로 로그인", "카카오로 로그인" 버튼 하나면 끝나는 세상인데 말이죠.
OAuth 2.0은 한마디로 "대신 인증해주는 표준 프로토콜"입니다. 마치 호텔에서 신분증 대신 여권을 보여주면 신원이 확인되는 것처럼, 구글이나 카카오 같은 신뢰할 수 있는 서비스가 사용자의 신원을 대신 보증해주는 방식입니다.
이것을 제대로 이해하면 안전하고 편리한 로그인 시스템을 구축할 수 있습니다.
다음 코드를 살펴봅시다.
// OAuth 2.0의 기본 구조를 이해하기 위한 개념 코드
const oauthConfig = {
// 클라이언트 정보 (구글/카카오에서 발급받은 값)
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'https://yourapp.com/callback',
// 요청할 권한 범위
scope: ['profile', 'email'],
// 인증 서버 엔드포인트
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/auth',
tokenEndpoint: 'https://oauth2.googleapis.com/token'
};
김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 이번에 새로운 프로젝트를 맡게 되었는데, PM이 이렇게 말했습니다.
"요즘 회원가입 폼 길게 만들면 아무도 안 써요. 소셜 로그인으로 간단하게 가입할 수 있게 해주세요." 김개발 씨는 고개를 끄덕였지만, 사실 OAuth가 정확히 뭔지 잘 몰랐습니다.
그래서 선배 박시니어 씨에게 물어봤습니다. "선배님, OAuth가 정확히 뭔가요?" 박시니어 씨가 웃으며 설명을 시작했습니다.
"쉽게 비유하자면 말이야, 아파트 경비실을 생각해봐." 아파트에 처음 온 택배 기사가 있다고 가정해봅시다. 택배 기사는 주민의 집에 직접 들어갈 권한이 없습니다.
하지만 경비실에서 주민에게 전화를 걸어 "택배 기사분이 오셨는데, 들여보내도 될까요?"라고 물어봅니다. 주민이 "네, 들여보내주세요"라고 허락하면, 경비실에서 택배 기사에게 임시 출입증을 발급해줍니다.
OAuth 2.0도 이와 똑같은 원리입니다. 여러분의 서비스(택배 기사)가 사용자의 정보(아파트)에 접근하려면, 구글이나 카카오(경비실)를 통해 사용자(주민)의 허락을 받아야 합니다.
허락을 받으면 Access Token(임시 출입증)을 발급받아 필요한 정보에 접근할 수 있게 됩니다. 그런데 왜 이런 복잡한 방식이 필요한 걸까요?
예전에는 어떤 서비스에서 다른 서비스의 정보를 가져오려면, 사용자에게 직접 아이디와 비밀번호를 물어봐야 했습니다. "구글 계정 정보를 입력해주세요"라고 말이죠.
하지만 이건 정말 위험한 방식입니다. 사용자의 비밀번호가 제3자 서비스에 그대로 노출되기 때문입니다.
OAuth 2.0은 이 문제를 깔끔하게 해결합니다. 사용자는 구글이나 카카오의 공식 로그인 페이지에서만 비밀번호를 입력합니다.
여러분의 서비스는 비밀번호를 절대 볼 수 없습니다. 대신 "이 사용자가 허락했다"는 증거인 토큰만 받게 됩니다.
OAuth 2.0에는 네 가지 핵심 역할이 있습니다. Resource Owner는 실제 사용자, 즉 데이터의 주인입니다.
Client는 여러분이 만드는 서비스입니다. Authorization Server는 구글이나 카카오의 인증 서버입니다.
Resource Server는 실제 사용자 정보가 저장된 서버입니다. 김개발 씨가 고개를 끄덕였습니다.
"아, 그래서 구글 로그인을 누르면 구글 페이지로 이동하는 거군요!" 박시니어 씨가 맞다며 덧붙였습니다. "그리고 중요한 건, OAuth는 인증이 아니라 인가를 위한 프로토콜이야.
인증은 '너 누구야?'를 확인하는 거고, 인가는 '뭘 할 수 있어?'를 허락하는 거지." 이 차이를 이해하는 것이 중요합니다. OAuth 2.0은 본래 "이 앱이 내 구글 드라이브에 접근해도 될까요?"와 같은 권한 위임을 위해 설계되었습니다.
하지만 워낙 널리 쓰이다 보니 로그인 용도로도 많이 사용하게 되었고, 이를 위해 OpenID Connect라는 확장 표준이 만들어졌습니다.
실전 팁
💡 - OAuth 2.0은 인가 프로토콜이지만, OpenID Connect와 함께 인증에도 사용됩니다
- 절대 사용자의 비밀번호를 직접 받아서 저장하지 마세요
2. Authorization Code Flow
김개발 씨는 OAuth 2.0의 개념을 이해했습니다. 이제 실제로 구현을 시작하려는데, "Authorization Code Flow"라는 용어가 나왔습니다.
Flow가 여러 가지라니, 어떤 걸 써야 하는 걸까요?
Authorization Code Flow는 OAuth 2.0에서 가장 안전하고 널리 사용되는 인증 방식입니다. 마치 은행에서 수표를 발행받아 현금으로 교환하는 것처럼, 먼저 인가 코드(수표)를 받고, 이를 액세스 토큰(현금)으로 교환하는 2단계 과정을 거칩니다.
서버 사이드 애플리케이션에서 권장되는 방식입니다.
다음 코드를 살펴봅시다.
// 1단계: 사용자를 인증 페이지로 리다이렉트
const authUrl = new URL('https://accounts.google.com/o/oauth2/auth');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('response_type', 'code'); // 핵심: code를 요청
authUrl.searchParams.set('scope', 'profile email');
authUrl.searchParams.set('state', generateRandomState()); // CSRF 방지
// 2단계: 콜백에서 code를 받아 토큰으로 교환
async function handleCallback(code) {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code: code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // 서버에서만 사용
redirect_uri: 'https://myapp.com/callback',
grant_type: 'authorization_code'
})
});
return response.json(); // { access_token, refresh_token, ... }
}
박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "OAuth 2.0에는 여러 가지 Flow가 있는데, 가장 기본이 되는 건 Authorization Code Flow야.
이걸 확실히 이해하면 나머지도 쉬워." Authorization Code Flow의 전체 과정을 영화 티켓 예매에 비유해봅시다. 여러분이 영화관 앱에서 "카카오페이로 결제"를 누른다고 가정해봅시다.
앱은 여러분을 카카오페이 페이지로 보냅니다. 카카오페이에서 로그인하고 결제를 승인하면, 카카오페이는 "결제 승인 번호"를 영화관 앱에 전달합니다.
영화관 앱은 이 승인 번호를 가지고 카카오페이 서버에 "정말 승인된 거 맞아?"라고 확인한 뒤, 최종적으로 결제를 완료합니다. OAuth의 Authorization Code Flow도 정확히 이 과정을 따릅니다.
첫 번째 단계에서는 사용자를 인증 서버로 보냅니다. 사용자가 "구글로 로그인" 버튼을 클릭하면, 여러분의 서비스는 사용자를 구글의 로그인 페이지로 리다이렉트합니다.
이때 중요한 정보들을 URL 파라미터로 함께 보냅니다. client_id는 "나는 이런 앱이야"라고 알려주는 것이고, redirect_uri는 "로그인 끝나면 여기로 보내줘"라고 알려주는 것입니다.
사용자는 구글 페이지에서 로그인하고, "이 앱이 내 정보에 접근해도 될까요?"라는 동의 화면을 봅니다. 사용자가 "허용"을 누르면 두 번째 단계로 넘어갑니다.
두 번째 단계에서 구글은 사용자를 다시 여러분의 서비스로 돌려보냅니다. 이때 URL에 Authorization Code가 포함됩니다.
예를 들어 https://myapp.com/callback?code=abc123xyz와 같은 형태입니다. 이 코드가 바로 "수표"입니다.
세 번째 단계에서 여러분의 서버가 이 코드를 가지고 구글 서버에 직접 요청을 보냅니다. "이 코드 진짜야, 토큰으로 바꿔줘"라고 요청하는 것입니다.
이때 client_secret이 함께 전송됩니다. 이 과정은 서버와 서버 사이에서 일어나기 때문에 사용자의 브라우저에서는 보이지 않습니다.
김개발 씨가 질문했습니다. "왜 바로 토큰을 안 주고 코드를 먼저 주는 거예요?
한 단계가 더 있으니까 복잡하잖아요." 박시니어 씨가 핵심을 짚었습니다. "바로 그게 보안의 핵심이야.
Authorization Code는 URL에 노출되지만, 그 자체로는 아무것도 못 해. 토큰으로 교환하려면 client_secret이 필요한데, 이건 서버에만 저장되어 있거든." 만약 토큰이 URL에 바로 노출된다면 어떻게 될까요?
브라우저 히스토리에 남을 수도 있고, 로그에 기록될 수도 있습니다. 중간에 누군가 가로챌 수도 있습니다.
하지만 코드만 노출되고, 토큰 교환은 서버에서 안전하게 이루어지기 때문에 이런 위험을 방지할 수 있습니다. 또 하나 중요한 것은 state 파라미터입니다.
이것은 CSRF 공격을 방지하기 위한 것입니다. 요청을 보낼 때 랜덤한 값을 생성해서 보내고, 콜백에서 돌아온 state 값이 일치하는지 확인합니다.
일치하지 않으면 누군가 악의적으로 조작한 요청일 수 있습니다. 김개발 씨는 이제 왜 이 Flow가 가장 안전한지 이해했습니다.
"결국 민감한 정보는 서버에서만 다루고, 브라우저에는 일시적인 코드만 노출되는 거군요!"
실전 팁
💡 - state 파라미터는 반드시 사용하세요. CSRF 공격 방지에 필수입니다
- client_secret은 절대 프론트엔드 코드에 포함하면 안 됩니다
3. Implicit Flow vs PKCE
김개발 씨가 SPA(Single Page Application)를 개발하게 되었습니다. 서버 없이 프론트엔드만으로 구성된 앱인데, Authorization Code Flow를 쓰려니 client_secret을 저장할 곳이 없습니다.
이럴 때는 어떻게 해야 할까요?
Implicit Flow는 서버 없이 브라우저에서 직접 토큰을 받는 방식으로, 과거 SPA에서 사용되었습니다. 하지만 보안 취약점 때문에 현재는 PKCE(Proof Key for Code Exchange)가 권장됩니다.
PKCE는 동적으로 생성한 검증 코드를 사용하여 client_secret 없이도 안전한 인증을 가능하게 합니다.
다음 코드를 살펴봅시다.
// PKCE Flow 구현
// 1단계: code_verifier와 code_challenge 생성
function generatePKCE() {
// 랜덤한 code_verifier 생성 (43-128자)
const verifier = base64URLEncode(crypto.randomBytes(32));
// code_challenge = SHA256(code_verifier)를 base64url 인코딩
const challenge = base64URLEncode(sha256(verifier));
return { verifier, challenge };
}
// 2단계: 인증 요청시 code_challenge 포함
const { verifier, challenge } = generatePKCE();
sessionStorage.setItem('code_verifier', verifier); // 나중에 사용
const authUrl = `https://accounts.google.com/o/oauth2/auth?
client_id=${CLIENT_ID}&
redirect_uri=${REDIRECT_URI}&
response_type=code&
code_challenge=${challenge}&
code_challenge_method=S256`;
// 3단계: 토큰 교환시 code_verifier 포함 (client_secret 대신)
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code: authCode,
client_id: CLIENT_ID,
code_verifier: sessionStorage.getItem('code_verifier'), // 핵심!
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
박시니어 씨가 화이트보드에 두 가지 Flow를 나란히 그렸습니다. "예전에는 Implicit Flow라는 게 있었어.
근데 지금은 쓰면 안 돼. 왜 그런지 설명해줄게." Implicit Flow는 Authorization Code Flow의 "간소화 버전"이었습니다.
코드를 받아서 토큰으로 교환하는 과정 없이, 인증 서버가 바로 토큰을 URL fragment(#)에 담아서 보내주는 방식이었습니다. 예를 들어 https://myapp.com/callback#access_token=xyz123처럼 말이죠.
서버가 없는 SPA에서는 client_secret을 안전하게 보관할 방법이 없었기 때문에, 이 방식이 유일한 선택지였습니다. 코드 교환 과정 자체를 생략하면 client_secret이 필요 없으니까요.
하지만 문제가 있었습니다. 토큰이 URL에 직접 노출된다는 것입니다.
URL fragment는 서버로 전송되지 않기 때문에 서버 로그에는 남지 않습니다. 하지만 브라우저 히스토리에 남을 수 있고, Referer 헤더를 통해 다른 사이트로 유출될 수도 있습니다.
무엇보다 Access Token이 탈취당하면 바로 악용될 수 있습니다. 2019년, OAuth 2.0 보안 모범 사례 문서에서 공식적으로 Implicit Flow 사용을 금지했습니다.
대신 등장한 것이 PKCE입니다. PKCE는 "픽시"라고 읽습니다.
Proof Key for Code Exchange의 약자입니다. 이름이 어려워 보이지만, 개념은 간단합니다.
수수께끼 놀이를 생각해봅시다. 제가 "사과"라는 답을 정하고, 그 힌트로 "빨간 과일"이라고 말합니다.
나중에 제가 "사과"라고 말하면, 상대방은 "아, 빨간 과일이라고 했던 그 사람이 맞구나"라고 확인할 수 있습니다. PKCE도 똑같습니다.
처음에 code_verifier라는 랜덤한 문자열을 만듭니다. 이것을 SHA256으로 해시해서 code_challenge를 만듭니다.
인증 요청을 보낼 때 code_challenge를 함께 보냅니다. 나중에 코드를 토큰으로 교환할 때, code_verifier를 함께 보냅니다.
인증 서버는 받은 code_verifier를 해시해서 처음에 받았던 code_challenge와 비교합니다. 일치하면 "처음에 요청한 그 앱이 맞구나"라고 확인하고 토큰을 발급합니다.
김개발 씨가 감탄했습니다. "아, 그러니까 client_secret 대신 매번 새로운 일회용 비밀번호를 만들어 쓰는 거네요!" 박시니어 씨가 고개를 끄덕였습니다.
"맞아. 그리고 code_verifier는 브라우저의 메모리나 sessionStorage에만 저장되니까, 코드가 탈취당해도 code_verifier 없이는 토큰을 받을 수 없어." PKCE의 또 다른 장점은 모바일 앱에서도 사용할 수 있다는 것입니다.
모바일 앱도 서버가 없는 경우가 많은데, PKCE를 사용하면 안전하게 OAuth를 구현할 수 있습니다. 현재 대부분의 OAuth 제공자(구글, 카카오, 네이버 등)가 PKCE를 지원합니다.
SPA나 모바일 앱을 개발한다면 반드시 PKCE를 사용하세요.
실전 팁
💡 - Implicit Flow는 절대 사용하지 마세요. 보안 취약점이 있습니다
- SPA/모바일 앱에서는 PKCE + Authorization Code Flow를 사용하세요
4. Access Token과 Scope
김개발 씨가 드디어 토큰을 받았습니다. 그런데 응답을 보니 access_token 외에도 refresh_token, expires_in, scope 같은 값들이 있습니다.
이것들은 각각 무슨 역할을 하는 걸까요?
Access Token은 API에 접근할 수 있는 열쇠이고, Scope는 그 열쇠로 열 수 있는 문의 범위를 정합니다. Access Token에는 유효기간이 있어서 만료되면 Refresh Token으로 새로운 Access Token을 발급받습니다.
이런 구조 덕분에 보안을 유지하면서도 편리한 사용자 경험을 제공할 수 있습니다.
다음 코드를 살펴봅시다.
// 토큰 응답 예시
const tokenResponse = {
access_token: "ya29.a0AfH6SMBx...", // API 호출용 토큰
refresh_token: "1//0eXx7W2v8...", // 재발급용 토큰
expires_in: 3600, // 만료 시간 (초)
token_type: "Bearer", // 토큰 타입
scope: "profile email" // 허용된 권한 범위
};
// Access Token으로 API 호출
async function getUserProfile(accessToken) {
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}` // 핵심: Bearer 토큰
}
});
return response.json();
}
// Refresh Token으로 새 Access Token 발급
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: 'refresh_token' // 핵심: grant_type 변경
})
});
return response.json();
}
박시니어 씨가 김개발 씨에게 회사 출입증을 보여주며 말했습니다. "이 출입증으로 비유해볼게.
토큰 시스템을 이해하는 데 딱이야." 회사 출입증에는 여러 정보가 담겨 있습니다. 이름, 부서, 그리고 접근 가능한 구역이 표시되어 있습니다.
일반 직원은 사무실만 들어갈 수 있고, 특정 권한이 있는 사람만 서버실에 들어갈 수 있습니다. 그리고 출입증에는 유효기간이 있어서 주기적으로 갱신해야 합니다.
Access Token은 바로 이 출입증과 같습니다. API를 호출할 때 "나는 이런 권한이 있는 사람이야"라고 증명하는 용도입니다.
요청 헤더에 Authorization: Bearer {토큰}을 넣어서 보내면 됩니다. Scope는 출입증에 표시된 "접근 가능한 구역"과 같습니다.
profile scope가 있으면 사용자 프로필 정보를 가져올 수 있고, email scope가 있으면 이메일을 가져올 수 있습니다. https://www.googleapis.com/auth/drive.readonly 같은 scope는 구글 드라이브를 읽기 전용으로 접근할 수 있는 권한입니다.
김개발 씨가 질문했습니다. "그런데 왜 Access Token의 유효기간이 1시간 정도로 짧은 거예요?
불편하지 않나요?" 박시니어 씨가 설명했습니다. "보안 때문이야.
만약 Access Token이 탈취당하면 어떻게 될까?" Access Token이 영구적이라면, 한 번 탈취당하면 영원히 악용될 수 있습니다. 하지만 유효기간이 1시간이라면, 최악의 경우에도 피해는 1시간으로 제한됩니다.
그런데 1시간마다 사용자가 다시 로그인해야 한다면 너무 불편하겠죠? 그래서 Refresh Token이 있습니다.
Refresh Token은 Access Token을 재발급받기 위한 특별한 토큰입니다. Access Token이 만료되면, Refresh Token을 사용해서 사용자의 개입 없이 새로운 Access Token을 받을 수 있습니다.
Refresh Token은 훨씬 긴 유효기간을 가집니다. 몇 주에서 몇 달까지도 유효합니다.
하지만 그만큼 더 안전하게 보관해야 합니다. 보통 서버 측에서만 관리하고, 절대 브라우저에 노출되어서는 안 됩니다.
김개발 씨가 이해했습니다. "아, 그러니까 Access Token은 자주 바뀌는 임시 출입증이고, Refresh Token은 새 출입증을 발급받을 수 있는 마스터 키 같은 거네요!" 한 가지 더 중요한 개념이 있습니다.
최소 권한의 원칙입니다. Scope를 요청할 때는 꼭 필요한 것만 요청해야 합니다.
소셜 로그인만 구현할 건데 "구글 드라이브 전체 접근 권한"을 요청하면 사용자가 거부감을 느낄 것입니다. 그리고 실제로 그런 광범위한 권한은 구글의 추가 심사 대상이 됩니다.
보통 소셜 로그인에는 profile과 email scope면 충분합니다. 사용자의 이름, 프로필 사진, 이메일 주소만 가져오면 회원가입 처리가 가능하니까요.
실전 팁
💡 - Access Token은 짧은 유효기간으로 설정하고, Refresh Token으로 갱신하세요
- Scope는 꼭 필요한 것만 요청하세요. 과도한 권한 요청은 사용자 이탈의 원인이 됩니다
5. 소셜 로그인 구현 (구글, 카카오)
이론은 충분합니다. 김개발 씨는 이제 실제로 구글과 카카오 소셜 로그인을 구현해보기로 했습니다.
각 서비스의 개발자 콘솔에서 앱을 등록하고, 실제 코드를 작성하는 과정을 함께 따라가봅시다.
실제 소셜 로그인 구현은 세 단계로 이루어집니다. 먼저 각 플랫폼의 개발자 콘솔에서 앱을 등록하고, 로그인 버튼을 클릭했을 때 인증 페이지로 리다이렉트하는 코드를 작성하고, 마지막으로 콜백 URL에서 토큰을 처리하는 로직을 구현합니다.
구글과 카카오는 세부 설정이 다르지만 전체 흐름은 동일합니다.
다음 코드를 살펴봅시다.
// Express.js 기반 소셜 로그인 구현 예시
const express = require('express');
const app = express();
// 구글 로그인 시작
app.get('/auth/google', (req, res) => {
const googleAuthUrl = `https://accounts.google.com/o/oauth2/auth?
client_id=${process.env.GOOGLE_CLIENT_ID}&
redirect_uri=${process.env.BASE_URL}/auth/google/callback&
response_type=code&
scope=profile email&
state=${generateState()}`;
res.redirect(googleAuthUrl);
});
// 구글 콜백 처리
app.get('/auth/google/callback', async (req, res) => {
const { code, state } = req.query;
// 토큰 교환
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code, client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${process.env.BASE_URL}/auth/google/callback`,
grant_type: 'authorization_code'
})
});
const { access_token } = await tokenRes.json();
// 사용자 정보 조회
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { 'Authorization': `Bearer ${access_token}` }
});
const user = await userRes.json(); // { id, email, name, picture }
// DB 저장 또는 세션 생성 로직...
});
// 카카오 로그인도 동일한 패턴 (엔드포인트만 다름)
// https://kauth.kakao.com/oauth/authorize
// https://kauth.kakao.com/oauth/token
김개발 씨가 드디어 코드를 작성하기 시작했습니다. 박시니어 씨가 옆에서 지켜보며 조언해주었습니다.
첫 번째 단계는 개발자 콘솔에서 앱 등록입니다. 구글의 경우 Google Cloud Console에 접속합니다.
새 프로젝트를 만들고, "API 및 서비스" 메뉴에서 "OAuth 동의 화면"을 설정합니다. 그 다음 "사용자 인증 정보"에서 OAuth 2.0 클라이언트 ID를 생성합니다.
이때 "승인된 리다이렉트 URI"에 여러분의 콜백 URL을 정확히 입력해야 합니다. 카카오의 경우 Kakao Developers에 접속합니다.
애플리케이션을 추가하고, "카카오 로그인"을 활성화합니다. "Redirect URI"를 설정하고, 필요한 동의 항목(닉네임, 프로필 사진, 이메일 등)을 선택합니다.
박시니어 씨가 중요한 점을 짚었습니다. "Redirect URI는 정확히 일치해야 해.
슬래시 하나라도 다르면 에러가 나." 두 번째 단계는 로그인 버튼 클릭 시 인증 페이지로 리다이렉트하는 것입니다. 사용자가 "구글로 로그인" 버튼을 클릭하면, 서버의 /auth/google 엔드포인트로 요청이 갑니다.
서버는 구글의 인증 URL을 만들어서 사용자를 리다이렉트합니다. 이 URL에는 client_id, redirect_uri, scope, state 등의 파라미터가 포함됩니다.
사용자는 구글의 로그인 페이지를 보게 됩니다. 로그인하고 권한에 동의하면, 구글은 사용자를 다시 여러분의 사이트로 돌려보냅니다.
세 번째 단계는 콜백 URL에서 토큰 처리입니다. 구글이 사용자를 /auth/google/callback?code=xxx&state=yyy로 리다이렉트합니다.
서버는 이 code를 받아서 구글 토큰 서버에 요청을 보내 access_token을 받습니다. 그 다음 access_token으로 구글 API를 호출해서 사용자 정보를 가져옵니다.
김개발 씨가 코드를 보며 물었습니다. "사용자 정보를 가져온 다음에는 어떻게 하나요?" 박시니어 씨가 답했습니다.
"두 가지 시나리오가 있어. 처음 로그인하는 사용자라면 DB에 새 계정을 만들고, 이미 있는 사용자라면 기존 계정과 연결하면 돼." 보통 구글이나 카카오에서 제공하는 고유 ID(sub 또는 id)를 기준으로 사용자를 식별합니다.
이메일은 바뀔 수 있지만, 고유 ID는 변하지 않기 때문입니다. 카카오 로그인도 거의 동일한 패턴입니다.
인증 URL이 https://kauth.kakao.com/oauth/authorize이고, 토큰 URL이 https://kauth.kakao.com/oauth/token이라는 점만 다릅니다. 사용자 정보 API는 https://kapi.kakao.com/v2/user/me입니다.
김개발 씨는 코드가 생각보다 간단해서 놀랐습니다. "이론이 복잡해 보였는데, 실제 코드는 정말 단순하네요!" 박시니어 씨가 웃으며 말했습니다.
"그게 OAuth의 장점이야. 복잡한 인증 로직은 구글이나 카카오가 다 처리해주니까, 우리는 토큰만 잘 받아서 쓰면 돼."
실전 팁
💡 - Redirect URI는 개발 환경과 운영 환경을 분리해서 등록하세요
- 환경 변수로 client_id와 client_secret을 관리하고, 절대 코드에 하드코딩하지 마세요
6. OAuth 보안 고려사항
김개발 씨의 소셜 로그인 기능이 잘 작동합니다. 하지만 박시니어 씨가 코드 리뷰를 하면서 몇 가지 보안 문제를 지적했습니다.
OAuth를 구현할 때 반드시 고려해야 할 보안 사항들이 있습니다.
OAuth 2.0은 잘 설계된 프로토콜이지만, 구현 실수로 인한 취약점이 발생할 수 있습니다. CSRF 공격 방지를 위한 state 파라미터, Redirect URI 검증, 토큰 안전한 저장, HTTPS 필수 사용 등 주요 보안 고려사항을 알아봅니다.
이것들을 놓치면 심각한 보안 사고로 이어질 수 있습니다.
다음 코드를 살펴봅시다.
// 보안을 고려한 OAuth 구현 예시
const crypto = require('crypto');
// 1. CSRF 방지: state 파라미터 검증
function generateState() {
const state = crypto.randomBytes(32).toString('hex');
// 세션에 저장하여 나중에 검증
req.session.oauthState = state;
return state;
}
function verifyState(receivedState, sessionState) {
if (!receivedState || receivedState !== sessionState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
}
// 2. 토큰 안전하게 저장 (httpOnly 쿠키 사용)
res.cookie('refresh_token', refreshToken, {
httpOnly: true, // JavaScript에서 접근 불가
secure: true, // HTTPS에서만 전송
sameSite: 'strict', // CSRF 추가 방어
maxAge: 7 * 24 * 60 * 60 * 1000 // 7일
});
// 3. Redirect URI 화이트리스트 검증
const ALLOWED_REDIRECT_URIS = [
'https://myapp.com/auth/callback',
'https://staging.myapp.com/auth/callback'
];
function validateRedirectUri(uri) {
if (!ALLOWED_REDIRECT_URIS.includes(uri)) {
throw new Error('Invalid redirect_uri');
}
}
// 4. Access Token은 메모리에만 저장 (localStorage 사용 금지)
// 프론트엔드에서는 변수로만 관리하고, 새로고침 시 재발급
박시니어 씨가 김개발 씨의 코드를 보며 빨간펜을 들었습니다. "기능은 잘 동작하는데, 보안적으로 몇 가지 문제가 있어." 첫 번째 문제는 CSRF 공격에 대한 방어입니다.
CSRF(Cross-Site Request Forgery)는 악의적인 사이트가 사용자 모르게 요청을 보내는 공격입니다. OAuth에서는 공격자가 자신의 계정으로 연결된 Authorization Code를 피해자에게 전달하는 방식으로 악용될 수 있습니다.
예를 들어, 공격자가 자신의 구글 계정으로 로그인하고, 받은 code가 포함된 콜백 URL을 피해자에게 보냅니다. 피해자가 그 링크를 클릭하면, 피해자의 계정에 공격자의 구글 계정이 연결됩니다.
이후 공격자는 자신의 구글 계정으로 피해자의 계정에 접근할 수 있게 됩니다. 이를 방지하는 것이 state 파라미터입니다.
인증 요청을 보낼 때 랜덤한 값을 생성해서 세션에 저장하고, 콜백에서 돌아온 state 값과 비교합니다. 다르면 공격으로 간주하고 요청을 거부합니다.
두 번째 문제는 토큰 저장 방식입니다. 김개발 씨가 물었습니다.
"localStorage에 토큰을 저장하면 안 되나요? 편한데요." 박시니어 씨가 고개를 저었습니다.
"XSS 공격에 취약해져. localStorage는 JavaScript로 접근할 수 있거든." XSS(Cross-Site Scripting) 공격이 성공하면, 공격자의 스크립트가 localStorage에 저장된 토큰을 읽어서 외부로 전송할 수 있습니다.
한 번 탈취된 토큰은 어디서든 악용될 수 있습니다. Refresh Token은 반드시 httpOnly 쿠키에 저장해야 합니다.
httpOnly 쿠키는 JavaScript에서 접근할 수 없기 때문에 XSS 공격으로부터 안전합니다. 추가로 secure 플래그를 설정하면 HTTPS에서만 쿠키가 전송되고, sameSite 플래그로 CSRF를 추가 방어할 수 있습니다.
Access Token은 가능하면 메모리에만 저장하는 것이 좋습니다. 변수로 관리하고, 페이지를 새로고침하면 Refresh Token으로 다시 발급받는 방식입니다.
세 번째 문제는 Redirect URI 검증입니다. OAuth 제공자(구글, 카카오)에서 등록된 Redirect URI만 허용하지만, 서버 측에서도 한 번 더 검증하는 것이 안전합니다.
특히 Redirect URI에 와일드카드를 사용하면 Open Redirect 취약점이 생길 수 있습니다. 네 번째는 당연하지만 HTTPS 필수 사용입니다.
OAuth 2.0은 HTTPS를 전제로 설계되었습니다. HTTP를 사용하면 토큰이 네트워크에서 평문으로 노출됩니다.
개발 환경에서는 localhost에서 HTTP를 허용하지만, 운영 환경에서는 반드시 HTTPS를 사용해야 합니다. 다섯 번째는 토큰 유효성 검증입니다.
프론트엔드에서 받은 토큰을 백엔드에서 그대로 신뢰하면 안 됩니다. 토큰이 위조되었을 수 있기 때문입니다.
구글이나 카카오의 토큰 검증 API를 호출하거나, JWT인 경우 서명을 검증해야 합니다. 김개발 씨가 한숨을 쉬었습니다.
"보안은 정말 신경 쓸 게 많네요." 박시니어 씨가 웃으며 말했습니다. "그래서 가능하면 검증된 라이브러리를 쓰는 게 좋아.
Passport.js나 NextAuth.js 같은 라이브러리는 이런 보안 고려사항들이 이미 구현되어 있거든." 보안은 한 번에 완벽하게 할 수 없습니다. 하지만 기본적인 원칙들을 지키면 대부분의 공격을 막을 수 있습니다.
OAuth를 구현할 때는 항상 "이 코드가 악용될 수 있는 방법이 없을까?"라고 자문해보세요.
실전 팁
💡 - state 파라미터는 반드시 사용하고 검증하세요
- Refresh Token은 httpOnly 쿠키에, Access Token은 메모리에 저장하세요
- 직접 구현보다는 검증된 라이브러리(Passport.js, NextAuth.js) 사용을 권장합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
WebSocket과 Server-Sent Events 실시간 통신 완벽 가이드
웹 애플리케이션에서 실시간 데이터 통신을 구현하는 핵심 기술인 WebSocket과 Server-Sent Events를 다룹니다. 채팅, 알림, 실시간 업데이트 등 현대 웹 서비스의 필수 기능을 구현하는 방법을 배워봅니다.
API 테스트 전략과 자동화 완벽 가이드
API 개발에서 필수적인 테스트 전략을 단계별로 알아봅니다. 단위 테스트부터 부하 테스트까지, 실무에서 바로 적용할 수 있는 자동화 기법을 익혀보세요.
효과적인 API 문서 작성법 완벽 가이드
API 문서는 개발자와 개발자 사이의 가장 중요한 소통 수단입니다. 이 가이드에서는 좋은 API 문서가 갖춰야 할 조건부터 Getting Started, 엔드포인트 설명, 에러 코드 문서화, 인증 가이드, 변경 이력 관리까지 체계적으로 배워봅니다.
API 캐싱과 성능 최적화 완벽 가이드
웹 서비스의 응답 속도를 획기적으로 개선하는 캐싱 전략과 성능 최적화 기법을 다룹니다. HTTP 캐싱부터 Redis, 데이터베이스 최적화, CDN까지 실무에서 바로 적용할 수 있는 핵심 기술을 초급자 눈높이에서 설명합니다.
JWT 기반 인증 구현하기 완벽 가이드
웹 애플리케이션에서 가장 널리 사용되는 JWT 인증 방식을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 토큰의 구조부터 보안 베스트 프랙티스까지 실무에 바로 적용할 수 있는 내용을 담았습니다.