|
1 | 1 | package com.boggle_boggle.bbegok.oauth.client.impl; |
2 | 2 |
|
3 | | -import com.boggle_boggle.bbegok.oauth.client.AppleTokenClient; |
4 | | -import com.boggle_boggle.bbegok.oauth.client.AppleUserInfoClient; |
| 3 | +import com.auth0.jwt.JWT; |
| 4 | +import com.auth0.jwt.algorithms.Algorithm; |
| 5 | +import com.boggle_boggle.bbegok.config.properties.OAuthProperties; |
5 | 6 | import com.boggle_boggle.bbegok.oauth.client.OAuth2ProviderClient; |
6 | 7 | import com.boggle_boggle.bbegok.oauth.client.response.AppleTokenResponse; |
7 | 8 | import com.boggle_boggle.bbegok.oauth.info.OAuth2UserInfo; |
8 | 9 | import com.boggle_boggle.bbegok.oauth.info.impl.AppleOAuth2UserInfo; |
| 10 | +import com.fasterxml.jackson.core.type.TypeReference; |
| 11 | +import com.fasterxml.jackson.databind.ObjectMapper; |
9 | 12 | import lombok.RequiredArgsConstructor; |
| 13 | +import lombok.extern.slf4j.Slf4j; |
| 14 | +import org.springframework.http.HttpHeaders; |
| 15 | +import org.springframework.http.MediaType; |
10 | 16 | import org.springframework.stereotype.Component; |
| 17 | +import org.springframework.web.reactive.function.BodyInserters; |
| 18 | +import org.springframework.web.reactive.function.client.WebClient; |
11 | 19 |
|
| 20 | +import java.io.IOException; |
| 21 | +import java.nio.charset.StandardCharsets; |
| 22 | +import java.security.interfaces.ECPrivateKey; |
| 23 | +import java.time.Instant; |
| 24 | +import java.util.Base64; |
| 25 | +import java.util.Date; |
12 | 26 | import java.util.Map; |
13 | 27 |
|
14 | 28 | @Component |
15 | 29 | @RequiredArgsConstructor |
| 30 | +@Slf4j |
16 | 31 | public class AppleOAuth2Client implements OAuth2ProviderClient { |
17 | 32 |
|
18 | | - private final AppleTokenClient appleTokenClient; // 애플은 비동기/JWT 토큰 생성 등 별도 로직 필요 |
19 | | - private final AppleUserInfoClient appleUserInfoClient; |
20 | | - private String cachedIdToken; // id_token 저장 |
| 33 | + private final OAuthProperties oAuthProperties; |
| 34 | + private final ObjectMapper objectMapper; |
| 35 | + private final ECPrivateKey privateKey; //p8로 로그인용 JWT(액세스토큰) 개인키 만듦 |
21 | 36 |
|
| 37 | + private String cachedIdToken; |
| 38 | + |
| 39 | + //콜백 code를 기반으로 access_token + id_token 발급 |
22 | 40 | @Override |
23 | 41 | public String requestAccessToken(String code) { |
24 | | - AppleTokenResponse tokenResponse = appleTokenClient.getToken(code); |
| 42 | + log.info("[Apple] access_token 요청 시작"); |
| 43 | + |
| 44 | + String clientSecret = generateClientSecret(); |
| 45 | + |
| 46 | + AppleTokenResponse tokenResponse = WebClient.create() |
| 47 | + .post() |
| 48 | + .uri(oAuthProperties.getApple().getTokenUri()) |
| 49 | + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) |
| 50 | + .body(BodyInserters.fromFormData("grant_type", "authorization_code") |
| 51 | + .with("code", code) |
| 52 | + .with("client_id", oAuthProperties.getApple().getClientId()) |
| 53 | + .with("client_secret", clientSecret) |
| 54 | + .with("redirect_uri", oAuthProperties.getApple().getRedirectUri())) |
| 55 | + .retrieve() |
| 56 | + .bodyToMono(AppleTokenResponse.class) |
| 57 | + .block(); |
| 58 | + |
25 | 59 | this.cachedIdToken = tokenResponse.getIdToken(); |
26 | 60 | return tokenResponse.getAccessToken(); |
27 | 61 | } |
28 | 62 |
|
| 63 | + //id_token 파싱하여 사용자 정보 추출 |
29 | 64 | @Override |
30 | 65 | public OAuth2UserInfo requestUserInfo(String accessToken) { |
31 | | - Map<String, Object> attributes = appleUserInfoClient.getUserInfo(cachedIdToken); |
| 66 | + Map<String, Object> attributes = decodeIdToken(cachedIdToken); |
32 | 67 | return new AppleOAuth2UserInfo(attributes); |
33 | 68 | } |
| 69 | + |
| 70 | + //JWT 기반 client_secret 생성 |
| 71 | + private String generateClientSecret() { |
| 72 | + Instant now = Instant.now(); |
| 73 | + Instant exp = now.plusSeconds(3600); |
| 74 | + |
| 75 | + return JWT.create() |
| 76 | + .withIssuer(oAuthProperties.getApple().getTeamId()) |
| 77 | + .withSubject(oAuthProperties.getApple().getClientId()) |
| 78 | + .withAudience(oAuthProperties.getApple().getIss()) |
| 79 | + .withIssuedAt(Date.from(now)) |
| 80 | + .withExpiresAt(Date.from(exp)) |
| 81 | + .withKeyId(oAuthProperties.getApple().getKeyId()) |
| 82 | + .sign(Algorithm.ECDSA256(null, privateKey)); |
| 83 | + } |
| 84 | + |
| 85 | + //Base64 + JSON 파싱으로 user info 추출 |
| 86 | + private Map<String, Object> decodeIdToken(String idToken) { |
| 87 | + try { |
| 88 | + String[] parts = idToken.split("\\."); |
| 89 | + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); |
| 90 | + return objectMapper.readValue(payloadJson, new TypeReference<>() {}); |
| 91 | + } catch (IOException e) { |
| 92 | + throw new RuntimeException("Apple ID Token 디코딩 실패", e); |
| 93 | + } |
| 94 | + } |
34 | 95 | } |
0 commit comments