-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #58 from YAPP-Github/feat/PC-175-oauth-apple
[PC-175] 애플 소셜 로그인 기능
- Loading branch information
Showing
12 changed files
with
365 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
api/src/main/java/org/yapp/domain/auth/application/oauth/apple/AppleIdTokenDecoder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package org.yapp.domain.auth.application.oauth.apple; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jws; | ||
import io.jsonwebtoken.Jwts; | ||
import java.math.BigInteger; | ||
import java.net.URL; | ||
import java.security.KeyFactory; | ||
import java.security.PublicKey; | ||
import java.security.spec.RSAPublicKeySpec; | ||
import java.util.Base64; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class AppleIdTokenDecoder { | ||
|
||
private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys"; | ||
|
||
public Claims decodeIdToken(String idToken) { | ||
try { | ||
JsonNode appleKeys = fetchApplePublicKeys(); | ||
String kid = extractKidFromIdToken(idToken); | ||
|
||
JsonNode matchedKey = findMatchingKey(appleKeys, kid); | ||
if (matchedKey == null) { | ||
throw new RuntimeException("No matching key found for kid: " + kid); | ||
} | ||
|
||
PublicKey publicKey = buildPublicKey(matchedKey); | ||
|
||
Jws<Claims> claimsJws = Jwts.parser() | ||
.setSigningKey(publicKey) | ||
.build() | ||
.parseClaimsJws(idToken); | ||
|
||
return claimsJws.getBody(); // Payload 반환 | ||
} catch (Exception e) { | ||
throw new RuntimeException("Failed to decode idToken", e); | ||
} | ||
} | ||
|
||
private JsonNode fetchApplePublicKeys() throws Exception { | ||
ObjectMapper objectMapper = new ObjectMapper(); | ||
URL url = new URL(APPLE_KEYS_URL); | ||
return objectMapper.readTree(url).get("keys"); | ||
} | ||
|
||
private String extractKidFromIdToken(String idToken) throws JsonProcessingException { | ||
String[] parts = idToken.split("\\."); | ||
if (parts.length != 3) { | ||
throw new IllegalArgumentException("Invalid idToken format"); | ||
} | ||
String header = new String(Base64.getDecoder().decode(parts[0])); | ||
JsonNode headerJson = new ObjectMapper().readTree(header); | ||
return headerJson.get("kid").asText(); | ||
} | ||
|
||
private JsonNode findMatchingKey(JsonNode appleKeys, String kid) { | ||
for (JsonNode key : appleKeys) { | ||
if (key.get("kid").asText().equals(kid)) { | ||
return key; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private PublicKey buildPublicKey(JsonNode keyInfo) throws Exception { | ||
String n = keyInfo.get("n").asText(); | ||
String e = keyInfo.get("e").asText(); | ||
|
||
BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(n)); | ||
BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(e)); | ||
|
||
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, exponent); | ||
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); | ||
return keyFactory.generatePublic(publicKeySpec); | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
api/src/main/java/org/yapp/domain/auth/application/oauth/apple/AppleOauthClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package org.yapp.domain.auth.application.oauth.apple; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.HttpEntity; | ||
import org.springframework.http.HttpHeaders; | ||
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 org.yapp.core.exception.ApplicationException; | ||
import org.yapp.core.exception.error.code.AuthErrorCode; | ||
import org.yapp.domain.auth.application.oauth.OauthClient; | ||
import org.yapp.domain.auth.application.oauth.apple.dto.AppleAuthResponse; | ||
import org.yapp.global.config.AppleOauthProperties; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
@Slf4j | ||
public class AppleOauthClient implements OauthClient { | ||
|
||
private final AppleOauthHelper appleOauthHelper; | ||
private final AppleOauthProperties appleOauthProperties; | ||
private final RestTemplate restTemplate; | ||
|
||
public AppleAuthResponse getAppleAuthResponse( | ||
String clientId, | ||
String clientSecret, | ||
String grantType, | ||
String authorizationCode | ||
) { | ||
String url = "https://appleid.apple.com/auth/token"; | ||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); | ||
params.add("client_id", clientId); | ||
params.add("client_secret", clientSecret); | ||
params.add("grant_type", grantType); | ||
params.add("code", authorizationCode); | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers); | ||
try { | ||
ResponseEntity<AppleAuthResponse> response = restTemplate.postForEntity( | ||
url, entity, AppleAuthResponse.class | ||
); | ||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { | ||
return response.getBody(); | ||
} else { | ||
throw new ApplicationException(AuthErrorCode.ID_TOKEN_NOT_FOUND); | ||
} | ||
} catch (Exception e) { | ||
throw new ApplicationException(AuthErrorCode.OAUTH_ERROR); | ||
} | ||
} | ||
|
||
@Override | ||
public String getOAuthProviderUserId(String authorizationCode) { | ||
String clientSecret = appleOauthHelper.generateClientSecret(); | ||
AppleAuthResponse appleAuthResponse = this.getAppleAuthResponse( | ||
appleOauthProperties.getClientId(), | ||
clientSecret, | ||
"authorization_code", | ||
authorizationCode | ||
); | ||
String idToken = appleAuthResponse.getIdToken(); | ||
Claims idTokenClaims = appleOauthHelper.decodeIdToken(idToken); | ||
if (idTokenClaims.containsKey("sub")) { | ||
return idTokenClaims.get("sub", String.class); | ||
} else { | ||
throw new ApplicationException(AuthErrorCode.OAUTH_ID_NOT_FOUND); | ||
} | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
api/src/main/java/org/yapp/domain/auth/application/oauth/apple/AppleOauthHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package org.yapp.domain.auth.application.oauth.apple; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jwts; | ||
import java.security.PrivateKey; | ||
import java.security.Security; | ||
import java.time.LocalDateTime; | ||
import java.time.ZoneId; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
import lombok.RequiredArgsConstructor; | ||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; | ||
import org.bouncycastle.jce.provider.BouncyCastleProvider; | ||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; | ||
import org.springframework.stereotype.Component; | ||
import org.yapp.global.config.AppleOauthProperties; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class AppleOauthHelper { | ||
|
||
private final AppleOauthProperties appleProperties; | ||
private final AppleIdTokenDecoder appleIdTokenDecoder; | ||
|
||
public String generateClientSecret() { | ||
LocalDateTime expiration = LocalDateTime.now().plusMinutes(5); | ||
|
||
return Jwts.builder() | ||
.claim("iss", appleProperties.getTeamId()) | ||
.claim("aud", appleProperties.getAuthServer()) | ||
.claim("sub", appleProperties.getClientId()) | ||
.claim("exp", Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant())) | ||
.claim("iat", new Date()) | ||
.header() | ||
.add("kid", appleProperties.getKeyId()) | ||
.and() | ||
.signWith(getPrivateKey()) | ||
.compact(); | ||
} | ||
|
||
public Claims decodeIdToken(String idToken) { | ||
return this.appleIdTokenDecoder.decodeIdToken(idToken); | ||
} | ||
|
||
private PrivateKey getPrivateKey() { | ||
Security.addProvider(new BouncyCastleProvider()); | ||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); | ||
|
||
try { | ||
byte[] privateKeyBytes = Base64.getDecoder().decode(appleProperties.getPrivateKey()); | ||
|
||
PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes); | ||
return converter.getPrivateKey(privateKeyInfo); | ||
} catch (Exception e) { | ||
throw new RuntimeException("Error converting private key from String", e); | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
api/src/main/java/org/yapp/domain/auth/application/oauth/apple/AppleOauthProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package org.yapp.domain.auth.application.oauth.apple; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
import org.yapp.domain.auth.application.oauth.OauthClient; | ||
import org.yapp.domain.auth.application.oauth.OauthProvider; | ||
import org.yapp.domain.auth.domain.enums.OauthType; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class AppleOauthProvider implements OauthProvider { | ||
|
||
private final AppleOauthClient appleOauthClient; | ||
|
||
@Override | ||
public OauthType getOauthType() { | ||
return OauthType.APPLE; | ||
} | ||
|
||
@Override | ||
public OauthClient getOAuthClient() { | ||
return this.appleOauthClient; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
api/src/main/java/org/yapp/domain/auth/application/oauth/apple/dto/AppleAuthResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package org.yapp.domain.auth.application.oauth.apple.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
public class AppleAuthResponse { | ||
|
||
@JsonProperty("access_token") | ||
private String accessToken; | ||
@JsonProperty("token_type") | ||
private String tokenType; | ||
@JsonProperty("expires_in") | ||
private int expiresIn; | ||
@JsonProperty("refresh_token") | ||
private String refreshToken; | ||
@JsonProperty("id_token") | ||
private String idToken; | ||
} |
Oops, something went wrong.