Skip to content

Commit

Permalink
Merge pull request #123 from Team-Motivoo/feat/#19-apple
Browse files Browse the repository at this point in the history
✨ feat: 애플 로그인 구현
  • Loading branch information
jun02160 authored Feb 29, 2024
2 parents a636147 + f365283 commit fcc2ca5
Show file tree
Hide file tree
Showing 22 changed files with 199 additions and 156 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@

@Builder
public record LoginResponse (
String id,
String nickname,
@JsonProperty("token_type")
String tokenType,
String id,
String nickname,
@JsonProperty("token_type")
String tokenType,

@JsonProperty("access_token")
String accessToken,
@JsonProperty("refresh_token")
String refreshToken
@JsonProperty("access_token")
String accessToken,
@JsonProperty("refresh_token")
String refreshToken
){

private static final String BEARER_TYPE = "Bearer";

public static LoginResponse of(LoginResult result) {
return LoginResponse.builder()
.id(result.id())
.nickname(result.nickname())
.tokenType(result.tokenType())
.accessToken(result.accessToken())
.refreshToken(result.refreshToken()).build();
.id(result.id())
.nickname(result.nickname())
.tokenType(result.tokenType())
.accessToken(result.accessToken())
.refreshToken(result.refreshToken()).build();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package sopt.org.motivoo.api.controller.auth.dto.response;


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

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class OAuthPlatformMemberResponse {

private String platformId;
private String email;
}

This file was deleted.

This file was deleted.

4 changes: 2 additions & 2 deletions motivoo-api/src/test/java/controller/BaseControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import sopt.org.motivoo.domain.auth.config.RedisConfig;
import sopt.org.motivoo.domain.auth.config.jwt.JwtTokenProvider;
import sopt.org.motivoo.domain.auth.repository.TokenRedisRepository;
import sopt.org.motivoo.domain.auth.service.apple.AppleClaimsValidator;
import sopt.org.motivoo.domain.auth.service.apple.AppleLoginService;
import sopt.org.motivoo.external.client.auth.apple.service.AppleClaimsValidator;
import sopt.org.motivoo.external.client.auth.apple.service.AppleLoginService;
import sopt.org.motivoo.domain.external.firebase.FirebaseService;
import sopt.org.motivoo.domain.external.firebase.config.FirebaseConfig;
import sopt.org.motivoo.external.s3.S3Service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public enum CommonExceptionType implements BusinessExceptionType {
UNSUPPORTED_IMAGE_EXTENSION(HttpStatus.BAD_REQUEST, "이미지 확장자는 jpg, png, webp만 가능합니다."),
UNSUPPORTED_IMAGE_SIZE(HttpStatus.BAD_REQUEST, "이미지 사이즈는 5MB를 넘을 수 없습니다."),

INVALID_APPLE_PUBLIC_KEY(HttpStatus.BAD_REQUEST, "Apple JWT 값의 alg, kid 정보가 올바르지 않습니다."),
INVALID_APPLE_IDENTITY_TOKEN(HttpStatus.BAD_REQUEST, "Apple OAuth Identity Token 형식이 올바르지 않습니다."),
EXPIRED_APPLE_IDENTITY_TOKEN(HttpStatus.BAD_REQUEST, "Apple OAuth 로그인 중 Identity Token 유효기간이 만료됐습니다."),
INVALID_APPLE_CLAIMS(HttpStatus.BAD_REQUEST, "Apple OAuth Claims 값이 올바르지 않습니다."),

INVALID_ENCRYPT_COMMUNICATION(HttpStatus.BAD_REQUEST, "Apple OAuth 통신 암호화 과정 중 문제가 발생했습니다."),
CREATE_PUBLIC_KEY_EXCEPTION(HttpStatus.BAD_REQUEST, "Apple OAuth 로그인 중 public verify 생성에 문제가 발생했습니다."),

/**
* 401 Unauthorized
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void saveBlockedToken(String accessToken) {
setExpirationInRedis(key, expiration);
}

private Date getExpirationFromToken(String accessToken) {
public Date getExpirationFromToken(String accessToken) {
try {
accessToken = accessToken.replaceAll("\\s+", "");
accessToken = accessToken.replace(BEARER_TYPE, "");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package sopt.org.motivoo.domain.auth.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import sopt.org.motivoo.domain.user.exception.UserException;

import java.util.Date;

import static sopt.org.motivoo.domain.user.exception.UserExceptionType.TOKEN_NOT_FOUND;


@Component
@RequiredArgsConstructor
public class TokenRedisRetriever {
private final TokenRedisRepository tokenRedisRepository;
public void saveRefreshToken(String refreshToken, String account) {
tokenRedisRepository.saveRefreshToken(refreshToken, account);
}


public void saveBlockedToken(String accessToken) {
tokenRedisRepository.saveBlockedToken(accessToken);
}

private Date getExpirationFromToken(String accessToken) {
return tokenRedisRepository.getExpirationFromToken(accessToken);
}


public String getRefreshToken(String refreshToken) {
return tokenRedisRepository.findByRefreshToken(refreshToken).orElseThrow(
() -> new UserException(TOKEN_NOT_FOUND));
}


public void deleteRefreshToken(String refreshToken) {
tokenRedisRepository.deleteRefreshToken(refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
package sopt.org.motivoo.domain.auth.service;


import static sopt.org.motivoo.domain.auth.config.jwt.JwtTokenProvider.*;

import java.util.List;
import java.util.Map;

import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;

import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import sopt.org.motivoo.domain.auth.config.UserAuthentication;
import sopt.org.motivoo.domain.auth.config.jwt.JwtTokenProvider;
import sopt.org.motivoo.domain.auth.dto.request.OauthTokenCommand;
import sopt.org.motivoo.domain.auth.dto.response.LoginResult;
import sopt.org.motivoo.domain.auth.repository.TokenRedisRepository;
import sopt.org.motivoo.domain.auth.service.apple.AppleLoginService;
import sopt.org.motivoo.external.client.auth.apple.service.dto.OAuthPlatformMemberResult;
import sopt.org.motivoo.domain.auth.repository.TokenRedisRetriever;
import sopt.org.motivoo.external.client.auth.apple.service.AppleLoginService;
import sopt.org.motivoo.domain.user.dto.request.KakaoUserProfile;
import sopt.org.motivoo.domain.user.entity.SocialPlatform;
import sopt.org.motivoo.domain.user.entity.User;
Expand All @@ -31,36 +24,60 @@
import sopt.org.motivoo.domain.user.exception.UserExceptionType;
import sopt.org.motivoo.domain.user.repository.UserRetriever;

import java.util.List;
import java.util.Map;

import static sopt.org.motivoo.domain.auth.config.jwt.JwtTokenProvider.getAuthenticatedUser;
import static sopt.org.motivoo.domain.user.entity.SocialPlatform.*;
import static sopt.org.motivoo.domain.user.exception.UserExceptionType.*;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OauthService {

private final InMemoryClientRegistrationRepository inMemoryRepository;
private final UserRetriever userRetriever;
private final TokenRedisRepository tokenRedisRepository;
private final TokenRedisRetriever tokenRedisRetriever;
private final JwtTokenProvider jwtTokenProvider;

private final AppleLoginService appleLoginService;

@Transactional
public LoginResult login(OauthTokenCommand tokenRequest) {
String providerName = tokenRequest.tokenType();
log.info("소셜플랫폼="+providerName);
SocialPlatform socialPlatform = SocialPlatform.of(providerName);
String refreshToken = jwtTokenProvider.createRefreshToken();

//카카오
ClientRegistration provider = inMemoryRepository.findByRegistrationId(providerName);
if (socialPlatform.equals(KAKAO)) {
ClientRegistration provider = inMemoryRepository.findByRegistrationId(providerName);

String refreshToken = jwtTokenProvider.createRefreshToken();
User user = getUserProfile(providerName, tokenRequest, provider, refreshToken);
log.info("유저 아이디="+user.getId());
User user = getUserProfile(providerName, tokenRequest, provider, refreshToken);
log.info("유저 아이디="+user.getId());

String accessToken = jwtTokenProvider.createAccessToken(new UserAuthentication(user.getId(), null, null));
tokenRedisRetriever.saveRefreshToken(refreshToken, String.valueOf(user.getId()));
return LoginResult.of(user, accessToken, refreshToken);
}

if (socialPlatform.equals(APPLE)) {
OAuthPlatformMemberResult applePlatformMember = appleLoginService.getApplePlatformMember(tokenRequest.accessToken());

String accessToken = jwtTokenProvider.createAccessToken(new UserAuthentication(user.getId(), null, null));
tokenRedisRepository.saveRefreshToken(refreshToken, String.valueOf(user.getId()));
return LoginResult.of(user, accessToken, refreshToken);
List<User> userEntity = userRetriever.getUsersBySocialId(applePlatformMember.platformId());
//처음 로그인 하거나 탈퇴한 경우 -> 회원가입
if (userEntity == null || isWithdrawn(userEntity)) {
saveUser(null, applePlatformMember.platformId(), socialPlatform, tokenRequest, refreshToken);
}

//로그인
updateRefreshToken(userEntity.get(0), refreshToken);
String accessToken = jwtTokenProvider.createAccessToken(new UserAuthentication(userEntity.get(0).getId(),null,null));
return LoginResult.of(userEntity.get(0), accessToken, refreshToken);
}
throw new UserException(INVALID_SOCIAL_PLATFORM);
}


public User getUserProfile(String providerName, OauthTokenCommand tokenRequest, ClientRegistration provider, String refreshToken) {
Map<String, Object> userAttributes = getUserAttributes(provider, tokenRequest);
OAuth2UserInfo oAuth2UserInfo = getOAuth2UserInfo(providerName, userAttributes);
Expand All @@ -71,12 +88,10 @@ public User getUserProfile(String providerName, OauthTokenCommand tokenRequest,

List<User> userEntity = userRetriever.getUsersBySocialId(providerId);

//처음 로그인 하거나 탈퇴한 경우 -> 회원가입
if(userEntity==null || isWithdrawn(userEntity)){
return saveUser(nickName, providerId, socialPlatform, tokenRequest, refreshToken);
}

//로그인
updateRefreshToken(userEntity.get(0), refreshToken);
return userEntity.get(0);
}
Expand All @@ -94,22 +109,18 @@ private OAuth2UserInfo getOAuth2UserInfo(String providerName, Map<String, Object
if (providerName.equals("kakao")) {
return new KakaoUserProfile(userAttributes);
}
throw new UserException(UserExceptionType.INVALID_SOCIAL_PLATFORM);
throw new UserException(INVALID_SOCIAL_PLATFORM);
}

private SocialPlatform getSocialPlatform(String providerName) {
try {
switch (providerName){
case "kakao":
return SocialPlatform.KAKAO;
case "apple":
return SocialPlatform.APPLE;
default:
throw new UserException(UserExceptionType.INVALID_SOCIAL_PLATFORM);

}
}catch (FeignException e){
throw new UserException(UserExceptionType.INVALID_SOCIAL_PLATFORM);
return switch (providerName) {
case "kakao" -> KAKAO;
case "apple" -> APPLE;
default -> throw new UserException(INVALID_SOCIAL_PLATFORM);
};
} catch (FeignException e){
throw new UserException(INVALID_SOCIAL_PLATFORM);
}

}
Expand Down Expand Up @@ -147,7 +158,7 @@ private Map<String, Object> getUserAttributes(ClientRegistration provider, Oauth
public void logout(String accessToken) {
String refreshToken = userRetriever.getRefreshTokenById(getAuthenticatedUser());

tokenRedisRepository.saveBlockedToken(accessToken);
tokenRedisRepository.deleteRefreshToken(refreshToken);
tokenRedisRetriever.saveBlockedToken(accessToken);
tokenRedisRetriever.deleteRefreshToken(refreshToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum SocialPlatform {
KAKAO("카카오"),
APPLE("애플"),
WITHDRAW("탈퇴한 유저");
KAKAO("kakao"),
APPLE("apple"),
WITHDRAW("withdraw");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ public enum UserExceptionType implements BusinessExceptionType {
NULL_VALUE_AGE(HttpStatus.BAD_REQUEST, "유저의 나이는 null이어서는 안 됩니다."),
ALREADY_WITHDRAW_USER(HttpStatus.BAD_REQUEST, "이미 탈퇴한 유저입니다."),

INVALID_APPLE_PUBLIC_KEY(HttpStatus.BAD_REQUEST, "Apple JWT 값의 alg, kid 정보가 올바르지 않습니다."),
INVALID_APPLE_IDENTITY_TOKEN(HttpStatus.BAD_REQUEST, "Apple OAuth Identity Token 형식이 올바르지 않습니다."),
EXPIRED_APPLE_IDENTITY_TOKEN(HttpStatus.BAD_REQUEST, "Apple OAuth 로그인 중 Identity Token 유효기간이 만료됐습니다."),
INVALID_APPLE_CLAIMS(HttpStatus.BAD_REQUEST, "Apple OAuth Claims 값이 올바르지 않습니다."),
INVALID_ENCRYPT_COMMUNICATION(HttpStatus.BAD_REQUEST, "Apple OAuth 통신 암호화 과정 중 문제가 발생했습니다."),
CREATE_PUBLIC_KEY_EXCEPTION(HttpStatus.BAD_REQUEST, "Apple OAuth 로그인 중 public verify 생성에 문제가 발생했습니다."),

/**
* 401 Unauthorized
*/
Expand Down
Loading

0 comments on commit fcc2ca5

Please sign in to comment.