Skip to content

Commit

Permalink
�애플 로그인 구현 테스트 (#82)
Browse files Browse the repository at this point in the history
* 애플 로그인 구현 테스트

* workflow 수정
  • Loading branch information
ysw789 authored Oct 8, 2024
1 parent b7ca360 commit e7839e4
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/cicd_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ src/.DS_Store
src/main/.DS_Store
src/main/resources/.DS_Store

AdminInit.java
AdminInit.java

/src/main/resources/key/key.p8
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +20,7 @@ public class AuthController {

private final SocialService socialService;
private final TokenService tokenService;
private final AppleLoginService appleLoginService;

@PostMapping("/kakao")
public ResponseEntity<ApiResponse<JwtToken>> kakao(@RequestBody SocialTokenDto token) {
Expand All @@ -34,9 +37,9 @@ public ResponseEntity<ApiResponse<JwtToken>> 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<ApiResponse<AppleLoginDto>> appleCallback(@RequestParam("code") String code) throws Exception {
return ResponseEntity.ok(new ApiResponse<>(appleLoginService.getAppleInfo(code)));
}

@PostMapping("/reissue")
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/dongyang/dongpo/dto/auth/AppleLoginDto.java
Original file line number Diff line number Diff line change
@@ -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;
}
187 changes: 187 additions & 0 deletions src/main/java/com/dongyang/dongpo/service/auth/AppleLoginService.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);

ResponseEntity<String> 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);
}
}
12 changes: 12 additions & 0 deletions src/main/resources/application-credentials.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}

0 comments on commit e7839e4

Please sign in to comment.