애플 앱스토어에 우리가 만든 앱을 등록하려면,
애플 로그인이 반드시 필요하기 때문에
TIFY 프로젝트에도 애플 로그인을 구현하게 되었다.
다른 팀원이 이미 구현해둔
OIDC를 이용한 카카오 로그인이 있었기 때문에
외부 서버(카카오, 애플 등)에 저장되어있는 사용자의 정보를 가져와
우리 서비스로 끌어와 사용하는 것에 대한 중간 검증이 어렵진 않았으나,
카카오 로그인과 애플 로그인의 방식이 미묘하게 다르고(client_secret을 알아서 만들어야 한다)
이미 짜여진 코드를 분석하며 구현을 하다 보니
포스팅으로 남겨놓으면 좋지 않을까 하여
블로그 작성을 진행한다.
자세한 코드는 아래 pr에 나와있다.
[FEATURE] 애플 로그인 구현 by bongsh0112 · Pull Request #124 · Team-TIFY/TIFY-SERVER
📝 PR Summary 애플 로그인 구현 🌲 Working Branch feat/105-apple-login 🌲 TODOs properties, feignclient 세팅 링크, 인증 code 받기 code를 이용하여 idtoken 받기 idtoken을 이용하여 회원가입, 로그인하기 로그아웃,
github.com
0) Apple Login 시 IdToken에 대한 검증이 필요한 이유
0-1) 애플 로그인 후 바로 발급되는 idToken은?
처음 애플 로그인을 구현할 때,
이미 구현되어있던 카카오 로그인을 참고했고
code에서부터 IdToken을 가져올 수 있게 세팅해놓고
그 IdToken만을 가지고 회원가입을 할 수 있는 것을 보고
왜 IdToken을 검증할까? 라는 생각이 들었다.
코드를 뜯어보고 프론트와의 관계도 생각을 해보니,
이미 카카오 로그인에서 redirect url에 딸려나오는 code를 가지고 로그인을 처리한 프론트의 로직에 좀 더 맞게 해야겠다고 생각했고,
그러기 위해서는 애플 로그인 링크의 scope를 바꾸는 것이 불가피했다.
[FEATURE] 애플 로그인 code 관련 수정 by bongsh0112 · Pull Request #136 · Team-TIFY/TIFY-SERVER
📝 PR Summary 애플 로그인 시 code가 redirect uri에 붙어 오지 않는 현상 수정 🌲 Working Branch feat/135-apple-login 🌲 TODOs Related Issues close #135
github.com
이렇게 되다 보니, IdToken을 한번에 가져오는 것이 불가능해졌고
code를 이용하여 토큰들을 발급받은 후
그것들을 이용하여 회원가입을 하는 것이 낫다고 생각했다.
0-2) OIDC AccessToken과 IdToken
애플로 로그인하기를 구현하기 위해
Apple Developer 공식 사이트에 들어가면
위와 같은 그림이 나타난다.
첫 단계인 Request with scope는
TIFY 서버 단에서 클라이언트에게 애플 로그인 링크(scope 포함됨)를 주면
사용자가 애플 로그인을 하는 과정(Request user information)이다.
이 과정이 지나고 그 산물인 code를 통해
Verify user and get token이 가능한데,
여기에서 Apple ID Server에서 발급해주는
AccessToken과 RefreshToken, IdToken을 얻을 수 있다.
그런데 공식 사이트에 나와있는
사용자 검증에서 IdToken 얘기만 나오는 걸 보고
AccessToken과 RefreshToken은 왜 언급도 없나 생각이 들었다.
생각해보면 이미 구현되어있는 카카오 로그인에서도
Oauth 리소스 서버에서 준 AccessToken, RefreshToken은 사용하지 않았다.
그래서 카카오 로그인을 한참 전에 구현해본 든든한나의친구찬진맨의 블로그를 찾아본 결과...
해답을 얻을 수 있었다. ⬇️
Apple ID Server에서 발급되는 AccessToken은
Apple OIDC를 이용할 수 있도록 인가해주는 토큰으로,
Oauth 리소스 서버, 즉 Apple ID Server에 접근하는데에 쓰이는 토큰이고,
우리 서비스에 회원가입을 하려고 한다면
Oauth 인증 과정을 거쳐서 회원가입, 로그인을 한뒤에 자신의 서버에서 인증용 토큰을 발급해줘야한다.
그럼, 혹시라도 어떤 누군가가
다른 서비스에서 애플 로그인을 한 후에
발급받은 AccessToken으로 우리 서비스에 회원가입 / 로그인을 하려고 하면...?
결국 AccessToken 만으로
우리 서비스에 회원가입하려고 하는 것은 무리가 있다.
따라서 어떤 사용자가
우리 서비스에서 애플 로그인 요청을 하였고
그를 통해 TIFY의 회원가입 / 로그인을 원하는 것을 검증하려면
AccessToken, RefreshToken과 같이 제공되는
IdToken을 이용해야 한다는 결론이 나온다.
1) IdToken 검증하기
위 과정(코드 발급 -> Oauth 리소스 서버에서 token 발급)을 수행하고 나온
AccessToken과 RefreshToken과는 달리
IdToken은 jwt 형식으로 인코딩되어있다.
IdToken을 jwt.io에서 인코딩 해보면
aud에 우리 서비스의 앱 키가 나타나며
이를 통해 우리 서비스에서 토큰 발급 요청을 보냈다는 것을 알 수 있다.
그렇다면 여기서 끝인가...?
방심하면 안된다.
위에서도 말했듯이 IdToken을 디코딩하는 것은
jwt.io에 복붙해서 할 수 있을 정도로 쉽다.
이 토큰이 안전하게 인증된 정보인지를 알아내는 것이 중요하다...!
1-1) 애플 공개 키 받아오기
IdToken을 디코딩하여 인증되었는지를 확인하기 위해서는
애플 공개 키 가져오기 과정이 필요하다.
공개 키를 가져오는 FeignClient 코드는 다음과 같다.
github에서 카카오 로그인 코드까지 다 보았다면
카카오 클라이언트에서는 레디스 캐싱까지 하는 것을 볼 수 있다.
카카오는 공개 키를 너무 많이 요청하게 되면 차단을 먹는다고 하는데,
애플은 그런 언급이 없는 것 같아 (더 읽어보면 나올수도..) 우선은 하지 않았다.
@FeignClient(
name = "AppleKeyClient",
url = "https://appleid.apple.com",
configuration = AppleAuthFeignConfig.class)
public interface AppleKeyClient {
@GetMapping(value = "/auth/keys")
ApplePublicKeyResponse getAppleAuthPublicKey();
}
@Getter
@ToString
@NoArgsConstructor
public class ApplePublicKeyResponse {
List<AppleOIDCPublicKeyDto> keys;
}
@Getter
@ToString
@NoArgsConstructor
public class AppleOIDCPublicKeyDto {
// 지금와서 드는 생각이지만,
// ApplePublicKeyResponse 클래스 안에 static class로 선언하면 더 나았지.. 싶다
private String kty;
private String kid;
private String alg;
private String use;
private String n;
private String e;
}
AppleOIDCPublicKeyDto를 List로 받는 이유는
공개키의 종류가 두 가지이기 때문인데,
IdToken을 디코딩했을 때 나오는 kid와 같은 값을 가지는
공개키를 사용하면 된다.
1-2) 공개 키로 IdToken 검증하기
// AppleOauthHelper 에서
public OIDCDto getOIDCDecodePayload(String token) {
ApplePublicKeyResponse response = appleKeyClient.getAppleAuthPublicKey();
// idToken의 kid가져오기
String kid =
oidcHelper.getKidFromUnsignedIdToken(
token,
oauthProperties.getAppleBaseUrl(),
oauthProperties.getAppleClientUrl());
// idToken의 알고리즘(alg) 가져오기
String alg =
oidcHelper.getAlgfromUnsignedIdToken(
token,
oauthProperties.getAppleBaseUrl(),
oauthProperties.getAppleClientUrl());
// 애플 공개키 중 kid, alg가 idToken과 일치하는 것 가져오기
AppleOIDCPublicKeyDto appleOIDCPublicKeyDto =
response.getKeys().stream()
.filter(o -> o.getKid().equals(kid) && o.getAlg().equals(alg))
.findFirst()
.orElseThrow();
return jwtOIDCProvider.getOIDCTokenBody(
token, appleOIDCPublicKeyDto.getN(), appleOIDCPublicKeyDto.getE());
}
// JwtOIDCProvider 에서
public String getKidFromUnsignedTokenHeader(String token, String iss, String aud) {
return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(KID);
}
public String getAlgFromUnsignedTokenHeader(String token, String iss, String aud) {
return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(ALG);
}
private Jwt<Header, Claims> getUnsignedTokenClaims(String token, String iss, String aud) {
try {
return Jwts.parserBuilder()
.requireAudience(aud)
.requireIssuer(iss)
.build()
.parseClaimsJwt(getParseUnsignedToken(token));
} catch (ExpiredJwtException e) {
throw ExpiredTokenException.EXCEPTION;
} catch (Exception e) {
throw InvalidTokenException.EXCEPTION;
}
}
// JwtTokenProvider 에서
public OIDCDto getOIDCTokenBody(String token, String modulus, String exponent) {
Claims body = getOIDCTokenJws(token, modulus, exponent).getBody();
return new OIDCDto(
body.getIssuer(),
body.getAudience(),
body.getSubject(),
body.get("email", String.class));
}
카카오 로그인은 kid만 같은 공개키를 써야하지만,
애플 로그인은 kid와 알고리즘 방식까지 같아야 하더라...
이렇게 검증에 필요한 공개키를 구한 후에,
서버 내에서 공개 키를 다시 만들어야 한다.
공개 키를 만드는 부분은 카카오 로그인 구현 시 사용했던 (위 JwtTokenProvider의 getOIDCTokenJws 참고)
이미 있던 코드를 이용했으므로 참고해주시길..
이렇게 OIDCDto(iss, aud, sub, email을 필드로 가지는 DTO)를 만들어내고,
이를 이용하여 서비스의 상황에 맞게 회원가입을 시키면 된다.
iss : 발급한 곳이 apple이라면 ok
aud : 서비스의 App Key라면 ok
sub : 사용자의 고유번호
email : 사용자의 email
이로써 IdToken이 안전하게 인증되었음 (애플에서 제공하는 공개키로 디코딩이 되는 점에서 입증) 과
TIFY에서 요청했음 (aud에 TIFY App Key가 들어감) 이 입증된다.
'🎉 프로젝트 > 🎁 TIFY' 카테고리의 다른 글
[TIFY] 검색 기능 성능 개선 with DB index (2) | 2024.01.28 |
---|---|
[TIFY] AWS S3 생성 및 Presigned URL 도입 (0) | 2024.01.15 |
[TIFY] Selenium을 이용한 올리브영 크롤링 in SpringBoot (0) | 2023.08.23 |
[TIFY] Slack WebHook으로 Spring 500에러 알림 받기 (0) | 2023.07.04 |