AppStore에 올리기 위해서 애플 로그인이 필수라 하여 하게되었다.
애플 로그인은 이전에 해보았던 카카오 로그인처럼 프로바이더(애플, 카카오, ...)에서 발급한 토큰을 이용해 프로바이더에 API 요청을 하고 데이터를 받는 방법이 없다고 한다.(첫 번째 고난의 시작: 토큰 검증과 사용자 정보 받는건 어떻게 할까... -> 아래의 검증 단계를 익힘으로써 해결)
IOS 클라이언트에서 Authenticate Code, Identity Token 등을 받는다.
여기서는 Identity Token(개인적으로 이 Identity Token이라는게 좀 헷갈렸었다)으로 하겠다.
이제 이 Identity Token을 IOS 클라이언트에서 서버로 넘긴다.
서버에서 할 일은 넘겨받은 토큰에 대한 검증을 하고, 추출할 수 있는 사용자 정보를 추출해 저장해두는 것이다.
토큰 검증 수행방법은 다음과 같다.
토큰 검증
먼저, 클라이언트에서 넘겨준 토큰을 가져온다.
토큰의 헤더 부분을 디코딩한다.(JWKS가 나온다)(두 번째 고난: 헤더 디코딩은 따로 어떻게 할까.... -> jwt-decode 라이브러리를 사용함으로써 해결)
const tokenDecodedHeader: IdentityTokenHeader =
jwtDecode<IdentityTokenHeader>(identity_token, {
header: true,
});
애플에 공개 JWKS를 요청한다.(JWKS가 응답으로 온다)
const client: jwksClient.JwksClient = jwksClient({
jwksUri: 'https://appleid.apple.com/auth/keys',
});
애플에서 받은 JWKS에 토큰의 헤더 부분을 디코딩 해서 나온 JWKS와 같은 것이 있는지 찾는다.(없으면 Reject 시킨다)(세 번째 고난: JwksClient만으로는 구현이 어렵다.... -> JwksClient를 사용하는 것이 아닌 따로 애플에 API 요청을 통해 받은 데이터를 변수에 저장하고 이를 필터링 함으로써 해결)
const applePublicKeys: ApplePublicKeyType = await this.api.Get(
'https://appleid.apple.com/auth/keys',
); //얘를 필터링 해서 찾고 getSigningKey()를 이용해 아래에 대입해서 추출
const client: jwksClient.JwksClient = jwksClient({
jwksUri: 'https://appleid.apple.com/auth/keys',
});
매핑된 JWKS를 이용해 Public Key를 생성한다.
Public Key를 이용해 Identity Token을 디코딩 해 페이로드를 얻는다.(유저 정보도 담겨있다)
const result: IdentityTokenSchema = jwt.verify(
identity_token,
publicKey,
) as IdentityTokenSchema;
페이로드 정보를 통해 토큰을 검증한다.(만료여부같은 검증은 이전에 jwt의 verify()를 통해 진행)
private ValidateToken(token: IdentityTokenSchema): void {
if (token.iss !== 'https://appleid.apple.com') {
throw new InvalidTokenError();
}
if (token.aud !== this.audience) {
throw new InvalidTokenError();
}
}
위 과정을 거쳐 Identity Token에 대한 검증을 수행한다.
배운것: 커스텀 전략, 가드(extends CanActivate
) 구성과 이 둘 연결. interface
와 type alias
의 사용
사실 처음에는 주먹구구식으로 실행만 되는 코드를 짰었다.(컨트롤러와 서비스에 로직 추가만해서)
전략(Strategy)을 커스텀하게 만들어 가드를 이용해 인증 과정을 진행하는식으로 구현하고 싶었지만, 방법을 몰라 그러지 못했었다.
완성된 코드를 보고 너무 지저분하고 목적성이 불분명한 부분들이 많이 보였다.
다시 커스텀 전략을 구성해 구현할 수 있는 방법을 모색해보고 결국, 찾게 되었다.
해당 방식을 이해하고, 내가 짠 로직에 맞게 바꿔보면서 이전에 했던 카카오 로그인과 더불어 애플 로그인 커스텀 전략을 만들고, 가드를 사용하는 방식으로 바꾸게 되었다.
확실히 보다 더 깔끔해지고 명확해졌다는 느낌이 들었다.
또, any
타입을 무분별하게 사용하는 것이 좋지 않음을 알면서 계속 사용된 것을 보았다.
interface
와 type alias
를 이용해 타입(형식)을 적용할 수 있는 부분은 최대한으로 하여 any
의 사용을 줄이게 되었다.
당연한 것이고, 기본적인 것이지만 이전에 그렇게 안해왔던터라 보다 더 TypeScript스러워졌음을 느꼈다.
안녕하세요! 저도 지금 NestJS를 활용해서 애플 로그인을 구현하고 있습니다. 혹시 Passport를 따로 사용하지 않으시고 구현하신건가요?