4 min read

์• ํ”Œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ for Backend

Table of Contents

์• ํ”Œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ•  ๋•Œ ํ•„์š”ํ•œ Backend ๋กœ์ง์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ํ•„์ž๋„ ์ด๋ฒˆ์— ์ฒ˜์Œ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ์ •๋ฆฌ์ด๋‹ค ๋ณด๋‹ˆ, ์• ํ”Œ์—์„œ ์›ํ•˜๋Š” ํ˜น์€ ์˜๋„ํ•œ ๊ฒ€์ฆ ๋ฐฉ์‹์„ ์ถฉ์‹คํžˆ ๋”ฐ๋ฅด์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋””๊นŒ์ง€๋‚˜ ์ฐธ๊ณ ์šฉ์œผ๋กœ ๋ด์ฃผ์‹œ๊ณ , ์˜๋ฌธ์ ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์ด ์žˆ์œผ์‹œ๋ฉด ๋Œ“๊ธ€์— ๋‚จ๊ฒจ์ฃผ์„ธ์š”! ๊ฐ™์ด ๊ณ ๋ฏผํ•ด๋ณด๋ฉด ๋” ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

2๊ฐ€์ง€ ๋ฐฉ๋ฒ•

์• ํ”Œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํฌ๊ฒŒ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ทจํ•  ์ˆ˜ ์žˆ๋‹ค.

  1. Frontend์—์„œ ๋ชจ๋“  ์œ ์ € ์ •๋ณด๊ฐ€ ๋‹ด๊ธด id_token ์„ ๋ฐ›์•„์„œ, ํ•ด๋‹น id_token ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•˜๊ธฐ
  2. Frontend์—์„œ Authorization์„ ์œ„ํ•œ code ๋ฅผ ๋ฐ›์•„์„œ, ํ•ด๋‹น code ๋กœ ์• ํ”Œ ์„œ๋ฒ„์—์„œ token ๊ณผ refresh_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_token ์˜ header ์—๋Š” 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๊ฐ€ ์ƒ๊ฒจ์„œ ์ƒ๋‹นํžˆ ์ฝ๊ธฐ ํŽธํ•ด์กŒ๋Š”๋ฐ, ๋‹ค์‹œ ์ฝ์–ด๋ณด๋ฉด์„œ ์• ํ”Œ ๋กœ๊ทธ์ธ ๊ฒ€์ฆ์— ๋Œ€ํ•œ ์˜๋„๋ฅผ ํŒŒ์•…ํ•ด๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

๋.