Skip to content

Commit

Permalink
[STMT-146] ✨ 애플 로그인 기능 추가 (#27)
Browse files Browse the repository at this point in the history
* [STMT-146] ✨ 애플 로그인 기능 추가

* [STMT-146] 🐛 AccessToken의 Bearer 파싱하여 애플 로그인 검증 하도록 변경

* [STMT-146] 🐛 Access Token에서 kid 값을 Jackson을 이용하여 추출
  • Loading branch information
zxcv9203 authored Feb 22, 2024
1 parent 16ee661 commit bc13c09
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.stumeet.server.common.auth.model.AuthenticationHeader;
import com.stumeet.server.common.auth.service.JwtAuthenticationService;
import com.stumeet.server.common.token.JwtTokenProvider;
import com.stumeet.server.common.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -27,7 +28,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.getContext();
if (context.getAuthentication() == null) {
String token = resolveToken(request.getHeader(AuthenticationHeader.ACCESS_TOKEN.getName()));
String token = JwtUtil.resolveToken(request.getHeader(AuthenticationHeader.ACCESS_TOKEN.getName()));

if (jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtAuthenticationService.getAuthentication(token);
Expand All @@ -38,10 +39,4 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
filterChain.doFilter(request, response);
}

private String resolveToken(String token) {
if (token != null && token.startsWith("Bearer ")) {
return token.substring(7);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,32 @@
import com.stumeet.server.member.domain.Member;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RequiredArgsConstructor
public class OAuthAuthenticationProvider implements AuthenticationProvider {

private final OAuthClient oAuthClient;
private final Map<String, OAuthClient> oAuthClient;
private final MemberOAuthUseCase memberOAuthUseCase;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;


@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
StumeetAuthenticationToken token = (StumeetAuthenticationToken) authentication;
String providerAccessToken = token.getCredentials();
String provider = token.getProvider();

try {
OAuthUserProfileResponse myProfile = oAuthClient.getMyProfile(providerAccessToken);
OAuthUserProfileResponse myProfile = oAuthClient.get(provider).getUserId(providerAccessToken);
Member member = memberOAuthUseCase.getMemberOrCreate(myProfile, provider);
LoginMember loginMember = new LoginMember(member);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse;

public interface OAuthClient {
OAuthUserProfileResponse getMyProfile(String accessToken);
OAuthUserProfileResponse getUserId(String accessToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.stumeet.server.common.client.oauth.apple;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.stumeet.server.common.client.oauth.apple.model.ApplePublicKeyResponses;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.math.BigInteger;
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;

@Component
@RequiredArgsConstructor
public class AppleIdTokenProvider {
private final ObjectMapper objectMapper;

public PublicKey getSecretKey(ApplePublicKeyResponses publicKeys, String accessToken) {
String kid = extractKid(accessToken);

ApplePublicKeyResponses.ApplePublicKeyResponse applePublicKey = publicKeys.keys().stream()
.filter(key -> key.kid().equals(kid))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("매칭되는 kid가 존재하지 않습니다."));

BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(applePublicKey.n()));
BigInteger publicExponent = new BigInteger(1, Base64.getUrlDecoder().decode(applePublicKey.e()));

RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");

return keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}

private String extractKid(String accessToken) {
String[] splitToken = accessToken.split("\\.");
String header = new String(Base64.getUrlDecoder().decode(splitToken[0]));
try {
return objectMapper.readTree(header).get("kid").asText();
} catch (Exception e) {
throw new RuntimeException(e);
}
}


public String extractUserId(PublicKey publicKey, String idToken) {
return Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(idToken)
.getPayload()
.getSubject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.stumeet.server.common.client.oauth.apple;

import com.stumeet.server.common.client.oauth.OAuthClient;
import com.stumeet.server.common.client.oauth.apple.model.ApplePublicKeyResponses;
import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse;
import com.stumeet.server.common.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.security.PublicKey;

@Component(value = "apple")
@RequiredArgsConstructor
public class AppleOAuthClient implements OAuthClient {

private final AppleOAuthFeignClient appleOAuthClient;
private final AppleIdTokenProvider appleIdTokenProvider;

@Override
public OAuthUserProfileResponse getUserId(String accessToken) {
ApplePublicKeyResponses publicKeys = appleOAuthClient.getPublicKeys();
String parseAccessToken = JwtUtil.resolveToken(accessToken);
PublicKey publicKey = appleIdTokenProvider.getSecretKey(publicKeys, parseAccessToken);
String id = appleIdTokenProvider.extractUserId(publicKey, parseAccessToken);

return new OAuthUserProfileResponse(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stumeet.server.common.client.oauth.apple;

import com.stumeet.server.common.client.oauth.apple.model.ApplePublicKeyResponses;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "appleOAuthClient", url = "https://appleid.apple.com")
public interface AppleOAuthFeignClient {

@GetMapping("/auth/keys")
ApplePublicKeyResponses getPublicKeys();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stumeet.server.common.client.oauth.apple.model;

import java.util.List;

public record ApplePublicKeyResponses(
List<ApplePublicKeyResponse> keys

) {
public record ApplePublicKeyResponse(
String kid,
String n,
String e
) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

@Component
@Component(value = "kakao")
@RequiredArgsConstructor
public class KakaoOAuthClient implements OAuthClient {

private final KakaoOAuthFeignClient kakaoOAuthClient;

@Override
public OAuthUserProfileResponse getMyProfile(String accessToken) {
public OAuthUserProfileResponse getUserId(String accessToken) {
ResponseEntity<KakaoUserProfileResponse> response = kakaoOAuthClient.getUserId(accessToken);

KakaoUserProfileResponse responseBody = response.getBody();
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/stumeet/server/common/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stumeet.server.common.util;

public class JwtUtil {

private JwtUtil() {

}

public static String resolveToken(String token) {
if (token != null && token.startsWith("Bearer ")) {
return token.substring(7);
}
return null;
}

}

0 comments on commit bc13c09

Please sign in to comment.