Skip to content

Commit

Permalink
merge: pull request #55 from SOPT-all/feat/#54
Browse files Browse the repository at this point in the history
[FEAT/#54] 애플 로그인 구현
  • Loading branch information
ckkim817 authored Jan 22, 2025
2 parents fd673e3 + 62b32ad commit 4180cb8
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.acon.server.global.auth.jwt;

import static org.apache.commons.codec.binary.Base64.decodeBase64;

import com.acon.server.global.exception.BusinessException;
import com.acon.server.global.exception.ErrorType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
Expand All @@ -8,9 +13,12 @@
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -107,4 +115,22 @@ public Long getMemberIdFromJwt(String token) {

return Long.valueOf(claims.get(MEMBER_ID).toString());
}

public Map<String, String> parseHeaders(String token) {
String header = token.split("\\.")[0];

try {
return new ObjectMapper().readValue(decodeBase64(header), Map.class);
} catch (IOException e) {
throw new BusinessException(ErrorType.INTERNAL_SERVER_ERROR);
}
}

public Claims getTokenClaims(String token, PublicKey publicKey) {
return Jwts.parserBuilder()
.setSigningKey(publicKey)
.build()
.parseClaimsJws(token)
.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package com.acon.server.global.config;

import com.acon.server.ServerApplication;
import feign.Retryer;
import feign.Retryer.Default;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients(basePackageClasses = ServerApplication.class)
public class OpenFeignConfig {

@Bean
public Retryer retryer() {
return new Default(1000, 1500, 2);
}
// TODO: 타임아웃 설정 추가, 서킷 브레이커 적용하기
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.acon.server.member.infra.entity.MemberEntity;
import com.acon.server.member.infra.entity.VerifiedAreaEntity;
import com.acon.server.member.infra.external.google.GoogleSocialService;
import com.acon.server.member.infra.external.ios.AppleAuthAdapter;
import com.acon.server.member.infra.repository.GuidedSpotRepository;
import com.acon.server.member.infra.repository.MemberRepository;
import com.acon.server.member.infra.repository.PreferenceRepository;
Expand Down Expand Up @@ -55,6 +56,7 @@ public class MemberService {
private final JwtTokenProvider jwtTokenProvider;
private final PrincipalHandler principalHandler;
private final GoogleSocialService googleSocialService;
private final AppleAuthAdapter appleAuthService;

private final NaverMapsAdapter naverMapsAdapter;

Expand All @@ -66,7 +68,16 @@ public LoginResponse login(
final SocialType socialType,
final String idToken
) {
String socialId = googleSocialService.login(idToken);
String socialId;

if (socialType == SocialType.GOOGLE) {
socialId = googleSocialService.login(idToken);
} else if (socialType == SocialType.APPLE) {
socialId = appleAuthService.getAppleAccountId(idToken);
} else {
throw new BusinessException(ErrorType.INVALID_SOCIAL_TYPE_ERROR);
}

Long memberId = fetchMemberId(socialType, socialId);
MemberAuthentication memberAuthentication = new MemberAuthentication(memberId, null, null);
String accessToken = jwtTokenProvider.issueAccessToken(memberAuthentication);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public String login(String unverifiedIdToken) {
private GoogleIdToken verifyIdToken(String unverifiedIdToken) {
try {
GoogleIdToken idToken = verifier.verify(unverifiedIdToken);

if (idToken == null) {
throw new BusinessException(ErrorType.INVALID_ID_TOKEN_ERROR);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.acon.server.member.infra.external.ios;

import com.acon.server.global.auth.jwt.JwtTokenProvider;
import java.security.PublicKey;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AppleAuthAdapter {

private final AppleAuthClient appleAuthClient;
private final ApplePublicKeyGenerator applePublicKeyGenerator;
private final JwtTokenProvider jwtTokenProvider;

public String getAppleAccountId(String identityToken) {
Map<String, String> headers = jwtTokenProvider.parseHeaders(identityToken);
PublicKey publicKey =
applePublicKeyGenerator.generatePublicKey(headers, appleAuthClient.getAppleAuthPublicKey());

return jwtTokenProvider.getTokenClaims(identityToken, publicKey).getSubject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.acon.server.member.infra.external.ios;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "appleAuthClient", url = "${apple.auth.public-key-url}")
public interface AppleAuthClient {

@GetMapping
ApplePublicKeyResponse getAppleAuthPublicKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.acon.server.member.infra.external.ios;

public record ApplePublicKey(
String kty,
String kid,
String alg,
String n,
String e
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.acon.server.member.infra.external.ios;

import static org.apache.commons.codec.binary.Base64.decodeBase64;

import com.acon.server.global.exception.BusinessException;
import com.acon.server.global.exception.ErrorType;
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.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ApplePublicKeyGenerator {

public PublicKey generatePublicKey(Map<String, String> tokenHeaders, ApplePublicKeyResponse applePublicKeys) {
ApplePublicKey publicKey = applePublicKeys.getMatchedKey(tokenHeaders.get("kid"), tokenHeaders.get("alg"));

return getPublicKey(publicKey);
}

private PublicKey getPublicKey(ApplePublicKey publicKey) {
byte[] nBytes = decodeBase64(publicKey.n());
byte[] eBytes = decodeBase64(publicKey.e());

RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(new BigInteger(1, nBytes), new BigInteger(1, eBytes));

try {
return KeyFactory.getInstance(publicKey.kty()).generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new BusinessException(ErrorType.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.acon.server.member.infra.external.ios;

import com.acon.server.global.exception.BusinessException;
import com.acon.server.global.exception.ErrorType;
import java.util.List;

public record ApplePublicKeyResponse(
List<ApplePublicKey> keys
) {

public ApplePublicKey getMatchedKey(String kid, String alg) {
return keys.stream()
.filter(key -> key.kid().equals(kid) && key.alg().equals(alg))
.findAny()
.orElseThrow(() -> new BusinessException(ErrorType.INTERNAL_SERVER_ERROR));
}
}

0 comments on commit 4180cb8

Please sign in to comment.