오늘 배운 것

어제 많은 삽질을 통해서 애플로그인에서 액세스토큰을 발급받는 데는 성공하였다. 그러면 기존의 로직을 사용해서, 이렇게 얻어낸 값을 앱의 값으로 저장해주면 되겠다. 또한 어제 봤는데 애플 로그인에서 맨 처음에 이메일 값을 제대로 저장하지 못할 경우에 대해서 걱정이 많았었다. 그런데 그 밑의 내용을 읽어보니 계속적으로 이메일을 받아올 수 있는 방법이 있었다. (다만 이 경우는 테스트 환경에서만 가능하긴 했다...) 어쨌든 다음과 같은 두 가지 작업을 하면 되겠다. 

  1. 설정 바꿔서 다시 email 받아오도록 설정하고, 이메일 값 저장해두는 로직 작성하기
  2. 받아온 identityToken 값을 백엔드 서버에 보내는 API 작성하기

우선 1번의 경우 위의 사진에 나온 것처럼 에뮬레이터의 설정에 들어가서 기존에 Apple ID를 사용한다는 설정을 다시 취소해 주어야 했다. 취소해 주고 다시 해보니 아래처럼 email 값과 identityToken 값 모두가 잘 들어온다. 

 

그런데 여기서 두 가지의 잠재적 문제점을 발견했다. 

 

첫 번째는 어쨌든 테스트 환경이 아니면 유저가 굳이 폰에서 로그인이 된 설정을 다시 풀 리가 없다. 그러므로 만약 유저가 앱에 최초 로그인을 하는데 그 과정에서 실패가 일어나서 실수로 딱 한번 주어지는 이메일 값을 저장소에 저장하지 못한다면, 문제가 생긴다. 

 

두 번째는 유저가 본인의 이메일 정보를 제공하지 않는 옵션을 선택할 경우이다. 이 경우에는 최초 로그인이더라도 프론트에서 유저의 이메일 정보를 갖고 있을 수 없다. 

 

그래도 두 경우 모두 identityToken의 값(애플에서 로그인을 마치고 받은 액세스토큰과 같은 개념)은 얻을 수 있었다. 여기서 궁금한 점은 identityToken 값으로 유저의 이메일 정보를 얻을 수 있는지였다.

 

이메일 정보가 중요한 이유는 사실상 이메일 필드가 pk를 제외하고 유저를 유일하게 구분할 수 있는 속성이기 때문에 꼭 필요하다고 판단했기 때문이다. 또한 구글 로그인 유저는 email이 항상 있는 반면 apple 로그인 유저에서는 없다면 이 경우에도 나중에 여러 유저의 데이터를 공통적으로 다루기 어려울 것 같았다. 

 

그래서 찾아보니 pyJWT 라이브러리로 해당 Identitytoken을 분해해볼 수 있을 것 같았다. GPT의 도움을 받아 스크립트를 작성해봤다. 

import jwt
import requests
import urllib3
from jwt.algorithms import RSAAlgorithm


APPLE_PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys"


urllib3.disable_warnings()

def get_apple_public_key(kid):
    # SSL 검증 비활성화하고 Apple 공개 키를 가져옴
    response = requests.get(APPLE_PUBLIC_KEY_URL, verify=False)
    keys = response.json().get("keys", [])

    # kid에 맞는 키 찾기
    for key in keys:
        if key["kid"] == kid:
            return RSAAlgorithm.from_jwk(key)
    raise ValueError("Matching key not found")

def validate_identity_token(identity_token):
    try:
        # JWT의 헤더에서 kid 추출
        unverified_header = jwt.get_unverified_header(identity_token)
        kid = unverified_header["kid"]

        # kid에 맞는 Apple 공개 키 가져오기
        public_key = get_apple_public_key(kid)

        # 토큰 디코드 및 검증
        decoded_token = jwt.decode(
            identity_token,
            public_key,
            algorithms=["RS256"],
            audience="audience",  # 실제 Apple Login에서 설정한 client_id로 변경
            issuer="https://appleid.apple.com"
        )

        # 유효한 토큰인 경우 정보 반환
        user_id = decoded_token.get("sub")
        email = decoded_token.get("email", None)

        return {
            "status": "success",
            "user_id": user_id,
            "email": email
        }
    except jwt.ExpiredSignatureError:
        return {"status": "error", "message": "Token has expired"}
    except jwt.InvalidTokenError:
        return {"status": "error", "message": "Invalid token"}
    except Exception as e:
        return {"status": "error", "message": e}


identity_token = "identity_token"


result = validate_identity_token(identity_token)

 

그리고 그 과정에서 원래는 SSL 관련 에러가 났었다. 이 에러는 SSL 인증서가 없어서 나는 에러였고, SSL 인증서를 발급받거나 SSL 인증을 임시로 무효화하는 방법이 있었다. SSL 인증서 발급은 간단했지만, 그래도 SSL 인증서를 발급받는 방법은 해당 인증서를 서버에도 설치해줘야 했기 때문에 더 번거롭다고 판단했다. 그래서 위에 SSL 인증을 임시로 건너뛰는 코드를 추가해줬다. 

 

그랬더니 다음과 같이 user id와 email 값이 잘 나오더라. 물론 user id는 실제 pk와 같은 정수값은 아니었다. 그리고 emai도 토큰에 잘 포함되어 있어서, 서버에 잘 저장이 가능해 보였다. 

 

그렇다면 앞서 맨 처음에 걱정했던 이메일을 저장하는 문제를 해결해볼 수 있겠다. 이메일을 최초 시도에 바로 받아오는 방법만이 아니라, identityToken 값에서 이를 추출하는 방법이 있겠다. 이 방법을 프론트에서도 사용할 수 있도록 코드를 바꿔보자. js 코드로 애플로그인에서 받은 identityToken을 디코딩하는 방법은 생각보다 간단했다. 

import { jwtDecode } from 'jwt-decode';

const appleAuthRequestResponse = await appleAuth.performRequest({
    requestedOperation: appleAuth.Operation.LOGIN,
    requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
})

const decodedToken = jwtDecode(identityToken);

 

이로서 프론트 앱에서도 identityToken 값을 통해서 email 값을 얻을 수 있게 되었다. email 값뿐만 아니라 많은 정보를 볼 수 있었다. 

 

이제 구글 로그인에서 해당 identityToken을 가지고 서버에 요청을 보내는 로직을 참고해서, 애플 로그인에서도 이를 비슷하게 구현해 보자. 

 

그리고 아이폰에서도 구글로그인도 되어야한다... 이를 위해서는 iosClientId 값이 필요하다. 이것도 API를 통해 받아와 보자. 우선은 애플 로그인부터 구현해 보자. 

 

우선 애플 로그인을 시도했을 때 response를 잘 받아와야 하기 때문에, 이를 저장할 상태 변수를 선언해 준다. 이 appleResponse는 애플 로그인이 성공적으로 실행되면 해당 response를 저장하기 위한 변수이다. 또한 구글로그인과 애플로그인은 로직이 겹치는 부분이 많기 때문에 중복 코드를 줄이기 위해서 loginType 변수에 구글 로그인인지, 애플 로그인인지를 저장하도록 하였다. 

const GOOGLE_LOGIN = 0;
const APPLE_LOGIN = 1;

const [appleResponse, setAppleResponse] = useState(null);
const [loginType, setLoginType] = useState(null);

 

그리고 useEffect 훅을 사용해서 해당 appleResponse의 상태가 null이 아니게 되면 애플 로그인 후 그에 맞는 조치(애플로그인에서 받은 idToken을 jwtToken으로 바꾸고, AsyncStorage에 관련 값을 설정하는 일)를 하도록 한다. 

useEffect(() => {
  if (appleResponse !== null) {
    const appleToken = appleResponse.identityToken;
    if (appleToken) {
      setLoginType(APPLE_LOGIN);
      handleLogin(appleToken, loginType);
    }
  }
}, [appleResponse]);

 

그리고 해당 loginType 변수값에 따라서 googleLogin API 또는 appleLogin API를 호출하도록 설정해 주었다.

if (loginType === GOOGLE_LOGIN) {
  loginResponse = await Api.googleLogin({ token, deviceToken });
} else if (loginType === APPLE_LOGIN) {
  loginResponse = await Api.appleLogin({ token, deviceToken });
}

 

그리고 Api 클래스에 appleLogin 함수를 추가해 주었다. 

 

+ Recent posts