Apple Auth(feat. RS256)

jaemoon406·2022년 9월 20일
0

프로젝트에 apple 로그인을 적용하였다.
처음 접해본 방식이라 블로깅을 하려 한다.
Apple 로그인 과정 중 앱(Ios)을 통해 code와 id_token을 받아, 유효성 검사를 진행한다.
유효성 검사를 통해 Apple Server의 유저 고유 값(id)를 받을 수 있었다.

절차

  1. 앱에서 받은 id_token(jwt)에서 kid를 추출
  2. Apple에서 제공하는 JWK를 조회 및 kid가 일치하는 값 찾기
  3. code, client_id, client_secret등 Apple에서 요구하는 여러 값을 해당 url로 post하면 나오는 id_token을 유지한다.
  4. 일치하는 값 n을 algorithm 서명으로 public key 생성
  5. 3에서 만든 id_token4에서 만든 public key로 복호화를 하면 sub(AppleID)가 추출된다.

아래는 Apple 로그인을 라이브러리화 하여 만든 코드이다.

import datetime
import os
import jwt

import requests
from django.core.exceptions import BadRequest
from rest_framework.exceptions import NotFound

from jwt.algorithms import RSAAlgorithm
from dotenv import load_dotenv
from .base import BaseSNSAuth

load_dotenv()

class AppleOAuth(BaseSNSAuth):
    key_id = os.environ.get('KEY_ID')
    client_id = os.environ.get('CLIENT_ID')

    JWKS_url = "https://appleid.apple.com/auth/keys"
    token_audience_url = 'https://appleid.apple.com'
    code_check_url = "https://appleid.apple.com/auth/token"

    def get_jwk(self, kid=None):
        """
        apple public key(jwk) GET
        """
        jwks = super().get_json_response(path=self.JWKS_url)['keys']
        if not isinstance(jwks, list):
            raise NotFound

        if kid:
            return next((jwks for jwks in jwks if jwks['kid'] == kid), None)
        else:
            return jwks

    def get_audience(self):
        return self.client_id

    def get_private_key(self):
        with open('**********.p8') as keyfile:
            private_key = keyfile.read()
        return private_key

    def encode_apple_jwt(self):
        now = datetime.datetime.now()
        key_id = self.key_id
        team_id = os.environ.get('TEAM_ID')
        bundle_id = self.client_id

        headers = {'kid': key_id}
        payload = {
            'iss': team_id,
            'iat': now,
            'exp': now + datetime.timedelta(days=2),
            'aud': self.token_audience_url,
            'sub': bundle_id
        }
        apple_jwt = jwt.encode(headers=headers, payload=payload, algorithm='ES256', key=self.get_private_key())
        return apple_jwt

    def get_apple_token(self, code, redirect_url='https://******.co.kr'):
        auth_check = requests.post(url=self.code_check_url,
                                   headers={'content-type': 'application/x-www-form-urlencoded'},
                                   data={
                                       'client_id': self.client_id,
                                       'client_secret': self.encode_apple_jwt(),
                                       'code': code,
                                       'grant_type': 'authorization_code',
                                       'redirect_uri': redirect_url,
                                   })
        response = auth_check.json()
        return response

    def decode_id_token(self, id_token):
        try:
            kid = jwt.get_unverified_header(id_token)['kid']
            jwk = self.get_jwk(kid)
            public_key = RSAAlgorithm.from_jwk(jwk)
            decoded_id_token = jwt.decode(
                jwt=id_token, key=public_key, algorithms=['RS256'], audience=self.get_audience())
        except Exception:
            raise ValueError('Wrong number of segments in token')
        return decoded_id_token

    def validate_decode_token(self, code):
        decoded_token = self.get_apple_token(code)
        id_token = decoded_token['id_token']
        refresh_token = decoded_token['refresh_token']

        return self.decode_id_token(id_token), refresh_token


def get_user_sns_id(data):
    apple = AppleOAuth()
    print(data)
    client_decode_data = apple.decode_id_token(data['token_key'])
    if client_decode_data:
        valid_id_tkn, refresh_tkn = apple.validate_decode_token(data['code'])
        sns_id = client_decode_data.get('sub')
        if sns_id == valid_id_tkn.get('sub'):
            return sns_id, 'apple', refresh_tkn
    else:
        raise ValueError('Wrong number of segments in token')
profile
이제부터 백엔드 개발자

0개의 댓글