최근 회사에서 OAuth 로그인 API 5종을 작업했다.
대충 후기를 남기면..
Kakao
, Naver
, Google
은 표준 방식과 유사하고 문서도 친절해서 적용하기 어렵지 않았다. (+ 개인적으로 진행해봤던 GitHub
도 마찬가지)
Facebook
은 구성이 어렵지는 않은데 문서 관리가 안된다는 느낌을 받았다. 항목을 찾기도 별로였고 깔끔하게 정리됐다는 느낌을 받기 힘들었다.
Apple
때문에 이글(독수리 아님ㅎ)을 쓰게 됐는데 방식이 다른 OAuth 와 달리 복잡했다. 정리가 잘 된 블로그들도 있지만 내가 잊지 않기 위해서 글을 적게 됐다. (조만간 또 작업을 해야해서..)
이전에 썼던 글에서 재탕인 사진인데 원래 플로우라면 (웹 기준으로)
authorzation_code
응답받음authorization_code
로 해당 플랫폼에 access_token
요청access_token
으로 사용자 프로필 요청까지가 authroziation_server 라고 표기되어 있는 플랫폼과 상호 작용을 하고 이후로는 구현에 따라.. 뭐 프로필을 가지고 제공하는 서비스 내부에서 사용자 특정을 한다던가 하지만, 애플은 좀 달랐따.
authorization_code
로 해당 플랫폼에 access_token
요청에서 id_token
을 함께 돌려주며 이걸 해석하면 그 안에 apple 에서 유저에게 부여해주는 unique key 와 email 등의 정보가 들어있었다. access_token
요청하는 데이터 값도 좀 달랐음
보통 access_token
을 요청 할 때는 grant_type
, code
, redirect_uri
와 함께 요구사항에 따라 클라이언트의 고유 id
값이나 secret_key
를 요구한다.
애플에서는 client_id
, client_secret
을 요구했다.
근데 그 client_secret
값이 일반적으로 앱 등록했을 때 주는 키값이 아니라 앱 등록 시 주는 암호화 키로 형식에 맞춰서 생성한 JWT
(완전 tmi인데 JWT 토큰이라고 하면 JSON Web Token Token 이라 주의해야함)을 의미한다.
private String createClientSecretToken() {
// 암호화 키 파일에서 값 가져오기 코드
// 나도 남의 코드를 긁어온 부분이라 가져오기 좀 그래서 생략.. 출처에 넣을 생각임
PrivateKey privateKey = getAppleIdPrivateKey();
// JWT 클레임 설정
long currentTimeMillis = System.currentTimeMillis();
Date now = new Date(currentTimeMillis);
Date expiration = new Date(currentTimeMillis + 3600000); // 1 hour
// JWT 생성
return Jwts.builder()
.header()
.keyId(clientSecret)
.setAlgorithm(SignatureAlgorithm.ES256.toString())
.and()
.issuer(teamId)
.issuedAt(now)
.expiration(expiration)
.audience()
.add("https://appleid.apple.com")
.and()
.subject(clientId)
.signWith(privateKey)
.compact();
}
요로코롬 만든 JWT
가 필요하다. jjwt
라이브러리를 썼고 기존에 블로그들에 있는 내용들이 deprecated 가 많아서.. 그대로 안가져오고 조금 수정했다. intellIJ 가 노란줄 뿜는게 좀 신경쓰였음.. 근데 지금 보니 setAlgorithm
도 deprecated 인데 미처 수정을 못했다.
무튼 이렇게 하면 access_token
과 함께 id_tokoen
을 준다.
근데 이 id_token
을 해석하려면 apple 에서 제공하는 공개 키 값이 필요하다.
이 공개 키 값은 API 를 쏘면 3개를 주는데 id_token
에 쓰인 값이 거기서 뭔지 특정해서 검증에 사용해야함...ㅎㅎㅎ
대강 작성한 코드를 아래 첨부한다. 더 필요한 내용들은 (예외 처리 등) 알아서 추가 해야함!
private Claims verifyAppleIdToken(String idToken) {
AppleAuthKeysResponseDTO appleAuthKeys = webClient.get()
.uri(requestAuthKeysUri)
.retrieve()
.bodyToMono(AppleAuthKeysResponseDTO.class)
.block()
try {
String headerOfIdentityToken = idToken.substring(0, idToken.indexOf("."));
Decoder urlDecoder = java.util.Base64.getUrlDecoder();
Map<String, String> header = objectMapper.readValue(new String(urlDecoder.decode(headerOfIdentityToken), Charsets.UTF_8), Map.class);
AppleAuthKeysResponseDTO.AppleAuthKey key = appleAuthKeys.getMatchedKey(header.get("kid"), header.get("alg"))
.orElseThrow(() -> ...);
byte[] nBytes = urlDecoder.decode(key.n());
byte[] eBytes = urlDecoder.decode(key.e());
BigInteger n = new BigInteger(1, nBytes);
BigInteger e = new BigInteger(1, eBytes);
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
KeyFactory keyFactory = KeyFactory.getInstance(key.kty());
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
JwtParser jwtParser = Jwts.parser()
.verifyWith(publicKey)
.build();
return jwtParser
.parseSignedClaims(idToken)
.getPayload();
} catch (Exception e) {
...
}
}
00시까지 20초 남아서 일단 끝!