Skip to content

Commit

Permalink
Merge pull request #12 from Team-Going/feature/9
Browse files Browse the repository at this point in the history
[feat] 소셜 로그인 OpenFeign 관련 세팅
  • Loading branch information
gardening-y authored Jan 7, 2024
2 parents 6cc41ff + dc5d6da commit 8beeb9c
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public enum ErrorMessage {
INVALID_REFRESH_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "e4015", "리프레시 토큰의 값이 올바르지 않습니다."),
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "e4016", "리프레시 토큰이 만료되었습니다. 다시 로그인해 주세요."),
MISMATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "e4017", "리프레시 토큰이 일치하지 않습니다."),
INVALID_IDENTITY_TOKEN(HttpStatus.UNAUTHORIZED, "e4018", "애플 아이덴티티 토큰의 형식이 올바르지 않습니다."),
INVALID_IDENTITY_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "e4019", "애플 아이덴티티 토큰의 값이 올바르지 않습니다."),
UNABLE_TO_CREATE_APPLE_PUBLIC_KEY(HttpStatus.UNAUTHORIZED, "e40110", "애플 로그인 중 퍼블릭 키 생성에 문제가 발생했습니다."),
EXPIRED_IDENTITY_TOKEN(HttpStatus.UNAUTHORIZED, "e40111", "애플 로그인 중 아이덴티티 토큰의 유효 기간이 만료되었습니다."),
INVALID_IDENTITY_TOKEN_CLAIMS(HttpStatus.UNAUTHORIZED, "e40112", "애플 아이덴터티 토큰의 클레임 값이 올바르지 않습니다."),
INVALID_KAKAO_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "e40113", "카카오 액세스 토큰의 정보를 조회하는 과정에서 오류가 발생하였습니다."),

/**
* 403 Forbidden
Expand Down
5 changes: 5 additions & 0 deletions doorip-external/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.3'
implementation 'org.springframework.boot:spring-boot-starter-web'
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 project(path: ':doorip-common')
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.doorip.config;

import org.doorip.ExternalRoot;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients(basePackageClasses = ExternalRoot.class)
public class FeignClientConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.doorip.openfeign.apple;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "${oauth.apple.name}", url = "${oauth.apple.url}")
public interface AppleFeignClient {
@GetMapping("/keys")
ApplePublicKeys getApplePublicKeys();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.doorip.openfeign.apple;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.*;
import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Map;

@Component
public class AppleIdentityTokenParser {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

public Map<String, String> parseHeaders(String identityToken) {
try {
String encoded = identityToken.split("\\.")[0];
String decoded = new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8);
return OBJECT_MAPPER.readValue(decoded, Map.class);
} catch (JsonProcessingException | ArrayIndexOutOfBoundsException e) {
throw new UnauthorizedException(ErrorMessage.INVALID_IDENTITY_TOKEN);
}
}

public Claims parseWithPublicKeyAndGetClaims(String identityToken, PublicKey publicKey) {
try {
return getJwtParser(publicKey)
.parseClaimsJws(identityToken)
.getBody();
} catch (ExpiredJwtException e) {
throw new UnauthorizedException(ErrorMessage.EXPIRED_IDENTITY_TOKEN);
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
throw new UnauthorizedException(ErrorMessage.INVALID_IDENTITY_TOKEN_VALUE);
}
}

private JwtParser getJwtParser(Key key) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.doorip.openfeign.apple;

import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppleIdentityTokenValidator {
@Value("${oauth.apple.iss}")
private String iss;
@Value("${oauth.apple.client-id}")
private String clientId;

public boolean isValidAppleIdentityToken(Claims claims) {
return claims.getIssuer().contains(iss)
&& claims.getAudience().equals(clientId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.doorip.openfeign.apple;

import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;
import org.springframework.stereotype.Component;

import java.security.PublicKey;
import java.util.Map;

@RequiredArgsConstructor
@Component
public class AppleOAuthProvider {
private final AppleFeignClient appleFeignClient;
private final AppleIdentityTokenParser appleIdentityTokenParser;
private final ApplePublicKeyGenerator applePublicKeyGenerator;
private final AppleIdentityTokenValidator appleIdentityTokenValidator;

public String getApplePlatformId(String identityToken) {
Map<String, String> headers = appleIdentityTokenParser.parseHeaders(identityToken);
ApplePublicKeys applePublicKeys = appleFeignClient.getApplePublicKeys();
PublicKey publicKey = applePublicKeyGenerator.generatePublicKeyWithApplePublicKeys(headers, applePublicKeys);
Claims claims = appleIdentityTokenParser.parseWithPublicKeyAndGetClaims(identityToken, publicKey);
validateClaims(claims);

return claims.getSubject();
}

private void validateClaims(Claims claims) {
if (!appleIdentityTokenValidator.isValidAppleIdentityToken(claims)) {
throw new UnauthorizedException(ErrorMessage.INVALID_IDENTITY_TOKEN_CLAIMS);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.doorip.openfeign.apple;

public record ApplePublicKey(
String kty,
String kid,
String use,
String alg,
String n,
String e) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.doorip.openfeign.apple;

import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;
import org.springframework.stereotype.Component;

import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import java.util.Map;
import java.math.BigInteger;

@Component
public class ApplePublicKeyGenerator {
public PublicKey generatePublicKeyWithApplePublicKeys(Map<String, String> headers, ApplePublicKeys applePublicKeys) {
ApplePublicKey applePublicKey = applePublicKeys
.getMatchesKey(headers.get("alg"), headers.get("kid"));

byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKey.n());
byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKey.e());

RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(
new BigInteger(1, nBytes), new BigInteger(1, eBytes));

try {
KeyFactory keyFactory = KeyFactory.getInstance(applePublicKey.kty());
return keyFactory.generatePublic(rsaPublicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
throw new UnauthorizedException(ErrorMessage.UNABLE_TO_CREATE_APPLE_PUBLIC_KEY);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.doorip.openfeign.apple;

import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;

import java.util.List;

public class ApplePublicKeys {
private List<ApplePublicKey> keys;

public ApplePublicKey getMatchesKey(String alg, String kid) {
return keys.stream()
.filter(applePublicKey -> applePublicKey.alg().equals(alg) && applePublicKey.kid().equals(kid))
.findFirst()
.orElseThrow(() -> new UnauthorizedException(ErrorMessage.INVALID_IDENTITY_TOKEN));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.doorip.openfeign.kakao;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class KakaoAccessToken {
private static final String TOKEN_TYPE = "Bearer ";
private String accessToken;

public static KakaoAccessToken createKakaoAccessToken(String accessToken) {
return new KakaoAccessToken(accessToken);
}

public String getAccessTokenWithTokenType() {
return TOKEN_TYPE + accessToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.doorip.openfeign.kakao;

public record KakaoAccessTokenInfo(
Long id
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.doorip.openfeign.kakao;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(name = "${oauth.kakao.name}", url = "${oauth.kakao.url}")
public interface KakaoFeignClient {
@GetMapping
KakaoAccessTokenInfo getKakaoAccessTokenInfo(@RequestHeader("Authorization") String accessTokenWithTokenType);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.doorip.openfeign.kakao;

import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;
import org.springframework.stereotype.Component;

@Slf4j
@RequiredArgsConstructor
@Component
public class KakaoOAuthProvider {
private final KakaoFeignClient kakaoFeignClient;

public String getKakaoPlatformId(String accessToken) {
KakaoAccessToken kakaoAccessToken = KakaoAccessToken.createKakaoAccessToken(accessToken);
String accessTokenWithTokenType = kakaoAccessToken.getAccessTokenWithTokenType();
KakaoAccessTokenInfo kakaoAccessTokenInfo = getKakaoAccessTokenInfo(accessTokenWithTokenType);
return String.valueOf(kakaoAccessTokenInfo.id());
}

private KakaoAccessTokenInfo getKakaoAccessTokenInfo(String accessTokenWithTokenType) {
try {
return kakaoFeignClient.getKakaoAccessTokenInfo(accessTokenWithTokenType);
} catch (FeignException e) {
log.error("Feign Exception: ", e);
throw new UnauthorizedException(ErrorMessage.INVALID_KAKAO_ACCESS_TOKEN);
}
}
}

0 comments on commit 8beeb9c

Please sign in to comment.