From cc1069ae0996f3a12f5f2c0396bdb7790ac4be18 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Tue, 8 Oct 2024 20:46:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=95=A0=ED=94=8C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- build.gradle | 2 + .../controller/auth/AuthController.java | 9 +- .../dongpo/dto/auth/AppleLoginDto.java | 12 ++ .../service/auth/AppleLoginService.java | 187 ++++++++++++++++++ .../resources/application-credentials.yml | 12 ++ 6 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/dongyang/dongpo/dto/auth/AppleLoginDto.java create mode 100644 src/main/java/com/dongyang/dongpo/service/auth/AppleLoginService.java diff --git a/.gitignore b/.gitignore index 0a91608..eb98388 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,6 @@ src/.DS_Store src/main/.DS_Store src/main/resources/.DS_Store -AdminInit.java \ No newline at end of file +AdminInit.java + +/src/main/resources/key/key.p8 diff --git a/build.gradle b/build.gradle index a359f56..672bf96 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.2' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + implementation 'com.nimbusds:nimbus-jose-jwt:3.10' + implementation 'com.googlecode.json-simple:json-simple:1.1.1' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/src/main/java/com/dongyang/dongpo/controller/auth/AuthController.java b/src/main/java/com/dongyang/dongpo/controller/auth/AuthController.java index e0b6cca..52b6fe8 100644 --- a/src/main/java/com/dongyang/dongpo/controller/auth/AuthController.java +++ b/src/main/java/com/dongyang/dongpo/controller/auth/AuthController.java @@ -1,9 +1,11 @@ package com.dongyang.dongpo.controller.auth; import com.dongyang.dongpo.apiresponse.ApiResponse; +import com.dongyang.dongpo.dto.auth.AppleLoginDto; import com.dongyang.dongpo.dto.auth.JwtToken; import com.dongyang.dongpo.dto.auth.JwtTokenReissueDto; import com.dongyang.dongpo.dto.auth.SocialTokenDto; +import com.dongyang.dongpo.service.auth.AppleLoginService; import com.dongyang.dongpo.service.auth.SocialService; import com.dongyang.dongpo.service.token.TokenService; import io.swagger.v3.oas.annotations.Operation; @@ -18,6 +20,7 @@ public class AuthController { private final SocialService socialService; private final TokenService tokenService; + private final AppleLoginService appleLoginService; @PostMapping("/kakao") public ResponseEntity> kakao(@RequestBody SocialTokenDto token) { @@ -34,9 +37,9 @@ public ResponseEntity> naver(@RequestBody SocialTokenDto t return ResponseEntity.ok(new ApiResponse<>(socialService.getNaverUserInfo(token.getToken()))); } - @GetMapping("/apple/callback") - public ResponseEntity callback(){ - return null; + @PostMapping("/apple/callback") + public ResponseEntity> appleCallback(@RequestParam("code") String code) throws Exception { + return ResponseEntity.ok(new ApiResponse<>(appleLoginService.getAppleInfo(code))); } @PostMapping("/reissue") diff --git a/src/main/java/com/dongyang/dongpo/dto/auth/AppleLoginDto.java b/src/main/java/com/dongyang/dongpo/dto/auth/AppleLoginDto.java new file mode 100644 index 0000000..4c98c93 --- /dev/null +++ b/src/main/java/com/dongyang/dongpo/dto/auth/AppleLoginDto.java @@ -0,0 +1,12 @@ +package com.dongyang.dongpo.dto.auth; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class AppleLoginDto { + private String id; + private String token; + private String email; +} diff --git a/src/main/java/com/dongyang/dongpo/service/auth/AppleLoginService.java b/src/main/java/com/dongyang/dongpo/service/auth/AppleLoginService.java new file mode 100644 index 0000000..39d245a --- /dev/null +++ b/src/main/java/com/dongyang/dongpo/service/auth/AppleLoginService.java @@ -0,0 +1,187 @@ +package com.dongyang.dongpo.service.auth; + +import com.dongyang.dongpo.dto.auth.AppleLoginDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import lombok.RequiredArgsConstructor; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.*; +import java.net.URL; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; + +@Service +@RequiredArgsConstructor +public class AppleLoginService { + + @Value("${apple.team.id}") + private String appleTeamId; + + @Value("${apple.login.key}") + private String appleLoginKey; + + @Value("${apple.client.id}") + private String appleClientId; + + @Value("${apple.redirect.uri}") + private String appleRedirectUri; + + @Value("${apple.key.path}") + private String appleKeyPath; + + private final static String appleAuthUrl = "https://appleid.apple.com"; + + public String getAppleLogin() { + return appleAuthUrl + "/auth/authorize" + + "?client_id=" + appleClientId + + "&redirect_uri=" + appleRedirectUri + + "&response_type=code%20id_token&scope=name%20email&response_mode=form_post"; + } + + public AppleLoginDto getAppleInfo(String code) throws Exception { + if (code == null) throw new Exception("Failed get authorization code"); + + String clientSecret = createClientSecret(); + String userId = ""; + String email = ""; + String accessToken = ""; + + try { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-type", "application/x-www-form-urlencoded"); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type" , "authorization_code"); + params.add("client_id" , appleClientId); + params.add("client_secret", clientSecret); + params.add("code" , code); + params.add("redirect_uri" , appleRedirectUri); + + RestTemplate restTemplate = new RestTemplate(); + HttpEntity> httpEntity = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange( + appleAuthUrl + "/auth/token", + HttpMethod.POST, + httpEntity, + String.class + ); + + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody()); + + accessToken = String.valueOf(jsonObj.get("access_token")); + + //ID TOKEN을 통해 회원 고유 식별자 받기 + SignedJWT signedJWT = SignedJWT.parse(String.valueOf(jsonObj.get("id_token"))); + ReadOnlyJWTClaimsSet getPayload = signedJWT.getJWTClaimsSet(); + + ObjectMapper objectMapper = new ObjectMapper(); + JSONObject payload = objectMapper.readValue(getPayload.toJSONObject().toJSONString(), JSONObject.class); + + userId = String.valueOf(payload.get("sub")); + email = String.valueOf(payload.get("email")); + } catch (Exception e) { + throw new Exception("API call failed"); + } + + return AppleLoginDto.builder() + .id(userId) + .token(accessToken) + .email(email).build(); + } + + private String createClientSecret() throws Exception { + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(appleLoginKey).build(); + JWTClaimsSet claimsSet = new JWTClaimsSet(); + + Date now = new Date(); + claimsSet.setIssuer(appleTeamId); + claimsSet.setIssueTime(now); + claimsSet.setExpirationTime(new Date(now.getTime() + 3600000)); + claimsSet.setAudience(appleAuthUrl); + claimsSet.setSubject(appleClientId); + + SignedJWT jwt = new SignedJWT(header, claimsSet); + + try { + ECPrivateKey ecPrivateKey = getPrivateKey(); + JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS()); + + jwt.sign(jwsSigner); + } catch (InvalidKeyException | JOSEException e) { + throw new Exception("Failed create client secret"); + } + + return jwt.serialize(); + } + + private ECPrivateKey getPrivateKey() throws Exception { + byte[] content = null; + File file = null; + + URL res = getClass().getResource(appleKeyPath); + + if ("jar".equals(res.getProtocol())) { + try { + InputStream input = getClass().getResourceAsStream(appleKeyPath); + file = File.createTempFile("tempfile", ".tmp"); + OutputStream out = new FileOutputStream(file); + + int read; + byte[] bytes = new byte[1024]; + + while ((read = input.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + + out.close(); + file.deleteOnExit(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } else { + file = new File(res.getFile()); + } + + if (file.exists()) { + try (FileReader keyReader = new FileReader(file); + PemReader pemReader = new PemReader(keyReader)) + { + PemObject pemObject = pemReader.readPemObject(); + content = pemObject.getContent(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + throw new Exception("File " + file + " not found"); + } + + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(content); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return (ECPrivateKey) keyFactory.generatePrivate(keySpec); + } +} diff --git a/src/main/resources/application-credentials.yml b/src/main/resources/application-credentials.yml index 003dd9b..bdfbf97 100644 --- a/src/main/resources/application-credentials.yml +++ b/src/main/resources/application-credentials.yml @@ -33,3 +33,15 @@ jwt: kakao: client_id: ${KAKAO_CLIENT_ID} redirect_uri: ${KAKAO_REDIRECT_URI} + +apple: + team: + id: ${APPLE_TEAM_ID} + login: + key: ${APPLE_LOGIN_KEY} + key: + path: ${APPLE_KEY_PATH} + client: + id: ${APPLE_CLIENT_ID} + redirect: + uri: ${APPLE_REDIRECT_URI} \ No newline at end of file From 18830eca7cdeb86e2c270095c9661ff3035f5b8f Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Tue, 8 Oct 2024 20:46:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?workflow=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cicd_workflow.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/cicd_workflow.yml b/.github/workflows/cicd_workflow.yml index 94af37d..cf344f7 100644 --- a/.github/workflows/cicd_workflow.yml +++ b/.github/workflows/cicd_workflow.yml @@ -17,6 +17,11 @@ jobs: distribution: 'adopt' java-version: '17' + - name: Set up Apple Key File + run: | + mkdir src/main/resources/key + echo "${{ secrets.APPLE_KEY }}" > src/main/resources/key/key.p8 + - name: Grant execute permission for gradlew run: chmod +x gradlew