OAuth2 Flow

김기현·2022년 3월 19일
0
post-thumbnail

OAuth

Flow

1. 준비

  • Client(YourHomeIsMine)는 OAuth를 제공하는 Resource Server(Kakao)의 개발자 콘솔에 서비스를 등록하고 client_id와 client_secret을 발급합니다. 그리고 redirect_url을 등록합니다.Client_id는 해당 프로젝트의 조건에 맞게 REST API 키를 사용하겠습니다.

2. Get Authorization Code

  • Client는 client_id와 redirect url, 그리고 권한 목록(이메일 등)의 정보들 등록해 로그인 버튼을 생성합니다.
  • User가 권한 승인과 더불어 로그인에 성공하면 Resource Server는 위에서 입력한 redirect url로 authorization code를 전달합니다.
    이때 authorization code는 사용자 정보에 접근할 수 없고, 사용자 정보에 접근할 수 있는 access_token을 획득하기 위해 사용합니다.

3. Get Access Token
Client는 정해진 시간(24시간 미만) 이내에 cliend_id, redirect url, authorization code와 client_secret을 포함한 요청을 Resource Server에 보내면 access_token을 획득할 수 있습니다.

프론트엔드에서 cliend_id와 redirect_url을 Resource Server(카카오 API)에 요청하고 백엔드에서 access_token을 획득합니다.

class KakaoCallback(View):
    def get(self, request):
        try:
        	# 카카오에서 토큰 발급
            kakao_token = request.headers.get("Authorization")
            
            if kakao_token == None:
                return JsonResponse({"message":"INVALID ACCESS TOKEN"}, status=401)
            
            profile_request = requests.get(
                "https://kapi.kakao.com/v2/user/me",
                headers = {"Authorization": f"Bearer {kakao_token}"},
                timeout = 2
            )
            
            # Client에서 access_token 발급
            access_token = jwt.encode(
                {'user_id' : user.id, 
                    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
                    }, SECRET_KEY, ALGORITHM)

4. Access 사용자 정보 및 Use API
Client는 access_token을 이용해 사용자가 권한을 허락한 정보에 접근합니다.

Front-End

프론트엔드는 위 과정 중 1~3번에 해당하는 작업을 진행해 access_token을 백엔드에 전달합니다.

Back-End

백엔드는 프론트엔드에서 전달받은 access_token을 이용해 Resource Server에 등록된 사용자의 정보(unique_index)를 가져옵니다.

위에서 언급한 unique index와 인증을 진행한 Resource Server의 종류(예 : kakao 등)의 종류를 user table에 저장해 사용자를 생성하고 식별합니다.

Use Kakao Login API

백엔드에서 Kakao Login API를 활용하는 코드는 다음과 같습니다.

import requests, jwt, datetime

from django.views     import View
from django.http      import JsonResponse

from .models     import User
from my_settings import SECRET_KEY, ALGORITHM

class KakaoCallback(View):
    def get(self, request):
        try:
        	# 카카오가 발행한 토큰을 받습니다.
            kakao_token = request.headers.get("Authorization")
            
            if kakao_token == None:
                return JsonResponse({"message":"INVALID ACCESS TOKEN"}, status=401)
            
            profile_request = requests.get(
                "https://kapi.kakao.com/v2/user/me",
                headers = {"Authorization": f"Bearer {kakao_token}"},
                timeout = 2
            )      
            
  			# Resource Server에서 User의 정보를 가져갑니다.
            profile_json  = profile_request.json()
            email         = profile_json.get("kakao_account").get("email", None)
            nickname      = profile_json.get("properties").get("nickname")
            profile_image = profile_json.get("kakao_account").get("profile").get("profile_image_url", None)
            gender        = profile_json.get("kakao_account").get("gender", None)
            kakao_id      = profile_json.get("id")
            
            if email is None:
                return JsonResponse({'message': 'EMAIL_REQUIRED'}, status = 405)
        
        	# 가입이 되어있지 않다면 인증된 유저의 정보를 바탕으로 가입합니다.
            user, is_created = User.objects.get_or_create(
                kakao_id      = kakao_id,
                defaults={
                'email'         : email,
                'nickname'      : nickname,
                'profile_image' : profile_image,
                'gender'        : gender
                }
            )
            user.save()
                
            # Client(나의 앱)에서 토큰을 발행 해 유저인지 인가합니다.
            access_token = jwt.encode(
                {'user_id' : user.id, 
                    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
                    }, SECRET_KEY, ALGORITHM)

            results = {
                'email'         : email,
                'nickname'      : nickname,
                'profile_image' : profile_image,
                'gender'        : gender,
                'kakao_id'      : kakao_id
                    }               
    
            return JsonResponse({
                    'message'   : 'SUCCESS',
                    'token'     : access_token,
                    'results'   : results
                }, status = 201)                                    

		# 유저가 정보를 허락하지 않았다면 get을 할 수 없어서 다음의 에러를 호출합니다.
        except AttributeError:
            return JsonResponse({'message' : 'CANNOT_GET_ATTRIBUTE'}, status = 400)
        
        #토큰이 만료될 시 다음의 에러를 호출합니다.
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message' : 'EXPIRED_TOKEN'}, status = 400)  

All Process in Backend

만약 백엔드에서 모든 과정(OAuth 서버로 API 사용)을 처리한다면 CORS 에러가 발생합니다.

All Process Code

다음은 백엔드에서 localhost/users/login/kakao를 요청한 후 localhost/users/login/kakao/callback을 받도록 프론트엔드가 해야할 과정을 백엔드에서 모두 처리한 로직입니다.
users/urls

# users/urls
from django.urls import path
from users.views import KakaoLogin, KakaoCallback

urlpatterns = [
    path("/login/kakao", KakaoLogin.as_view() ),
    path("/login/kakao/callback", KakaoCallback.as_view()),
    ]

users/views

# users.views
필요한 라이브러리 import
.
.
.
class KakaoLogin(View):
    def get(self, request):
        client_id      = CLIENT_ID
        redirect_uri   = "http://127.0.0.1:8000/users/login/kakao/callback"
        kakao_auth_api = "https://kauth.kakao.com/oauth/authorize?response_type=code"
        return redirect(
            f"{kakao_auth_api}&client_id={client_id}&redirect_uri={redirect_uri}"
        )


class KakaoCallback(View):
    def get(self, request):
        
        try:
            code            = request.GET.get("code")
            client_id       = CLIENT_ID
            redirect_uri    = "http://127.0.0.1:8000/users/login/kakao/callback"
            kakao_token_api = "https://kauth.kakao.com/oauth/token"
            .
            .
            .
   

Why Error...?

CORS를 사용하면 서로 다른 두 철처간의 리소스의 공유를 안전하게 허용할 수 있지만, 프론트에서 form을 통해 요청하는데 form은 CORS의 동일한 출처 정책을 시행하지 않기에 해당 에러를 내뱉습니다.

동일한 출처 정책을 시행하지 않는 태그들은 <script>, <forms>, <img>, <a>, <frame>, <iframe>입니다.

아래는 프론트엔드의 코드이며 <forms>를 사용한 것을 알 수 있습니다.

// Example of request to kakaop oauth using a Form
const requestWithForm = () => {
  var form = document.createElement('form');
  form.setAttribute('method', 'GET');
  form.setAttribute('action', 'https://kapi.kakao.com/v2/user/me');
  
  var idInput = document.createElement('input');
  idInput.setAttribute('type', 'hidden');
  idInput.setAttribute('name', 'client_id');
  idInput.setAttribute('value', 'myapplicationskakaoclientid');
  form.appendChild(idInput);
  
  //.. all other parameters (input elements) I need in my request.
  
  document.body.appendChild(form);
  form.submit();
}

참고 사이트

profile
피자, 코드, 커피를 사랑하는 피코커

0개의 댓글