Skip to content

Commit

Permalink
Merge pull request #65 from Team-UMC/feature/#62/login_apple
Browse files Browse the repository at this point in the history
Feature/#62/login apple
  • Loading branch information
junseokkim authored Feb 8, 2024
2 parents bf5905c + 5e18466 commit 7783f24
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,30 +1,106 @@
package com.umc.networkingService.domain.member.client;

import com.umc.networkingService.domain.member.dto.client.AppleResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.umc.networkingService.domain.member.dto.client.ApplePublicKeyResponse;
import com.umc.networkingService.global.common.exception.RestApiException;
import com.umc.networkingService.global.common.exception.code.AuthErrorCode;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import java.util.Map;

@Component
public class AppleMemberClient {
private WebClient webClient;

public AppleMemberClient(WebClient.Builder webclientBuilder){
this.webClient = webclientBuilder
.baseUrl("https://appleid.apple.com/auth/keys")
.baseUrl("https://appleid.apple.com/auth")
.build();
}

public String getappleClientID(final String accessToken){
AppleResponse response = webClient.get()
.header("Authorization", "Bearer " + accessToken)
public String getappleClientID(final String accessToken) {
Claims claims = getClaimsBy(accessToken);
validateClaims(claims);

return claims.getSubject();
}

/*
* μ• ν”Œ 검증 단계 (https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user)
* 1. μ„œλ²„μ˜ 곡개 ν‚€λ₯Ό μ‚¬μš©ν•˜μ—¬ JWS E256 μ„œλͺ…을 검증
* 2. 인증에 λŒ€ν•œ nonceλ₯Ό 검증
* 3. iss ν•„λ“œκ°€ https://appleid.apple.com을 ν¬ν•¨ν•˜λŠ”μ§€ 확인
* 4. aud ν•„λ“œκ°€ 개발자의 ν΄λΌμ΄μ–ΈνŠΈ IDλ₯Ό ν¬ν•¨ν•˜λŠ”μ§€ 확인
* 5. ν˜„μž¬ μ‹œκ°„μ„ κΈ°μ€€μœΌλ‘œ exp ν•„λ“œκ°€ μœ νš¨ν•œμ§€ 확인
* */

//1. Apple의 κ³΅κ°œν‚€λ₯Ό μ‚¬μš©ν•˜μ—¬ identityToken을 검증

//InvalidKeySpecException, NoSuchAlgorithmException, UnsupportedEncodingException, JsonProcessingException
public Claims getClaimsBy(String identityToken) {

try {
ApplePublicKeyResponse response = getAppleAuthPublicKey(); //κ³΅κ°œν‚€ κ°€μ Έμ˜€κΈ°

//곡개 ν‚€λ₯Ό μ‚¬μš©ν•˜μ—¬ identityToken을 검증
String headerOfIdentityToken = identityToken.substring(0, identityToken.indexOf("."));
Map<String, String> header = new ObjectMapper().readValue(new String(Base64.getDecoder().decode(headerOfIdentityToken), "UTF-8"), Map.class);
ApplePublicKeyResponse.Key key = response.getMatchedKeyBy(header.get("kid"), header.get("alg"))
.orElseThrow(() -> new RestApiException(AuthErrorCode.FAILED_GET_APPLE_KEY)); //κ³΅κ°œν‚€λ₯Ό κ°€μ Έμ˜€λŠ”λ° μ‹€νŒ¨

byte[] nBytes = Base64.getUrlDecoder().decode(key.getN());
byte[] eBytes = Base64.getUrlDecoder().decode(key.getE());

BigInteger n = new BigInteger(1, nBytes);
BigInteger e = new BigInteger(1, eBytes);

RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
KeyFactory keyFactory = KeyFactory.getInstance(key.getKty());
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(identityToken).getBody();
}catch (InvalidKeySpecException | NoSuchAlgorithmException | UnsupportedEncodingException | JsonProcessingException e) {
throw new RestApiException(AuthErrorCode.FAILED_GET_APPLE_KEY);
}
}

public ApplePublicKeyResponse getAppleAuthPublicKey() { //Apple의 κ³΅κ°œν‚€λ₯Ό κ°€μ Έμ˜€κΈ°
return webClient.get()
.uri("/keys")
.retrieve()
.bodyToMono(AppleResponse.class)
.bodyToMono(ApplePublicKeyResponse.class)
.block();
}

//2. 인증에 λŒ€ν•œ nonceλ₯Ό 검증
//3. iss ν•„λ“œκ°€ https://appleid.apple.com을 ν¬ν•¨ν•˜λŠ”μ§€ 확인
//4. aud ν•„λ“œκ°€ 개발자의 ν΄λΌμ΄μ–ΈνŠΈ IDλ₯Ό ν¬ν•¨ν•˜λŠ”μ§€ 확인
//5. ν˜„μž¬ μ‹œκ°„μ„ κΈ°μ€€μœΌλ‘œ exp ν•„λ“œκ°€ μœ νš¨ν•œμ§€ 확인
public void validateClaims(Claims claims) {
if (claims == null){
throw new RestApiException(AuthErrorCode.FAILED_SOCIAL_LOGIN);
}

//TODO 정보 λ°›κΈ° μ‹€νŒ¨ μ˜ˆμ™Έ 처리
if(response == null)
return null;
if (!claims.getIssuer().contains("https://appleid.apple.com"))
throw new RestApiException(AuthErrorCode.INVALID_APPLE_ID_TOKEN);

return response.getSub();
//if (!claims.getAudience().contains("com.networkingService.umc"))
// throw new RestApiException(AuthErrorCode.INVALID_APPLE_ID_TOKEN);

if (claims.getExpiration().before(new java.util.Date()))
throw new RestApiException(AuthErrorCode.INVALID_APPLE_ID_TOKEN);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.umc.networkingService.domain.member.dto.client;

import lombok.Getter;
import lombok.Setter;

import java.util.List;
import java.util.Optional;

@Getter
@Setter
public class ApplePublicKeyResponse {
private List<Key> keys;

@Getter
@Setter
public static class Key {
private String kty;
private String kid;
private String use;
private String alg;
private String n;
private String e;
}

public Optional<Key> getMatchedKeyBy(String kid, String alg) {
return this.keys.stream()
.filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg))
.findFirst();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public MemberIdResponse withdrawal(Member member) {
private MemberLoginResponse loginByApple(final String accessToken){
// apple μ„œλ²„μ™€ ν†΅μ‹ ν•΄μ„œ μœ μ € κ³ μœ κ°’(clientId) λ°›κΈ°
String clientId = appleMemberClient.getappleClientID(accessToken);

//쑴재 μ—¬λΆ€ νŒŒμ•…
Optional<Member> getMember = memberRepository.findByClientIdAndSocialType(clientId, SocialType.APPLE);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public enum AuthErrorCode implements ErrorCodeInterface {
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH006", "μœ νš¨ν•˜μ§€ μ•Šμ€ REFRESH TOKENμž…λ‹ˆλ‹€."),
FAILED_SOCIAL_LOGIN(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH007", "μ†Œμ…œ λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."),
FAILED_GITHUB_AUTHENTICATION(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH008", "κΉƒν—ˆλΈŒ μ„œλ²„μ™€ 톡신이 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."),
FAILED_GET_APPLE_KEY(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH009", "μ• ν”Œ μ„œλ²„μ™€ 톡신이 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."),
INVALID_APPLE_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH010","μœ νš¨ν•˜μ§€ μ•Šμ€ μ• ν”Œ ID TOKENμž…λ‹ˆλ‹€."),
;

private final HttpStatus httpStatus;
Expand Down

0 comments on commit 7783f24

Please sign in to comment.