소셜 로그인 마지막 단계에서의 리디렉션
README#
본 문서는 운영진이 여러분의 코드를 리뷰할 때 요청해주신 피드백 타입을 중점적으로 확인할 수 있도록 구체적인 피드백 요구사항을 작성하는 곳입니다.
피드백 타입 (하나 이상의 타입을 선택해주세요)#
- [ ] 요구사항 준수여부
- [ ] 추가기능에 대한 피드백
- [ ] 코드 가독성 및 올바른 주석
- [ ] 버그 문의시 아래에 구체적인 문의사항을 작성해 주세요
- [x] 토론
달성하려는 목표#
백엔드 redirect uri로 로그인 완료시 단순 JSON을 넘겨주었더니 웹 브라우저에서 JSON이 뜹니다. 프론트엔드가 정의한 라우트로 이동과 동시에 장고 서버의 액세스 토큰과 리프레시 토큰을 쿼리 파라메터가 아닌 방식으로 전달하고 싶습니다.
내가 작성한 것#
코드블럭으로 제공해주셔도 좋고, GitHub 파일 혹은 line URL을 첨부하셔도 됩니다.
-
프론트엔드 입장에서 느낀 문제점
-
프론트엔드에서 Authorization Code를 처리할 때
-
유저가 로그인 버튼을 클릭하면 직접 카카오 로그인 동의 화면을 호출합니다.
kakaoLoginButton.onclick = () => { location.href = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${kakaoClientId}&redirect_uri=${redirectURI}` };
-
이후 프론트엔드에서 설정한 redirect uri로 Authorization Code를 담은 쿼리 스트링이 전달되어 옵니다.
-
프론트엔드는 쿼리 스트링에서 Authorization Code를 가져와서 백엔드 서버에 전달하고 이후 과정은 잘 아실테니 생략하겠습니다.
-
-
백엔드에서 Authorization Code를 처리할 때
-
유저가 로그인 버튼을 클릭하면 백엔드 엔드포인트로 화면을 호출합니다.
-
여기서 프론트엔드는 흐름이 끊깁니다. (get요청이 끝나는걸로 알고있습니다)
kakaoLoginButton.onclick = () => { location.href = '<https://www.example.com/api/kakao/login>' };
-
이후 유저가 로그인 계정을 선택하면 백엔드에서 Authorization Code를 받고 Access Token도 받고 로그인 처리 후 프론트엔드한테 jwt 토큰을 넘겨줍니다.
-
여기서 유저가 로그인이 완료된 순간인데 웹브라우저에 백엔드에서 JSON으로 넘겨준 토큰 정보가 그대로 노출됩니다.
-
-
저는 지난 프로젝트때 이 문제점을 발견했고, 해결 방법을 찾지 못해 프론트엔드에서 설정한 라우트로 쿼리 파라미터로 토큰을 전달받았습니다. (보안에 취약한걸로 알고있습니다)
- 백엔드에서 Authorization Code를 처리하는 과정으로 진행했을때, 프론트엔드에서 토큰을 쿼리 파라미터가 아닌 다른 방식으로 받을 수 있는지 알고싶습니다.
-
오류 메시지#
- 로그인이 완료되는 순간 웹브라우저에 토큰이 노출되는 모습입니다.
FIG 1.
답변#
https://developers.kakao.com/docs/latest/ko/kakaologin/common#link-and-signup 의 연결과 서비스 가입 항목을 보면 사용자와 앱의 연결 과정이 순서도로 나타나 있습니다.
FIG 2.
1 ~ 7까지는 제가 지난 특강 (유튜브 링크)를 통해 진행방법을 알려드렸으나, 8번과 관련한 내용은 제공해드리지 않았습니다.
8. 로그인된 서비스 제공#
서비스 서버는 카카오로부터 사용자 정보를 식별하여 가입 및 로그인 처리 (7번 참고)까지 완료한 상태입니다. 따라서 서비스 서버는 User 객체를 가지고 있을 것이고, User 정보를 다시 사용자에게 건네주어야 합니다. 사용자 인가 및 특정 기능에 대한 권한부여를 위하여 서비스 서버는 세션 방식의 인가 또는 토큰 방식의 인가를 사용할 것입니다. (참고: https://www.youtube.com/watch?v=UBUNrFtufWo) 이번에는 토큰 방식의 인가에 대해서만 설명하겠습니다.
토큰 방식의 인가
토큰방식은 토큰을 만든 자(예: Django 서버) 가 비밀 키를 사용하여 토큰 페이로드(데이터)를 암호화하여 유저를 식별하는 방식으로, 유저는 마치 놀이동산의 팔찌를 착용한 것과 같이 놀이기구를 타기 전 직원에게 팔찌를 보여주기만 하면 되는 방식을 의미합니다. 따라서 서비스 서버는 로그인 유저정보를 따로 저장할 필요가 없어 성능이 높아집니다. 대표적으로 JWT 토큰을 사용하며, 아래와 같이 두개의 마침표 .
로 세 파트가 구분되어있는 모습을 볼 수 있습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
서비스 서버는 대표적으로 jwt
라는 패키지 또는 simple-jwt
라는 패키지를 사용하여 아주 쉽게 토큰을 생성하고 검증할 수 있습니다. 따라서 7번단계인 “가입 및 로그인 처리”까지 완료된 상황에서 서비스 서버는 User 객체 중에서 민감하지 않은 정보만 페이로드에 담아서 JWT 토큰을 만들어낼 수 있습니다.
def kakao_callback(request):
...
user = User.objects.get_or_create(...)
payload = {
'user_id': user['id'],
'username': user['username'],
'email': user['email'],
'exp': datetime.utcnow() + timedelta(days=1),
'iat': datetime.utcnow(),
}
jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
...
하지만 이때 브라우저의 상태는 소셜 로그인 페이지에서 멈춰있기 때문에 JSON 타입으로 응답할 경우, 브라우저에 JSON 데이터가 그대로 노출이 되는 문제가 발생합니다. (FIG 1. 참조) 이 문제는 서비스 서버가 사용자 페이지로 리다이렉트 하는 것으로 해결할 수 있습니다. 이때 생성한 JWT 토큰은 HTTP 헤더에 담아서 보낼 수 있습니다. 주로 Set-Cookie 헤더를 통하여 삽입할 수 있습니다.
def kakao_callback(request):
...
jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
# Set the JWT token in a cookie
response = redirect('your_frontend_url') # Replace with your frontend URL
response.set_cookie(
'jwt_token',
jwt_token,
httponly=True,
secure=True,
samesite='Lax'
)
return response
...
아래는 소셜 로그인 콜백 핸들러 예시의 전문입니다.
import requests
from django.shortcuts import redirect
from django.http import JsonResponse
from django.conf import settings
import jwt
from datetime import datetime, timedelta
def kakao_callback(request):
# Get the authorization code from the request
code = request.GET.get('code')
# Exchange the authorization code for an access token
token_url = "<https://kauth.kakao.com/oauth/token>"
payload = {
'grant_type': 'authorization_code',
'client_id': settings.KAKAO_CLIENT_ID,
'redirect_uri': settings.KAKAO_REDIRECT_URI,
'code': code,
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.post(token_url, data=payload, headers=headers)
token_json = response.json()
access_token = token_json.get('access_token')
# Use the access token to get user information from Kakao
user_info_url = "<https://kapi.kakao.com/v2/user/me>"
headers = {
'Authorization': f'Bearer {access_token}',
}
response = requests.get(user_info_url, headers=headers)
user_info = response.json()
# Create a JWT token for your application
payload = {
'user_id': user_info['id'],
'exp': datetime.utcnow() + timedelta(days=1),
'iat': datetime.utcnow(),
}
jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
# Set the JWT token in a cookie
response = redirect('your_frontend_url') # Replace with your frontend URL
response.set_cookie('jwt_token', jwt_token, httponly=True, secure=True, samesite='Lax')
return response
Sequence Diagram#
- 백엔드 서버로부터 소셜 로그인 URL을 GET 요청으로 받아옵니다. 백엔드 서버는 client_id와 redirect_uri와 같은 query params과 함께 302 redirect 응답을 반환합니다. 이때 redirect_uri는 백엔드의 API 입니다.
- 클라이언트는 소셜 로그인 페이지로 이동하여 로그인을 진행합니다.
- 로그인이 성공할 경우, OAuth 서버는 백엔드의 redirect_uri로 인가코드와 함께 GET 요청을 보냅니다. 백엔드는 인가코드를 가지고 OAuth 서버로부터 토큰을 검증 및 유저 정보를 획득할 수 있습니다.
- 유저 정보를 획득한 백엔드는 데이터베이스에 유저 존재 여부를 검사할 수 있고, 유저가 없다면 회원가입을, 있다면 로그인 로직을 수행하면 됩니다.
- 마지막으로 백엔드는 프론트엔드가 정의한 위치로 302 redirect 응답을 반환하여 로그인 시에 끊어졌던 웹 브라우저의 상태를 복원합니다. 백엔드는 클라이언트에게 인가를 위한 JWT 토큰을 발급 할 수 있고, Set-Cookie 헤더에 토큰을 담아 클라이언트가 저장하도록 할 수 있습니다.
sequenceDiagram
participant Client
participant OAuth Server
participant Backend
participant Database
Client->>Backend: GET /social-login-url
Backend-->>Client: 302 Redirect with client_id and redirect_uri
Client->>OAuth Server: Access OAuth login page
OAuth Server->>Backend: GET /redirect_uri with Authorization Code
Backend->>OAuth Server: Validate Authorization Code and Request Tokens
OAuth Server-->>Backend: Return Tokens and User Info
Backend->>Database: Check if user exists
Database-->>Backend: Return User Status (Exists/Not Exists)
alt User Exists
Backend->>Backend: Perform Login Logic
else User Does Not Exist
Backend->>Backend: Perform Registration Logic
end
Backend-->>Client: 302 Redirect to frontend URL with JWT Token in Set-Cookie header