๋์๋ฆฌ๋ฅผ ํตํด ์งํํ์๋ ์น๋ทฐ ํํ์ ์ฑ ์๋น์ค ํ๋ก์ ํธ์์ OAuth 2.0 ํ์ฉํ ์๋ ๋ก๊ทธ์ธ์ ๊ตฌํํ์๋ค. ์นด์นด์ค์ ์ ํ ๋ชจ๋ REST ๋ฐฉ์์ผ๋ก ๊ตฌํํ ๋ค, ์๋น์ค ์์ฒด JWT ํ ํฐ์ ํตํด ์ ์ ์ธ์ฆ ๋ก์ง์ ๊ตฌํํ์๊ณ ์ด ๊ณผ์ ์์ Cookie๋ฅผ ํ์ฉํด ์๋ ๋ก๊ทธ์ธ์ด ๊ฐ๋ฅํ๋๋ก ํ์๋ค.
๊ฐ IdP์ developers ํ์ด์ง์์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๋ค๋ฅธ ๋ธ๋ก๊ทธ์๋ ์์ธํ ์ ์์ฑ๋์ด์๊ธฐ ๋๋ฌธ์ ์๋ตํ๊ณ ์ ํ๋ค. ์๊ฐํ๊ณ ์ ํ๋ ํ๋ก์ ํธ์์๋ ์นด์นด์ค ๋ก๊ทธ์ธ๊ณผ ์ ํ ๋ก๊ทธ์ธ๋ง ์ฌ์ฉํ์์ง๋ง, ์ถ๊ฐ๋ก ์นด์นด์ค ๋ก๊ทธ์ธ๊ณผ ๋งค์ฐ ์ ์ฌํ ๋ค์ด๋ฒ ๋ก๊ทธ์ธ๊น์ง ์๊ฐํ๋ ค๊ณ ํ๋ค.
ํ๋ก ํธ์์ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ์ ์นด์นด์ค ์๋ฒ์๊ฒ ์ธ๊ฐ์ฝ๋๋ฅผ ์์ฒญํ๋ค. ์นด์นด์ค ์๋ฒ๋ ์ธ๊ฐ์ฝ๋๋ฅผ redirect_uri๋ก ๋ฐํํด์ฃผ๊ณ , ๊ทธ ํํ๋ ๋ค์๊ณผ ๊ฐ๋ค. ์๋๋ ๋ง์๋๋ก ์์ฑํ ์์ ์ฝ๋๋ค.
"~~/callback?code=kSc9f-ZZ0asdfgafjai4t-D05Tl9zYgas-dfQasdfas3-diw"
์ด ์ฝ๋๋ฅผ ๋ฐฑ์๋๋ก ๋๊ฒจ์ฃผ๋ฉด ๋๋ค.
๋ ๋ฒ์ ์์ฒญ์ด ์ค๊ณ ๊ฐ๋ค.
"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id="
+ KAKAO_KEY + "&redirect_uri=" + KAKAO_URI + "&code="
+ kakaoRequest.getCode()
๋จผ์ ํ๋ก ํธ๋ก๋ถํฐ ๋๊ฒจ ๋ฐ์ ์ธ๊ฐ ์ฝ๋๋ฅผ ์์ ์ฃผ์์ POST ์์ฒญํ์ฌ ์ธ๊ฐ์ฝ๋์ ์ ํจ์ฑ์ ํ์ธ ๋ฐ๊ณ , ๊ทธ ๋๊ฐ๋ก ์นด์นด์ค ์๋ฒ์ Access Token๊ณผ Refresh Token์ ๋๋ ค ๋ฐ๋๋ค. ์ด ํ ํฐ๋ค์ ํ๋ก ํธ๋ก ๋๊ฒจ์ ธ์๋ ์๋๋ ์ค์ํ ์ ๋ณด๋ค. ์ฌ๋ฌ๋ฒ ์คํํด๋ณธ ๊ฒฐ๊ณผ ๋ง๋ฃ๊ธฐ๊ฐ์ด ๊ต์ฅํ ๊ธด ๋ฏ ๋ณด์๊ณ , ๋ค์ ์์ฒญ์๋ ํ๋ก์ ํธ ๊ณ ์ ์ ๋น๋ฐ ํค ๋ฑ์ด ์๊ตฌ๋์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
"https://kapi.kakao.com/v2/user/me"
๋ ๋ฒ์งธ๋ก ์์์ ๋ฐ์ ์นด์นด์ค ์๋ฒ์ Access Token๊ณผ Refresh Token๊ณผ ํจ๊ป ํด๋น ์ฃผ์์ POST ์์ฒญ์ ๋ ๋ ค์ผ ํ๋ค.
์ด ๋, ์นด์นด์ค๋ก๋ถํฐ ์๋์ ๊ฐ์ ํํ์ ์ ์ ์ ๋ณด๋ฅผ ๋ฐ๊ฒ ๋๋ค.
{
"authenticationCode": "",
"connectedAt": "",
"kakaoAccount": {
"profile": {
"nickname": ""
}
}
}
์์ ๋ฆฌ์คํฐ์ค์์ authenticationCode๊ฐ uniqueํ๋ฏ๋ก ์ ์ ๋ฅผ ์๋ณํ๋ ์ฉ๋๋ก ์ฌ์ฉํ ์ ์๋ค.
์นด์นด์ค ๋ก๊ทธ์ธ๊ณผ ๋งค์ฐ ์ ์ฌํ๋ค.
๋ง์ฐฌ๊ฐ์ง๋ก ํ๋ก ํธ์์ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํจ์ ๋ฐ๋ผ ๋ค์ด๋ฒ ์๋ฒ์์ ๋๋ ค ๋ฐ์ ์ธ๊ฐ์ฝ๋์ state๋ผ๋ ํ๋ก์ ํธ์์ ์ง์ ํ String ๊ฐ์ ํจ๊ป ๋ฐฑ์๋๋ก ๋๊ฒจ์ฃผ๋ฉด ๋๋ค.
๋ ๋ฒ์ ์์ฒญ ์ค ์ฒซ ์์ฒญ์ ์ฃผ์๋ ์๋์ ๊ฐ์ผ๋ฉฐ ๋ง์ฐฌ๊ฐ์ง๋ก POST ์์ฒญํ๋ค.
"https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id="
+ NAVER_KEY + "&client_secret=" + NAVER_SECRET + "&redirect_uri=" + NAVER_URI
+ "&code=" + naverRequest.getCode() + "&state=" + naverRequest.getState()
๋ง์ฐฌ๊ฐ์ง๋ก ๋ค์ด๋ฒ ์๋ฒ์ Access Token๊ณผ Refresh Token์ ๋๋ ค ๋ฐ๊ณ , ์ด๋ฅผ ํ๋ก ํธ์ ์ ๋ฌํ์ง ์๊ณ ๋ ๋ฒ์งธ ์์ฒญ์ ์ฌ์ฉํ๋ค.
"https://openapi.naver.com/v1/nid/me"
์ ์ฃผ์์ ๋ ๋ฒ์งธ POST ์์ฒญ์ ๋ ๋ ค ์๋์ ๊ฐ์ ํํ์ ์ ์ ์ ๋ณด๋ฅผ ๋ฐ๋๋ค.
์ด ๋, ์นด์นด์ค๋ก๋ถํฐ ์๋์ ๊ฐ์ ํํ์ ์ ์ ์ ๋ณด๋ฅผ ๋ฐ๊ฒ ๋๋ค.
{
"resultCode": "",
"message": "",
"response": {
"authenticationCode": "",
"nickname": "",
"imageUrl": ""
}
}
์์ ๋ฆฌ์คํฐ์ค์์ response์ authenticationCode๊ฐ uniqueํ๋ฏ๋ก ์ ์ ๋ฅผ ์๋ณํ๋ ์ฉ๋๋ก ์ฌ์ฉํ ์ ์๋ค.
์ ํ ๋ก๊ทธ์ธ์ ๊ฒฝ์ฐ ์์ ๋ ๋ฐฉ์๊ณผ ์กฐ๊ธ ๋ ๊ธธ๊ณ ๋ณต์กํ๋ค.
๋ก๊ทธ์ธ ์ ๋ฐฑ์๋์ Post API๋ก ๋ฐ๋ก redirect ์ํจ๋ค. ์๋์ ๊ฐ์ด MultiValueMap์ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์๋ค.
@ApiOperation(value = "์ ํ ๋ก๊ทธ์ธ")
@PostMapping(value = "/login", produces = "application/json; charset=utf-8", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public void postAppleLogin(@RequestBody MultiValueMap<String, String> formData, HttpServletResponse response){
...
}
์ค์ ์ ๋ฐ๋ผ ๋๋ ค ๋ฐ์ ์ ์๋ ์ ๋ณด๊ฐ ์์ดํ๋ฐ, ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ๊ฒ ๋๋ ์ ๋ณด๋ ์๋์ ๊ฐ๋ค. ์ด์ค id_token์ผ๋ก ์ ํจ์ฑ ๊ฒ์ฆ์ ํด์ผํ๋ค.
{
"state": "",
"code": "",
"id_token": "",
"user": "{}"
}
"https://appleid.apple.com/auth/keys"
์ ์ฃผ์์ GET ์์ฒญ์ ํตํด ๊ณต๊ฐ ํค๋ฅผ ๋ฐ์ ์จ๋ค. ๊ฐ๋ฐํ๋ ๋น์์๋ ์ธ ๊ฐ์ ํค๋ฅผ ์ ๊ณตํ์๋๋ฐ, ๊ทธ ์ค ์์์ ๋ฐ์ id_token๊ณผ HEADER์ kid์ alg ์์ญ์ ๊ฐ์ด ์ผ์นํ๋ ํค๋ฅผ ์ฐพ๋๋ค. ํด๋น ๊ธฐ๋ฅ์ ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
public AppleSubjectDTO verifyIdentityToken(AppleRequest appleRequest,
AppleKeyListDTO appleKeyListDTO) {
PublicKey publicKey = null;
try {
// String์ JWT ํํ๋ก ๋งคํํ๊ณ ์ ํจ ์๊ฐ์ ๊ฒ์ฆํ๋ค.
SignedJWT signedJWT = SignedJWT.parse(appleRequest.getIdToken());
JWTClaimsSet payload = signedJWT.getJWTClaimsSet();
Date currentTime = new Date(System.currentTimeMillis());
if (!currentTime.before(payload.getExpirationTime())) {
throw new BadRequestException(ErrorCode.APPLE_TOKEN_EXPIRED.getErrorCode());
}
// . ๊ธฐ์ค์ผ๋ก IdToken String์ HEADER ๋ถ๋ถ๋ง ๊ฐ์ ธ์จ๋ค.
String headerOfIdentityToken = appleRequest.getIdToken()
.substring(0, appleRequest.getIdToken().indexOf("."));
// ๋ณตํธํ ํ ๋ค kid์ alg๋ก ์ผ์นํ๋ key๋ฅผ ์ฐพ๋๋ค.
AppleHeaderDTO appleHeaderDTO = new ObjectMapper().readValue(
new String(Base64.getDecoder().decode(headerOfIdentityToken), "UTF-8"),
AppleHeaderDTO.class);
AppleKeyDTO appleKeyDTO = appleKeyListDTO.getMatchedKeyBy(
appleHeaderDTO.getKid(),
appleHeaderDTO.getAlg())
.orElseThrow(
() -> new BadRequestException(ErrorCode.APPLE_KEY_UNAVAILABLE.getErrorCode()));
// ๊ตฌํ ๊ฐ์ผ๋ก ์๋ก์ด RSA ๊ณต๊ฐ ํค๋ฅผ ๋ง๋ ๋ค.
byte[] nBytes = Base64.getUrlDecoder().decode(appleKeyDTO.getN());
byte[] eBytes = Base64.getUrlDecoder().decode(appleKeyDTO.getE());
BigInteger n = new BigInteger(1, nBytes);
BigInteger e = new BigInteger(1, eBytes);
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
KeyFactory keyFactory = KeyFactory.getInstance(appleKeyDTO.getKty());
publicKey = keyFactory.generatePublic(publicKeySpec);
} catch (ParseException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
} catch (InvalidKeySpecException ex) {
ex.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// ๋ค์ ๋ณตํธํํ์ฌ ์ป์ Subject๋ฅผ ๋ฐํํ๋ค.
Claims claims = Jwts.parser().setSigningKey(publicKey)
.parseClaimsJws(appleRequest.getIdToken())
.getBody();
if (!claims.get("nonce").equals(APPLE_NONCE)) {
throw new BadRequestException(ErrorCode.APPLE_NONCE_INCORRECT.getErrorCode());
}
return new AppleSubjectDTO(claims.getSubject());
}
๋ง์ง๋ง์ ์ป์ Subject๋ ๋ค์๊ณผ ๊ฐ์ AppleSubjectDTO์ ๊ฐ์ธ์ฃผ์์ผ๋ฉฐ, ์ด String ๊ฐ์ด ์ ์ ์๋ณ ์ฉ๋๋ก ์ฌ์ฉํ ์ ์๋ authenticationCode์ ํด๋นํ๋ค.
public class AppleSubjectDTO {
private String authenticationCode;
}
์ฌ๊ธฐ๊น์ง๋ง ์งํํด๋ ๋ก๊ทธ์ธ ์์ฒด๋ ๊ตฌํํ ์ ์์ง๋ง, ์ฑ ์ถ์๋ฅผ ์ํด์๋ ์ผ๋ จ์ ๊ณผ์ ์ด ๋ ํ์ํ๋ค. 2022๋ ๋ถํฐ ์ฑ์คํ ์ด ์ถ์๋ฅผ ์ํด์๋ ์ฌ์ฉ์๊ฐ ์๋น์ค ํํดํ ๋ DB์์๋ง ์ญ์ ํ ๊ฒ์ด ์๋๋ผ ์ ํ์๋ ํด๋น ๊ณ์ ์ด ํํดํ๋ ๋ก์ง์ ๊ตฌํํด์ผ ํ๋ค.
"https://appleid.apple.com/auth/token?client_id=" + APPLE_CLIENT_ID + "&client_secret="
+ APPLE_SECRET + "&grant_type=authorization_code&code=" + appleRequest.getCode()
+ "&redirect_uri=" + APPLE_URI + option
Client Secret์ ๋ง๋ค๊ณ ์์ ์ฃผ์์ POST ์์ฒญ์ ๋ ๋ฆฌ๋ฉด ์ ํ ๊ณ ์ ์ Access Token๊ณผ Refresh Token์ ์ ๊ณตํ๋ค. ํด๋น ๊ณผ์ ์ด ๋ณต์กํ๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๊ธ์์ ํํด์ ๋ํด ๋ฐ๋ก ๋ค๋ฃจ์ด ๋ณด๋ ค๊ณ ํ๋ค. ์๋ฌด์ชผ๋ก ์ด ๋ ์ป์ Refresh Token๊ณผ Client Secret์ ํ์ฉํด "https://appleid.apple.com/auth/revoke"
์ POST ์์ฒญํ์ฌ ํํดํ ์ ์๋ค.
OAuth 2.0 ๋ก๊ทธ์ธ ํ ์์ฒด ์๋น์ค JWT Token์ ํ์ฉํ ์๋ ๋ก๊ทธ์ธ ๊ตฌํ์ ๋ค์ ํฌ์คํธ์ ์์ฑํ๋ ค๊ณ ํ๋ค.