-
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.
Feature/#41 애플 oauth 연동
- Loading branch information
Showing
12 changed files
with
272 additions
and
3 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
7 changes: 7 additions & 0 deletions
7
src/main/java/play/pluv/login/application/dto/AppleLoginRequest.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,7 @@ | ||
package play.pluv.login.application.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public record AppleLoginRequest(@NotBlank String idToken) { | ||
|
||
} |
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
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
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,14 @@ | ||
package play.pluv.oauth.apple; | ||
|
||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; | ||
|
||
import org.springframework.util.MultiValueMap; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.service.annotation.PostExchange; | ||
import play.pluv.oauth.apple.dto.AppleTokenResponse; | ||
|
||
public interface AppleApiClient { | ||
|
||
@PostExchange(url = "https://appleid.apple.com/auth/oauth2/v2/token", contentType = APPLICATION_FORM_URLENCODED_VALUE) | ||
AppleTokenResponse fetchToken(@RequestParam final MultiValueMap<String, String> params); | ||
} |
14 changes: 14 additions & 0 deletions
14
src/main/java/play/pluv/oauth/apple/AppleConfigProperty.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,14 @@ | ||
package play.pluv.oauth.apple; | ||
|
||
import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
||
@ConfigurationProperties(prefix = "apple") | ||
public record AppleConfigProperty( | ||
String keyId, | ||
String teamId, | ||
String clientId, | ||
String redirectUri, | ||
String privateKey | ||
) { | ||
|
||
} |
108 changes: 108 additions & 0 deletions
108
src/main/java/play/pluv/oauth/apple/AppleConnector.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,108 @@ | ||
package play.pluv.oauth.apple; | ||
|
||
import static io.jsonwebtoken.io.Decoders.BASE64; | ||
import static java.lang.System.currentTimeMillis; | ||
import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
import static java.util.concurrent.TimeUnit.MINUTES; | ||
import static play.pluv.login.exception.LoginExceptionType.GENERATE_APPLE_CLIENT_SECRET_ERROR; | ||
import static play.pluv.playlist.domain.MusicStreaming.APPLE; | ||
|
||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import io.jsonwebtoken.Jwts; | ||
import java.security.KeyFactory; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.interfaces.ECPrivateKey; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.PKCS8EncodedKeySpec; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
import java.util.Map; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.LinkedMultiValueMap; | ||
import org.springframework.util.MultiValueMap; | ||
import play.pluv.login.exception.LoginException; | ||
import play.pluv.oauth.apple.dto.AppleTokenResponse; | ||
import play.pluv.oauth.application.SocialLoginClient; | ||
import play.pluv.oauth.domain.OAuthMemberInfo; | ||
import play.pluv.playlist.domain.MusicStreaming; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class AppleConnector implements SocialLoginClient { | ||
|
||
private static final String AUDIENCE = "https://appleid.apple.com"; | ||
private static final Long EXP = MILLISECONDS.convert(30, MINUTES); | ||
|
||
private final ObjectMapper objectMapper; | ||
private final AppleApiClient appleApiClient; | ||
private final AppleConfigProperty appleConfigProperty; | ||
|
||
@Override | ||
public OAuthMemberInfo fetchMember(final String idToken) { | ||
final String userIdentifier = extractSub(idToken); | ||
return new OAuthMemberInfo(userIdentifier, APPLE); | ||
} | ||
|
||
public AppleTokenResponse geTokens(final String authCode) { | ||
final MultiValueMap<String, String> param = createRequestParamForAccessToken(authCode); | ||
return appleApiClient.fetchToken(param); | ||
} | ||
|
||
private String extractSub(final String idToken) { | ||
try { | ||
final String claimsBase64 = idToken.substring(idToken.indexOf('.') + 1, | ||
idToken.lastIndexOf('.')); | ||
final var decode = BASE64.decode(claimsBase64); | ||
final Map<String, Object> claims = objectMapper.readValue(decode, new TypeReference<>() { | ||
}); | ||
return (String) claims.get("sub"); | ||
} catch (final Exception exception) { | ||
throw new IllegalArgumentException("토큰이 유효하지 않습니다."); | ||
} | ||
} | ||
|
||
@Override | ||
public MusicStreaming supportedType() { | ||
return APPLE; | ||
} | ||
|
||
private MultiValueMap<String, String> createRequestParamForAccessToken(final String authCode) { | ||
final MultiValueMap<String, String> param = new LinkedMultiValueMap<>(); | ||
param.add("grant_type", "authorization_code"); | ||
param.add("code", authCode); | ||
param.add("redirect_uri", appleConfigProperty.redirectUri()); | ||
param.add("client_id", appleConfigProperty.clientId()); | ||
param.add("client_secret", generateClientSecret()); | ||
return param; | ||
} | ||
|
||
private String generateClientSecret() { | ||
final Map<String, Object> jwtHeaders = Map.of( | ||
"kid", appleConfigProperty.keyId(), | ||
"alg", "ES256" | ||
); | ||
|
||
return Jwts.builder() | ||
.header().add(jwtHeaders).and() | ||
.issuer(appleConfigProperty.teamId()) | ||
.issuedAt(new Date()) | ||
.audience().add(AUDIENCE).and() | ||
.expiration(new Date(EXP + currentTimeMillis())) | ||
.subject(appleConfigProperty.clientId()) | ||
.signWith(getPrivateKeyFromPem(appleConfigProperty.privateKey())) | ||
.compact(); | ||
} | ||
|
||
private static ECPrivateKey getPrivateKeyFromPem(final String privateKey) { | ||
try { | ||
final byte[] decoded = Base64.getDecoder().decode(privateKey); | ||
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded); | ||
final KeyFactory keyFactory = KeyFactory.getInstance("EC"); | ||
return (ECPrivateKey) keyFactory.generatePrivate(keySpec); | ||
} catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { | ||
throw new LoginException(GENERATE_APPLE_CLIENT_SECRET_ERROR); | ||
} | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/play/pluv/oauth/apple/dto/AppleTokenResponse.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,12 @@ | ||
package play.pluv.oauth.apple.dto; | ||
|
||
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
|
||
@JsonNaming(SnakeCaseStrategy.class) | ||
public record AppleTokenResponse( | ||
String accessToken, | ||
String idToken | ||
) { | ||
|
||
} |
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
Submodule secret
updated
from 25925d to 6b5547
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
Oops, something went wrong.