From d7710807c5705d6dc2781e2c1de36290739d952c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8C=80=ED=98=84?= Date: Wed, 25 Oct 2023 00:59:36 +0900 Subject: [PATCH] feat: Apple Login --- build.gradle | 1 + .../member/auth/AppleAuth.java | 93 +++++++++++++++++++ .../member/auth/appleDto/KeyInfo.java | 29 ++++++ .../member/auth/appleDto/Keys.java | 12 +++ .../member/controller/MemberController.java | 5 + .../member/dto/AppleAuthDto.java | 19 ++++ .../member/service/MemberService.java | 2 + .../member/service/MemberServiceImpl.java | 11 ++- 8 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/suite/suite_user_service/member/auth/AppleAuth.java create mode 100644 src/main/java/com/suite/suite_user_service/member/auth/appleDto/KeyInfo.java create mode 100644 src/main/java/com/suite/suite_user_service/member/auth/appleDto/Keys.java create mode 100644 src/main/java/com/suite/suite_user_service/member/dto/AppleAuthDto.java diff --git a/build.gradle b/build.gradle index ba3b65e..a12f046 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ dependencies { //security && JWT implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation group: 'com.auth0', name: 'java-jwt', version: '3.4.0' //actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' diff --git a/src/main/java/com/suite/suite_user_service/member/auth/AppleAuth.java b/src/main/java/com/suite/suite_user_service/member/auth/AppleAuth.java new file mode 100644 index 0000000..6dc91a1 --- /dev/null +++ b/src/main/java/com/suite/suite_user_service/member/auth/AppleAuth.java @@ -0,0 +1,93 @@ +package com.suite.suite_user_service.member.auth; + +import com.suite.suite_user_service.member.auth.appleDto.KeyInfo; +import com.suite.suite_user_service.member.auth.appleDto.Keys; +import com.suite.suite_user_service.member.dto.ReqSignInMemberDto; +import com.suite.suite_user_service.member.handler.CustomException; +import com.suite.suite_user_service.member.handler.StatusCode; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +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.HashMap; +import java.util.Map; + +@Component +public class AppleAuth { + public static final String APPLE_KEY = "https://appleid.apple.com/auth/keys"; + public static final int HEADER = 0; + public static final int PAYLOAD = 1; + public static final int SIGNATURE = 2; + public static String KID = "kid"; + public static String ALG = "alg"; + public static String ALGORITHM = "RSA"; + + public ReqSignInMemberDto getAppleMemberInfo(String identityToken) { + try { + RestTemplate restTemplate = new RestTemplate(); + Keys keys = restTemplate.getForEntity(APPLE_KEY, Keys.class).getBody(); + + Map headerKey = getTokenHeaderInfo(identityToken); + + KeyInfo keyInfo = keys.getKeys().stream().filter( + key -> key.validateKey(headerKey.get(KID), headerKey.get(ALG))).findFirst().orElseThrow( ()-> new CustomException(StatusCode.FORBIDDEN)); + + PublicKey publicKey = getPublicKey(keyInfo); + Claims memberInfo = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(identityToken).getBody(); + JSONObject claims = claimsToJSONObject(memberInfo); + + return ReqSignInMemberDto.builder() + .email(claims.get("email").toString()) + .password(claims.get("sub").toString()) + .isOauth(true).build(); + } catch (ParseException e) { + throw new CustomException(StatusCode.NOT_FOUND); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new CustomException(StatusCode.FAILED_SIGNUP); + } + } + + private Map getTokenHeaderInfo(String identityToken) throws ParseException { + String[] decodeToken = identityToken.split("\\."); + String headerInfo = new String(Base64.getDecoder().decode(decodeToken[HEADER])); + JSONParser parser = new JSONParser(); + JSONObject keyObject = (JSONObject) parser.parse(headerInfo); + + Map map = new HashMap<>(); + map.put(KID, keyObject.get(KID).toString()); + map.put(ALG, keyObject.get(ALG).toString()); + + return map; + } + + private PublicKey getPublicKey(KeyInfo keyInfo) throws NoSuchAlgorithmException, InvalidKeySpecException { +// byte[] nBytes = Base64.getUrlDecoder().decode(keyInfo.getN().substring(1, keyInfo.getN().length() - 1)); +// byte[] eBytes = Base64.getUrlDecoder().decode(keyInfo.getE().substring(1, keyInfo.getE().length() - 1)); + byte[] nBytes = Base64.getUrlDecoder().decode(keyInfo.getN()); + byte[] eBytes = Base64.getUrlDecoder().decode(keyInfo.getE()); + BigInteger n = new BigInteger(1, nBytes); + BigInteger e = new BigInteger(1, eBytes); + + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); + return keyFactory.generatePublic(publicKeySpec); + } + + private JSONObject claimsToJSONObject(Claims claims) { + JSONObject jsonObject = new JSONObject(); + jsonObject.putAll(claims); + return jsonObject; + } + +} diff --git a/src/main/java/com/suite/suite_user_service/member/auth/appleDto/KeyInfo.java b/src/main/java/com/suite/suite_user_service/member/auth/appleDto/KeyInfo.java new file mode 100644 index 0000000..85ca7e5 --- /dev/null +++ b/src/main/java/com/suite/suite_user_service/member/auth/appleDto/KeyInfo.java @@ -0,0 +1,29 @@ +package com.suite.suite_user_service.member.auth.appleDto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KeyInfo { + private String kty; + private String kid; + private String use; + private String alg; + private String n; + private String e; + + public KeyInfo(String kty, String kid, String use, String alg, String n, String e) { + this.kty = kty; + this.kid = kid; + this.use = use; + this.alg = alg; + this.n = n; + this.e = e; + } + + public boolean validateKey(String kid, String alg) { + if(this.kid.equals(kid) && this.alg.equals(alg)) return true; + return false; + } +} diff --git a/src/main/java/com/suite/suite_user_service/member/auth/appleDto/Keys.java b/src/main/java/com/suite/suite_user_service/member/auth/appleDto/Keys.java new file mode 100644 index 0000000..4bf145c --- /dev/null +++ b/src/main/java/com/suite/suite_user_service/member/auth/appleDto/Keys.java @@ -0,0 +1,12 @@ +package com.suite.suite_user_service.member.auth.appleDto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class Keys { + private List keys; +} diff --git a/src/main/java/com/suite/suite_user_service/member/controller/MemberController.java b/src/main/java/com/suite/suite_user_service/member/controller/MemberController.java index 56aa981..2e04fd3 100644 --- a/src/main/java/com/suite/suite_user_service/member/controller/MemberController.java +++ b/src/main/java/com/suite/suite_user_service/member/controller/MemberController.java @@ -69,6 +69,11 @@ public ResponseEntity loginAuthSuite(@RequestBody Map t return ResponseEntity.ok(memberService.getOauthSuiteToken(token.get("access_token"), userAgent, passwordEncoder)); } + @PostMapping("/auth/apple/signin") + public ResponseEntity loginAppleAuthSuite(@RequestBody Map token, @RequestHeader("User-Agent") String userAgent) { + return ResponseEntity.ok(memberService.getAppleOauthSuiteToken(token.get("access_token"), userAgent, passwordEncoder)); + } + @PostMapping("/id") public ResponseEntity findSuiteId(@RequestBody Map inputPhoneByMember) { return ResponseEntity.ok(new Message(StatusCode.OK, memberService.lookupEmailByPhoneNumber(inputPhoneByMember.get("phoneNumber")))); diff --git a/src/main/java/com/suite/suite_user_service/member/dto/AppleAuthDto.java b/src/main/java/com/suite/suite_user_service/member/dto/AppleAuthDto.java new file mode 100644 index 0000000..6d91e2b --- /dev/null +++ b/src/main/java/com/suite/suite_user_service/member/dto/AppleAuthDto.java @@ -0,0 +1,19 @@ +package com.suite.suite_user_service.member.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AppleAuthDto { + private String iss; + private String aud; + private String exp; + private String iat; + private String sub; + private String email; + private String email_verified; + private String auth_time; + private boolean nonce_supported; + +} diff --git a/src/main/java/com/suite/suite_user_service/member/service/MemberService.java b/src/main/java/com/suite/suite_user_service/member/service/MemberService.java index 4935ea7..cb1c1ee 100644 --- a/src/main/java/com/suite/suite_user_service/member/service/MemberService.java +++ b/src/main/java/com/suite/suite_user_service/member/service/MemberService.java @@ -12,6 +12,8 @@ public interface MemberService { Token getSuiteToken(ReqSignInMemberDto reqSignInMemberDto, String userAgent, PasswordEncoder passwordEncoder); Message getOauthSuiteToken(String accessToken, String userAgent, PasswordEncoder passwordEncoder); + + Message getAppleOauthSuiteToken(String accessToken, String userAgent, PasswordEncoder passwordEncoder); Map saveMemberInfo(ReqSignUpMemberDto reqSignUpMemberDto); void uploadImageS3(Long memberId, MultipartFile file); ResMemberInfoDto getMemberInfo(AuthorizerDto authorizerDto); diff --git a/src/main/java/com/suite/suite_user_service/member/service/MemberServiceImpl.java b/src/main/java/com/suite/suite_user_service/member/service/MemberServiceImpl.java index d56f082..8f4d456 100644 --- a/src/main/java/com/suite/suite_user_service/member/service/MemberServiceImpl.java +++ b/src/main/java/com/suite/suite_user_service/member/service/MemberServiceImpl.java @@ -4,6 +4,7 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.ObjectMetadata; +import com.suite.suite_user_service.member.auth.AppleAuth; import com.suite.suite_user_service.member.auth.GoogleAuth; import com.suite.suite_user_service.member.dto.*; @@ -49,6 +50,7 @@ public class MemberServiceImpl implements MemberService { private final MemberInfoRepository memberInfoRepository; private final JwtCreator jwtCreator; private final GoogleAuth googleAuth; + private final AppleAuth appleAuth; private final AmazonS3 amazonS3; private final SnsClient snsClient; private final SuiteUserProducer suiteUserProducer; @@ -77,13 +79,20 @@ else if(member.getAccountStatus().equals(AccountStatus.DISABLED.getStatus())) } @Override - public Message getOauthSuiteToken( String accessToken, String userAgent, PasswordEncoder passwordEncoder) { + public Message getOauthSuiteToken(String accessToken, String userAgent, PasswordEncoder passwordEncoder) { ReqSignInMemberDto reqSignInMemberDto = googleAuth.getGoogleMemberInfo(accessToken); Optional token = memberRepository.findByEmail(reqSignInMemberDto.getEmail()).map(member -> verifyOauthAccount(reqSignInMemberDto, userAgent, passwordEncoder)); return token.map(suiteToken -> new Message(StatusCode.OK, suiteToken)).orElseGet(() -> new Message(StatusCode.CREATED, reqSignInMemberDto)); } + public Message getAppleOauthSuiteToken(String accessToken, String userAgent, PasswordEncoder passwordEncoder) { + ReqSignInMemberDto reqSignInMemberDto = appleAuth.getAppleMemberInfo(accessToken); + + Optional token = memberRepository.findByEmail(reqSignInMemberDto.getEmail()).map(member -> verifyOauthAccount(reqSignInMemberDto, userAgent, passwordEncoder)); + return token.map(suiteToken -> new Message(StatusCode.OK, suiteToken)).orElseGet(() -> new Message(StatusCode.CREATED, reqSignInMemberDto)); + } + @Override @Transactional public Map saveMemberInfo(ReqSignUpMemberDto reqSignUpMemberDto) {