도림.로그

Tags

Series

About

애플 로그인 구현 for Backend

2022. 06. 10.


    애플 로그인을 구현하려고 할 때 필요한 Backend 로직에 대해서 정리하려고 합니다. 필자도 이번에 처음 구성하는 것에 대한 정리이다 보니, 애플에서 원하는 혹은 의도한 검증 방식을 충실히 따르지 못할 수 있습니다. 어디까지나 참고용으로 봐주시고, 의문점이나 피드백이 있으시면 댓글에 남겨주세요! 같이 고민해보면 더 좋을 것 같습니다.

    2가지 방법

    애플 로그인을 구현하기 위해서는 크게 2가지 방법을 취할 수 있다.

    1. Frontend에서 모든 유저 정보가 담긴 id_token 을 받아서, 해당 id_token 이 유효한지 검증하기
    2. Frontend에서 Authorization을 위한 code 를 받아서, 해당 code 로 애플 서버에서 tokenrefresh_token 받기

    혹은 1과 2를 동시에 사용하여 조금 더 엄격한 인증 관리도 가능하겠다.

    이번 글에서는 각각의 방법을 어떻게 구현하면 되는지, 그리고 왜 이 인증 방식이 유효한지에 대해 알아보도록 하겠다. (예제 코드는 TypeScript로 작성하도록 하겠다.)

    id_token 검증

    이 방법은 애플 로그인을 구현하는 직관적이고 가장 쉬운 방법이라고 볼 수 있다.

    이 검증 방식의 Sequence Diagram은 다음과 같다.

    Login With Apple - id_token.svg

    이 방식의 핵심은 Client에서 API 서버로 건네주는 JWT 토큰인 id_token 의 payload 파트에 기본적으로 필요한 정보(sub, iat, exp 등)는 모두 포함되어 있다는 것이다.

    따라서 우리는 애플 서버에 별도의 요청을 할 필요가 없다. 그저 이 토큰이 유효한지, 아니면 API 서버에 도달하기 전에 변조되지 않았는지 확인하면 되는 것이다.

    이를 수행하기 위한 Function을 간단히 작성해보면 다음과 같다. (로직만 보기 위해서 예외 처리 부분은 제거하였다.)

    import * as jwt from 'jsonwebtoken';
    import { JwksClient } from 'jwks-rsa';
    
    interface AppleJwtTokenPayload {
      iss: string;
      aud: string;
      exp: number;
      iat: number;
      sub: string;
      nonce: string;
      c_hash: string;
      email?: string;
      email_verified?: string;
      is_private_email?: string;
      auth_time: number;
      nonce_supported: boolean;
    }
    
    async verifyAppleToken(appleIdToken: string): Promise<AppleJwtTokenPayload> {
      const decodedToken = jwt.decode(appleIdToken, { complete: true }) as {
        header: { kid: string; alg: jwt.Algorithm };
        payload: { sub: string };
      };
      const keyIdFromToken = decodedToken.header.kid;
    
      const applePublicKeyUrl = 'https://appleid.apple.com/auth/keys';
    
      const jwksClient = new JwksClient({ jwksUri: applePublicKeyUrl });
    
      const key = await jwksClient.getSigningKey(keyIdFromToken);
      const publicKey = key.getPublicKey();
    
      const verifiedDecodedToken: AppleJwtTokenPayload = jwt.verify(appleIdToken, publicKey, {
        algorithms: [decodedToken.header.alg]
      }) as AppleJwtTokenPayload;
    
      return verifiedDecodedToken;
    }

    Function의 작동 흐름은 다음과 같다.

    1. id_token(JWT)을 decode 해야 한다. 이 때, complete 옵션을 true 로 설정하여 header 까지 얻을 수 있는 형태로 decode 한다.
    2. 애플의 id_tokenheader 에는 key ID인 kid 가 포함되어 있다. 이것이 필요하다.
    3. 애플의 Public Key URL에서 JWKS(Json Web Key Set)을 가져온다.
    4. 이 JWKS에는 우리가 header 에서 꺼내온 kid 에 대응되는 Key Set이 반드시 존재해야 한다. 이 Key Set으로부터 signingKey 를 가져온다.
    5. 해당 signingKey 에서 publicKey 를 추출한다.
    6. 이제 publicKey 를 이용하여 id_token 을 검증한다. 이 때 암호화 알고리즘은 header 에 들어있었던 알고리즘 정보를 사용하도록 한다.
    7. 검증된 토큰의 payload를 반환한다.

    크게 어려운 부분 없이 Sequence Diagram을 충실히 따르고 있는 Function이라고 볼 수 있다.

    id_token 의 Signature는 애플의 Private Key로 암호화 되어 있는 상태이고, 이것은 애플의 Public Key로만 복호화가 가능하다. 다르게 말해서, 애플의 Private Key가 아닌 임의의 Private Key로 암호화 되었을 경우, 애플의 Public Key로 복호화가 불가능 할 것이다.

    이를 통해서 id_token 이 중간에 변조 되었는지, 아니면 네트워크 전송 과정에서 손상 되었는지 검증할 수 있다.

    code 검증

    이 방법은 제약 사항도 몇 가지 더 생기고 애플 서버에 요청도 추가적으로 보내야 하는 단점이 있지만, 애플로부터 우리의 API 서버에 직통으로 토큰을 제공받기 때문에 해당 토큰을 100% 신뢰할 수 있다는 장점이 있다. 또한, 한 번 Refresh Token을 제공 받고, 이를 잘 관리하면 유저가 추가적으로 애플 로그인 모달을 보지 않아도 된다는 장점이 있다.

    이 검증 방식의 Sequence Diagram은 다음과 같다.

    Login With Apple - _Authorization Code.svg

    이 방법을 수행하기 위해서는 애플 개발자 콘솔에서 .p8 확장자를 가진 Private Key를 생성해야 한다. (이에 대한 전반적인 절차는 https://github.com/ananay/apple-auth/blob/master/SETUP.md 를 참고하면 된다.)

    이 검증 방식에 대한 코드는 https://github.com/ananay/apple-auth 를 참고하거나, 사용하면 된다.

    Passport 사용자들의 경우 https://github.com/ananay/passport-apple 를 사용하면 되나, NestJs에서의 동작은 약간의 수정을 요한다는 Issue들이 있다.

    뭐가 맞는 방법일까

    어떤 것이 옳은 방법인지, 혹은 애플에서 권장하는 방법이 무엇인지에 대해서 아직도 개념적으로 정리하지는 못한 것 같다. 2022년 WWDC를 기점으로 애플 개발자 문서에 SideBar가 생겨서 상당히 읽기 편해졌는데, 다시 읽어보면서 애플 로그인 검증에 대한 의도를 파악해봐야 할 것 같다.

    끝.

    #apple#auth#node#TypeScript#jwt#jwks

    © 2024, Built with Gatsby