-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/141 add apple oauth #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
cf491a1
a0b5252
49cdfea
0b9e7c5
84ccc90
b761b9b
8c3f986
4afb17d
e21d8ad
34bf7a6
14f3510
d4d4c52
37181d5
c9d62c3
f6ee7d6
9e732e9
428de7a
3aca3f9
ce1d144
eded031
473cd20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,83 @@ | ||||||||||||||||
| package com.example.solidconnection.auth.client; | ||||||||||||||||
|
|
||||||||||||||||
| import com.example.solidconnection.auth.dto.oauth.AppleTokenDto; | ||||||||||||||||
| import com.example.solidconnection.auth.dto.oauth.AppleUserInfoDto; | ||||||||||||||||
| import com.example.solidconnection.config.client.AppleOAuthClientProperties; | ||||||||||||||||
| import com.example.solidconnection.custom.exception.CustomException; | ||||||||||||||||
| import io.jsonwebtoken.Jwts; | ||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||
| import org.springframework.http.HttpEntity; | ||||||||||||||||
| import org.springframework.http.HttpHeaders; | ||||||||||||||||
| import org.springframework.http.HttpMethod; | ||||||||||||||||
| import org.springframework.http.MediaType; | ||||||||||||||||
| import org.springframework.http.ResponseEntity; | ||||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||
| import org.springframework.util.LinkedMultiValueMap; | ||||||||||||||||
| import org.springframework.util.MultiValueMap; | ||||||||||||||||
| import org.springframework.web.client.RestTemplate; | ||||||||||||||||
|
|
||||||||||||||||
| import java.security.PublicKey; | ||||||||||||||||
| import java.util.Objects; | ||||||||||||||||
|
|
||||||||||||||||
| import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_AUTHORIZATION_FAILED; | ||||||||||||||||
| import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN; | ||||||||||||||||
|
|
||||||||||||||||
| /* | ||||||||||||||||
| * 애플 인증을 위한 OAuth2 클라이언트 | ||||||||||||||||
| * https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens | ||||||||||||||||
| * */ | ||||||||||||||||
| @Component | ||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||
| public class AppleOAuthClient { | ||||||||||||||||
|
|
||||||||||||||||
| private final RestTemplate restTemplate; | ||||||||||||||||
| private final AppleOAuthClientProperties properties; | ||||||||||||||||
| private final AppleOAuthClientSecretProvider clientSecretProvider; | ||||||||||||||||
| private final ApplePublicKeyProvider publicKeyProvider; | ||||||||||||||||
|
|
||||||||||||||||
| public AppleUserInfoDto processOAuth(String code) { | ||||||||||||||||
| String idToken = requestIdToken(code); | ||||||||||||||||
| PublicKey applePublicKey = publicKeyProvider.getApplePublicKey(idToken); | ||||||||||||||||
| return new AppleUserInfoDto(parseEmailFromToken(applePublicKey, idToken)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| public String requestIdToken(String code) { | ||||||||||||||||
| HttpHeaders headers = new HttpHeaders(); | ||||||||||||||||
| headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||||||||||||||||
| MultiValueMap<String, String> formData = buildFormData(code); | ||||||||||||||||
|
|
||||||||||||||||
| try { | ||||||||||||||||
| ResponseEntity<AppleTokenDto> response = restTemplate.exchange( | ||||||||||||||||
| properties.tokenUrl(), | ||||||||||||||||
| HttpMethod.POST, | ||||||||||||||||
| new HttpEntity<>(formData, headers), | ||||||||||||||||
| AppleTokenDto.class | ||||||||||||||||
| ); | ||||||||||||||||
| return Objects.requireNonNull(response.getBody()).idToken(); | ||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||
| throw new CustomException(APPLE_AUTHORIZATION_FAILED, e.getMessage()); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid exposing internal exception messages to clients Passing Apply this diff to prevent information leakage: } catch (Exception e) {
- throw new CustomException(APPLE_AUTHORIZATION_FAILED, e.getMessage());
+ // Log the exception message internally
+ // logger.error("Failed to request ID token from Apple", e);
+ throw new CustomException(APPLE_AUTHORIZATION_FAILED);
}📝 Committable suggestion
Suggested change
|
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private MultiValueMap<String, String> buildFormData(String code) { | ||||||||||||||||
| MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(); | ||||||||||||||||
| formData.add("client_id", properties.clientId()); | ||||||||||||||||
| formData.add("client_secret", clientSecretProvider.generateClientSecret()); | ||||||||||||||||
| formData.add("code", code); | ||||||||||||||||
| formData.add("grant_type", "authorization_code"); | ||||||||||||||||
| formData.add("redirect_uri", properties.redirectUrl()); | ||||||||||||||||
| return formData; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private String parseEmailFromToken(PublicKey applePublicKey, String idToken) { | ||||||||||||||||
| try { | ||||||||||||||||
| return Jwts.parser() | ||||||||||||||||
| .setSigningKey(applePublicKey) | ||||||||||||||||
| .parseClaimsJws(idToken) | ||||||||||||||||
| .getBody() | ||||||||||||||||
| .get("email", String.class); | ||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||
| throw new CustomException(INVALID_APPLE_ID_TOKEN); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||||||||||
| package com.example.solidconnection.auth.client; | ||||||||||||||
|
|
||||||||||||||
| import com.example.solidconnection.config.client.AppleOAuthClientProperties; | ||||||||||||||
| import com.example.solidconnection.custom.exception.CustomException; | ||||||||||||||
| import io.jsonwebtoken.Jwts; | ||||||||||||||
| import io.jsonwebtoken.SignatureAlgorithm; | ||||||||||||||
| import jakarta.annotation.PostConstruct; | ||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||
| import org.apache.tomcat.util.codec.binary.Base64; | ||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||
|
|
||||||||||||||
| import java.io.BufferedReader; | ||||||||||||||
| import java.io.InputStream; | ||||||||||||||
| import java.io.InputStreamReader; | ||||||||||||||
| import java.nio.charset.StandardCharsets; | ||||||||||||||
| import java.security.KeyFactory; | ||||||||||||||
| import java.security.PrivateKey; | ||||||||||||||
| import java.security.spec.PKCS8EncodedKeySpec; | ||||||||||||||
| import java.util.Date; | ||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||
|
|
||||||||||||||
| import static com.example.solidconnection.custom.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY; | ||||||||||||||
|
|
||||||||||||||
| /* | ||||||||||||||
| * 애플 OAuth 에 필요하 클라이언트 시크릿은 매번 동적으로 생성해야 한다. | ||||||||||||||
| * 클라이언트 시크릿은 애플 개발자 계정에서 발급받은 개인키(*.p8)를 사용하여 JWT 를 생성한다. | ||||||||||||||
| * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret | ||||||||||||||
| * */ | ||||||||||||||
| @Component | ||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||
| public class AppleOAuthClientSecretProvider { | ||||||||||||||
|
|
||||||||||||||
| private static final String KEY_ID_HEADER = "kid"; | ||||||||||||||
| private static final long TOKEN_DURATION = 1000 * 60 * 10; // 10min | ||||||||||||||
| private static final String SECRET_KEY_PATH = "secret/AppleOAuthKey.p8"; | ||||||||||||||
|
|
||||||||||||||
| private final AppleOAuthClientProperties appleOAuthClientProperties; | ||||||||||||||
| private PrivateKey privateKey; | ||||||||||||||
|
|
||||||||||||||
| @PostConstruct | ||||||||||||||
| private void initPrivateKey() { | ||||||||||||||
| privateKey = readPrivateKey(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public String generateClientSecret() { | ||||||||||||||
| Date now = new Date(); | ||||||||||||||
| Date expiration = new Date(now.getTime() + TOKEN_DURATION); | ||||||||||||||
|
|
||||||||||||||
| return Jwts.builder() | ||||||||||||||
| .setHeaderParam("alg", "ES256") | ||||||||||||||
| .setHeaderParam(KEY_ID_HEADER, appleOAuthClientProperties.keyId()) | ||||||||||||||
| .setSubject(appleOAuthClientProperties.clientId()) | ||||||||||||||
| .setIssuer(appleOAuthClientProperties.teamId()) | ||||||||||||||
| .setAudience(appleOAuthClientProperties.clientSecretAudienceUrl()) | ||||||||||||||
| .setExpiration(expiration) | ||||||||||||||
| .signWith(SignatureAlgorithm.ES256, privateKey) | ||||||||||||||
| .compact(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private PrivateKey readPrivateKey() { | ||||||||||||||
| try (InputStream is = getClass().getClassLoader().getResourceAsStream(SECRET_KEY_PATH); | ||||||||||||||
| BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { | ||||||||||||||
|
|
||||||||||||||
| String secretKey = reader.lines().collect(Collectors.joining("\n")); | ||||||||||||||
| byte[] encoded = Base64.decodeBase64(secretKey); | ||||||||||||||
| PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); | ||||||||||||||
| KeyFactory keyFactory = KeyFactory.getInstance("EC"); | ||||||||||||||
| return keyFactory.generatePrivate(keySpec); | ||||||||||||||
| } catch (Exception e) { | ||||||||||||||
| throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+70
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Include the original exception when throwing Currently, the original exception is not passed to the Apply this diff to include the original exception: } catch (Exception e) {
- throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY);
+ throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY, e);
}📝 Committable suggestion
Suggested change
|
||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,94 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.example.solidconnection.auth.client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.example.solidconnection.config.client.AppleOAuthClientProperties; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.example.solidconnection.custom.exception.CustomException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.jsonwebtoken.ExpiredJwtException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.http.ResponseEntity; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.web.client.RestTemplate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.math.BigInteger; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.nio.charset.StandardCharsets; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.security.KeyFactory; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.security.PublicKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.security.spec.RSAPublicKeySpec; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Base64; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.ConcurrentHashMap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_ID_TOKEN_EXPIRED; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_PUBLIC_KEY_NOT_FOUND; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.apache.tomcat.util.codec.binary.Base64.decodeBase64URLSafe; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * idToken 검증을 위해서 애플의 공개키를 가져온다. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - 애플 공개키는 주기적으로 바뀐다. 이를 효율적으로 관리하기 위해 캐싱한다. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - idToken 의 헤더에 있는 kid 값에 해당하는 키가 캐싱되어있으면 그것을 반환한다. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - 그렇지 않다면 공개키가 바뀌었다는 뜻이므로, JSON 형식의 공개키 목록을 받아오고 캐시를 갱신한다. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class ApplePublicKeyProvider { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final AppleOAuthClientProperties properties; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RestTemplate restTemplate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final Map<String, PublicKey> applePublicKeyCache = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public PublicKey getApplePublicKey(String idToken) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String kid = getKeyIdFromTokenHeader(idToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (applePublicKeyCache.containsKey(kid)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return applePublicKeyCache.get(kid); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchApplePublicKeys(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (applePublicKeyCache.containsKey(kid)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return applePublicKeyCache.get(kid); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CustomException(APPLE_PUBLIC_KEY_NOT_FOUND); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (ExpiredJwtException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CustomException(APPLE_ID_TOKEN_EXPIRED); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CustomException(INVALID_APPLE_ID_TOKEN); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid catching generic In the Apply this diff to improve exception handling: } catch (ExpiredJwtException e) {
throw new CustomException(APPLE_ID_TOKEN_EXPIRED);
- } catch (Exception e) {
+ } catch (JsonProcessingException e) {
+ throw new CustomException(INVALID_APPLE_ID_TOKEN);
+ } catch (CustomException e) {
+ throw e; // Re-throw custom exceptions
+ } catch (Exception e) {
+ // Log unexpected exceptions
+ // logger.error("Unexpected exception", e);
throw new CustomException(INVALID_APPLE_ID_TOKEN);
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * idToken 은 JWS 이므로, 원칙적으로는 서명까지 검증되어야 parsing 이 가능하다 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 하지만 이 시점에서는 서명(=공개키)을 알 수 없으므로, Jwt 를 직접 인코딩하여 헤더를 가져온다. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private String getKeyIdFromTokenHeader(String idToken) throws JsonProcessingException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String[] jwtParts = idToken.split("\\."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (jwtParts.length < 2) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CustomException(INVALID_APPLE_ID_TOKEN); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0]), StandardCharsets.UTF_8); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new ObjectMapper().readTree(headerJson).get("kid").asText(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle the absence of In the Apply this diff to handle missing private String getKeyIdFromTokenHeader(String idToken) throws JsonProcessingException {
String[] jwtParts = idToken.split("\\.");
if (jwtParts.length < 2) {
throw new CustomException(INVALID_APPLE_ID_TOKEN);
}
String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0]), StandardCharsets.UTF_8);
- return new ObjectMapper().readTree(headerJson).get("kid").asText();
+ JsonNode kidNode = new ObjectMapper().readTree(headerJson).get("kid");
+ if (kidNode == null) {
+ throw new CustomException(INVALID_APPLE_ID_TOKEN);
+ }
+ return kidNode.asText();
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void fetchApplePublicKeys() throws Exception { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectMapper objectMapper = new ObjectMapper(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResponseEntity<String> response = restTemplate.getForEntity(properties.publicKeyUrl(), String.class); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JsonNode jsonNode = objectMapper.readTree(response.getBody()).get("keys"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| applePublicKeyCache.clear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (JsonNode key : jsonNode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| applePublicKeyCache.put(key.get("kid").asText(), generatePublicKey(key)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure thread safety when updating the public key cache In the Apply this diff to synchronize cache updates: private void fetchApplePublicKeys() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
ResponseEntity<String> response = restTemplate.getForEntity(properties.publicKeyUrl(), String.class);
JsonNode jsonNode = objectMapper.readTree(response.getBody()).get("keys");
- applePublicKeyCache.clear();
- for (JsonNode key : jsonNode) {
- applePublicKeyCache.put(key.get("kid").asText(), generatePublicKey(key));
- }
+ Map<String, PublicKey> newKeys = new ConcurrentHashMap<>();
+ for (JsonNode key : jsonNode) {
+ newKeys.put(key.get("kid").asText(), generatePublicKey(key));
+ }
+ applePublicKeyCache.clear();
+ applePublicKeyCache.putAll(newKeys);
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private PublicKey generatePublicKey(JsonNode key) throws Exception { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BigInteger modulus = new BigInteger(1, decodeBase64URLSafe(key.get("n").asText())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BigInteger exponent = new BigInteger(1, decodeBase64URLSafe(key.get("e").asText())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return KeyFactory.getInstance("RSA").generatePublic(spec); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Handle potential null response body to prevent
NullPointerExceptionThe method
response.getBody()may returnnullif the response does not contain a body. Relying onObjects.requireNonNullwill cause aNullPointerExceptionwhich may not provide sufficient context. Consider checking if the response body is null and throw aCustomExceptionwith a meaningful message.Apply this diff to improve error handling:
📝 Committable suggestion