[UniLetter] Sign in with Apple 애플 로그인/회원가입 구현(서버측)

Seohyun-kim·2022년 11월 12일
4
post-thumbnail

iOS 개발 시작

2022-03월에 Android 앱을 1차 출시하고,
3월~6월 한 학기 동안 교내에 홍보를 하며 테스트를 해 보았다.

사실 20대가 대부분인 대학교 특성 상 아이폰 유저가 대부분일것이다..

iOS에 대한 요청이 꽤 있었고

감사하게도 2022-07월부터 앱센터 iOS 두 분이 함께해주시기로 했다!!

기존에 개발해 놓았던 서버에 크게 변동되는 것은 없었지만

한 2달동안 내 발목을 붙잡았던 애.플.로.그.인ㅋㅋㅋ 아우

iOS님들 죄송합니다...ㅎ 제가 미루고 부족한 탓에 출시가 늦어졌습니다ㅠㅠ



1. Sign in with Apple 시나리오

공식문서 : https://developer.apple.com/documentation/sign_in_with_apple

  1. App-client : 나 로그인 할래! 내 정보랑 토큰 좀 발급해주라~
  2. Apple Server : 자 요기 너라는 것을 증명해줄 토큰이얌 (authorization_code)
  3. App-client : 나 토큰 가져왔서!! 우리 서버에 입장하고 싶엉~
  4. App-Server : 오케이 너 정보로 Apple 한테 물어보고올게!
  5. Apple Server : 오 맞네! 맞다는 증거로 Refresh Token 이랑 User 정보 보내줄게!

2. 애플이 빡치는 점

2.1 소셜로그인을 사용한다면 Apple 로그인은 필수

  • 어떤 로그인 방식을 사용할까에 대한 논의 후, 안드로이드 유저에게는 아주 편한 구글로그인으로 선택했다.

  • 그치만 앱스토어에서는 적어도 하나의 소셜 로그인을 사용한다면,
    반드시 Apple 로그인은 필수로 넣어야 한다는 것이다.

에라이... 애플은 사용자에겐 편한데 개발자에겐 힘든것같움 ㅠ


2.2 Auth code 발급을 위해 우리가 만든 앱에서 요청을 해야한다.

  • 구글에서는 엑세스 토큰을 발급해주는 곳이 있어서 클라이언트 기기 없이도 서버측 테스트를 편하게 할 수 있었음.

  • 근데 이 애플 자슥은 그런 웹 페이지같은 것도 없고...
    실제 우리 App 단에서 요청을 보내야 한다고 들었고..

  • Client 단이 뒷받침 되어야 서버 테스트가 가능하고,
    발급된 authorization_code가 유효기간이 10분인가 이기 때문에 실시간으로 소통이 필요하다.


2.3 우리 앱을 등록하고 key를 생성해야 한다.

  • Apple Development Account에서 애플로그인을 위해 정보들을 체크하고 key 파일을 받아와야 한다.

  • 테스트 용으로 내가 해보려고 Apple 기기 하나 없지만 Apple 계정을 만들었것만
    참내 macOS에서만 발급이 가능한가보네...

  • 아무튼 이 과정에서
    BUNDLE_ID, SERVICE_ID, TEAM_ID, KEY_IDENTIFIER의 정보가 생성되는 것 같고 얘가 나중에 우리서버에서 애플서버로 인증을 요청할 때 쓰인다.

  • 여기서 생성한 key파일 (우리는 p8파일)도 서버에서 PRIVATE_KEY로 쓰인다.

- 얘네 값은 환경변수에 담아둔다.


3. Client(모바일)에서 로그인 요청

  • 우리 앱에서 로그인을 요청하고,
    애플 서버에서 그 사용자에 대한 사용자 정보(id token)
    이 사용자가 맞는지 검증할 토큰(authorization_code)를 내려준다.

  • id token은 기기와 사용자 정보 고유값(email, sub,..) 인 것 같고,
    authorization_code 얘는 유효시간 10분인듯

  • 그리고 여기서 authorization_code를 아래만들 POST 요청에 body에 담아서 보내준다.


4. POST 로그인 요청 라우터

src/server/routes/login/appleOauthLogin.ts

export default defineRoute('post', '/login/oauth/apple', schema, async (req, res) => {
  const {accessToken} = req.body;

  const {user, jwt, rememberMeToken} = await LoginService.appleOAuthLogin(accessToken);

  return res
    .header('token', jwt)
    .cookie(config.server.jwt.cookieName, jwt, config.server.jwt.cookieOptions)
    .json({
      jwt,

      userId: user.id,
      rememberMeToken,
    });
});
  • body에 담겨온 authorization_code 를 편의상 코드에서는 accessToken이라 칭했다.

  • LoginService.appleOAuthLogin에 이 accessToken를 넘겨주고
    로그인이 성공되어 돌아온 user 정보를 받아온다.

  • 아래에서 다시 설명하겠지만, rememberMeToken은 자동로그인을 하기 위함이다.


5. 로그인 서비스

src/service/LoginService.ts

 /** 엑세스 토큰으로 로그인 (사실 authorization_code)
   * @param accessToken
   */
  async appleOAuthLogin(accessToken: string): Promise<LoginResult> {
    const {email, oauthId} = await this.resolveUserInfoFromApple(accessToken);

    const user = await this.getOrCreateUser(email, 'apple', oauthId);

    return this.onSuccess(user);
  }

  private async resolveUserInfoFromApple(accessToken: string) {
    try {
      return await getAppleOAuthInfo(accessToken); //token -> info
    } catch (e: any) {
      printError(e);

      throw WrongAuth();
    }
  }

  private async getOrCreateUser(email: string, oauthProvider: string, oauthId: string): Promise<User> {
    const found = await User.findOne({where: {oauthProvider, oauthId}});
    if (found != null) {
      return found;
    }

    return await User.create({
      email: email,
      nickname: `uni-${new Date().getTime()}`,
      oauthProvider: oauthProvider,
      oauthId: oauthId,
      rememberMeToken: generateUUID(),
    }).save();
  }
  • 다시 애플 서버에 accessToken를 보내 인증을 요청하는 함수 getAppleOAuthInfo를 호출하게 된다.
  • 여기서는 로그인 성공 후 받아온 정보로 데이터베이스의 user 테이블에 생성하는 함수 getOrCreateUser를 호출하게 된다.

    • 기존에 있는 회원이 아니라 신규가입자 라면,
    • 이메일과 oauthId를 저장하고, 닉네임을 임의로 생성한다.
    • 자동 로그인을 위한 rememberMeToken에는 임의로 uuid를 생성하는 함수 generateUUID()를 호출한다.

6. 애플서버로부터 정보 가져오기

src/oauth.ts

/**
 * 애플 로그인 (Token -> User Info)
 */
export async function getAppleOAuthInfo(accessToken: string): Promise<OAuthInfo> {
  const clientID = config.external.appleSignIn.bundleID; /*일단 지금은 Apple iOS 기기용으로 기대중*/

  const info = await appleSignin.getAuthorizationToken(accessToken, {
    clientID,
    clientSecret: appleSignin.getClientSecret({
      clientID,
      ...config.external.appleSignIn
    }),
    ...config.external.appleSignIn
  });

  log(info)

  const {id_token, refresh_token} = info; /* info에서 refresh token은 버리나봄...*/

  const idTokenDecoded = jwt.decode(id_token) as { email: string, sub: string };

  const {email, sub} = idTokenDecoded;

  if (email == null) {
    throw NoEmail();
  }

  if (sub == null) {
    throw NoSubject();
  }

  return {
    email: email,
    oauthId: sub,
  };
}
  • 환경변수에 담아두었던 key 값들 (bundleID, serviceID, teamID, keyIdentifier, privateKey,redirectUri) 이 여기서
    config.external.appleSignIn에 담겨있다고 보면 된다.

  • accessToken과 key값 정보를 애플 서버로 넘겨주고,
    그 리턴 타입은 아래와 같다. (이 코드에서 info에 담겨짐)

    (appleSignin 정보)
        export interface AppleAuthorizationTokenResponseType {
      /** A token used to access allowed data. */
      access_token: string;
      /** It will always be Bearer. */
      token_type: 'Bearer';
      /** The amount of time, in seconds, before the access token expires. */
      expires_in: number;
      /** used to regenerate (new) access tokens. */
      refresh_token: string;
      /** A JSON Web Token that contains the user’s identity information. */
      id_token: string;
    }
  • 사용자의 id_token을 decode 하여 email과 sub(=oauth id) 를 받아오고
    두 값을 리턴한다.


코드 시나리오.

모바일(App-client)로부터 로그인 요청을 받고,
애플 서버가 auth code를 발급해 준다.
그 code를 우리 서버(App-server)에 제시하고
이 토큰을 들고 애플서버로 가서 사용자가 진쨔 맞는지 검증
맞으면 사용자에 대한 정보 id_token을 내려줌
얘는 jwt 형태이기 때문에 decode해서 email, subject,.. 정보 알아냄
그 사용자가 신규 회원이면 DB에 저장.
이제 유저 정보(userId)와 자동로그인을 위한 rememberMeToken를 응답 내려줌.



7. Swagget API 문서

  • 요청

  • 응답

4개의 댓글

comment-user-thumbnail
2022년 11월 24일

애플 로그인 해 보려 검색해 보는데 알고 있는 것과 달라서 뭐가 맞는지 확인차 여쭤봅니다.
앱에서 로그인하면 id_token과 authorization code를 주는데
id_token은 사용자 정보가 들어 있고 (이메일, 이름 등)
authorization code는 만료 기간 갱신을 위한 토큰이 아닌가요?

굳이 다른 소셜 로그인과 비교하자면 id_token은 access_token, authorization code는 refresh token으로 이해했는데 본문글을 읽고 좀 헷갈려서요.

로그인할 때 이미 사용자 정보가 담긴 id_token을 주지만 이걸 열어보기 위해, 검증을 위해
애플 서버의 퍼블릭키를 발급받아 같은 알고리즘 타입의 키를 이용해 id_token을 복호화해
사용자 정보를 이용하는 것이 아닌지 궁금합니다.

1개의 답글
comment-user-thumbnail
2023년 7월 7일

애플 로그인은 플로우부터 차이점이 있어서 구현하기도 전에 팔짱 끼고 2시간 동안 고생했던 것 같아요 ㅋㅋㅋ ㅜ

답글 달기