Skip to content

Commit

Permalink
Merge pull request #108 from KCY-Fit-a-Pet/hotfix/106
Browse files Browse the repository at this point in the history
πŸ› OIDC Token deserializer μ‹€νŒ¨ (ν…ŒμŠ€νŠΈ ν•„μš”)
  • Loading branch information
heejinnn authored Feb 12, 2024
2 parents dc8e4a7 + 598f8e0 commit 870170e
Show file tree
Hide file tree
Showing 24 changed files with 213 additions and 146 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@

- WAS Server 내뢀에 Nginxλ₯Ό 톡해 Reverse Proxyλ₯Ό κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

## Multi Module Architecture

<div align="center"><img src="https://github.com/KCY-Fit-a-Pet/fit-a-pet-server/assets/96044622/df673786-71d5-4a30-b0ba-2c9e9f4f2603"></img></div>

- Multi Module Architectureλ₯Ό μ μš©ν•˜μ—¬ 각 λͺ¨λ“ˆλ³„λ‘œ λΆ„λ¦¬ν•˜μ—¬ κ°œλ°œν•˜κ³ , λΉŒλ“œ 및 배포λ₯Ό μ§„ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
- 각 λͺ¨λ“ˆλ³„λ‘œ λ‹΄λ‹Ήν•˜λŠ” 역할을 λΆ„λ¦¬ν•˜μ—¬ κ°œλ°œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
- 각 λͺ¨λ“ˆμ— λŒ€ν•œ Convention은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.
- [fitapet-common](https://github.com/KCY-Fit-a-Pet/fit-a-pet-server/tree/develop/fitapet-common/Readme.md) : 독립적인 μ˜€ν”ˆ μ†ŒμŠ€λ‘œ 배포 κ°€λŠ₯ν•œ μˆ˜μ€€μ˜ 곡톡 λͺ¨λ“ˆ. λͺ¨λ“  λͺ¨λ“ˆμ΄ μ˜μ‘΄ν•œλ‹€.
- [fitapet-infra](https://github.com/KCY-Fit-a-Pet/fit-a-pet-server/tree/develop/fitapet-infra/Readme.md) : λ°μ΄ν„°λ² μ΄μŠ€, μΈν”„λΌμŠ€νŠΈλŸ­μ²˜, ν΄λΌμš°λ“œ μ„œλΉ„μŠ€μ™€ μ—°λ™ν•˜λŠ” λͺ¨λ“ˆ. common λͺ¨λ“ˆμ— μ˜μ‘΄ν•œλ‹€.
- [fitapet-domain](https://github.com/KCY-Fit-a-Pet/fit-a-pet-server/tree/develop/fitapet-domain/Readme.md) : 도메인 λ‘œμ§μ„ λ‹΄λ‹Ήν•˜λ©°, repositoryλ₯Ό λ³΄ν˜Έν•˜λŠ” λͺ¨λ“ˆ. common λͺ¨λ“ˆμ— μ˜μ‘΄ν•œλ‹€.
- [fitapet-app-external-api](https://github.com/KCY-Fit-a-Pet/fit-a-pet-server/tree/develop/fitapet-app-external-api/Readme.md) : Client와 μ—°λ™ν•˜λŠ” μ™ΈλΆ€ APIλ₯Ό λ‹΄λ‹Ήν•˜λŠ” λͺ¨λ“ˆ. common, infra, domain λͺ¨λ“  λͺ¨λ“ˆμ— μ˜μ‘΄ν•œλ‹€.

## ERD
<div align="center"><img src="https://github.com/KCY-Fit-a-Pet/fit-a-pet-server/assets/96044622/9b75726d-1695-4459-8c3f-72f664d6d036"></img></div>

Expand Down
1 change: 1 addition & 0 deletions fitapet-app-external-api/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## API
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ public boolean isTokenExpired(String token, JwtType type) {
return jwtProviderMap.get(type).isTokenExpired(token);
}

/**
* 토큰을 λΈ”λž™ λ¦¬μŠ€νŠΈμ— λ“±λ‘ν•˜λŠ” λ©”μ„œλ“œ
* @param token : λΈ”λž™ λ¦¬μŠ€νŠΈμ— 등둝할 토큰
* @param type : ν† ν°μ˜ νƒ€μž…
*/
public void ban(String token, JwtType type) {
AccessToken forbiddenToken = AccessToken.of(token, getSubInfoFromToken(token, type).id(), getExpiryDate(token, type));
forbiddenTokenService.register(forbiddenToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,51 @@
package kr.co.fitapet.api.apis.auth.mapper;

import kr.co.fitapet.domain.common.redis.sms.service.SmsOauthService;
import kr.co.fitapet.domain.common.redis.sms.service.SmsPasswordService;
import kr.co.fitapet.domain.common.redis.sms.service.SmsRegisterService;
import kr.co.fitapet.domain.common.redis.sms.service.SmsUidService;
import kr.co.fitapet.domain.common.redis.sms.provider.SmsRedisProvider;
import kr.co.fitapet.domain.common.redis.sms.qualify.SmsOauthQualifier;
import kr.co.fitapet.domain.common.redis.sms.qualify.SmsPasswordQualifier;
import kr.co.fitapet.domain.common.redis.sms.qualify.SmsRegisterQualifier;
import kr.co.fitapet.domain.common.redis.sms.qualify.SmsUidQualifier;
import kr.co.fitapet.domain.common.redis.sms.type.SmsPrefix;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class SmsRedisMapper {
private final SmsOauthService smsOauthService;
private final SmsRegisterService smsRegisterService;
private final SmsPasswordService smsPasswordService;
private final SmsUidService smsUidService;
private final Map<SmsPrefix, SmsRedisProvider> smsProviderMap;

public SmsRedisMapper(
@SmsOauthQualifier SmsRedisProvider smsOauthService,
@SmsRegisterQualifier SmsRedisProvider smsRegisterService,
@SmsPasswordQualifier SmsRedisProvider smsPasswordService,
@SmsUidQualifier SmsRedisProvider smsUidService
) {
smsProviderMap = Map.of(
SmsPrefix.OAUTH, smsOauthService,
SmsPrefix.REGISTER, smsRegisterService,
SmsPrefix.PASSWORD, smsPasswordService,
SmsPrefix.UID, smsUidService
);
}

public void saveSmsAuthToken(String phone, String code, SmsPrefix prefix) {
switch (prefix) {
case OAUTH -> smsOauthService.save(phone, code, prefix);
case REGISTER -> smsRegisterService.save(phone, code, prefix);
case PASSWORD -> smsPasswordService.save(phone, code, prefix);
case UID -> smsUidService.save(phone, code, prefix);
}
smsProviderMap.get(prefix).saveSmsAuthToken(phone, code, prefix);
}

public boolean isCorrectCode(String phone, String code, SmsPrefix prefix) {
return switch (prefix) {
case OAUTH -> smsOauthService.isCorrectCode(phone, code, prefix);
case REGISTER -> smsRegisterService.isCorrectCode(phone, code, prefix);
case PASSWORD -> smsPasswordService.isCorrectCode(phone, code, prefix);
case UID -> smsUidService.isCorrectCode(phone, code, prefix);
};
return smsProviderMap.get(prefix).isCorrectCode(phone, code, prefix);
}

public boolean isExistsCode(String phone, SmsPrefix prefix) {
return switch (prefix) {
case OAUTH -> smsOauthService.isExistsCode(phone, prefix);
case REGISTER -> smsRegisterService.isExistsCode(phone, prefix);
case PASSWORD -> smsPasswordService.isExistsCode(phone, prefix);
case UID -> smsUidService.isExistsCode(phone, prefix);
};
return smsProviderMap.get(prefix).isExistsCode(phone, prefix);
}

public void removeCode(String phone, SmsPrefix prefix) {
switch (prefix) {
case OAUTH -> smsOauthService.removeCode(phone, prefix);
case REGISTER -> smsRegisterService.removeCode(phone, prefix);
case PASSWORD -> smsPasswordService.removeCode(phone, prefix);
case UID -> smsUidService.removeCode(phone, prefix);
};
smsProviderMap.get(prefix).removeCode(phone, prefix);
}

public LocalDateTime getExpiredTime(String phone, SmsPrefix prefix) {
return switch (prefix) {
case OAUTH -> smsOauthService.getExpiredTime(phone, prefix);
case REGISTER -> smsRegisterService.getExpiredTime(phone, prefix);
case PASSWORD -> smsPasswordService.getExpiredTime(phone, prefix);
case UID -> smsUidService.getExpiredTime(phone, prefix);
};
return smsProviderMap.get(prefix).getExpiredTime(phone, prefix);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
@RequestMapping("/api/v1/auth/oauth")
@Slf4j
public class OauthApi {
private final MemberAuthUseCase memberAuthUseCase;
private final OauthUseCase oauthUseCase;
private final CookieUtil cookieUtil;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.co.fitapet.api.apis.oauth.mapper;

import kr.co.fitapet.infra.client.oauth.OauthApplicationConfig;
import kr.co.fitapet.infra.client.oauth.provider.apple.AppleApplicationConfig;
import kr.co.fitapet.infra.client.oauth.provider.google.GoogleApplicationConfig;
import kr.co.fitapet.infra.client.oauth.provider.kakao.KakaoApplicationConfig;
import kr.co.fitapet.infra.client.oauth.type.Provider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class OauthApplicationConfigMapper {
private final Map<Provider, OauthApplicationConfig> oauthApplicationConfigMap;

public OauthApplicationConfigMapper(
KakaoApplicationConfig kakaoApplicationConfig,
GoogleApplicationConfig googleApplicationConfig,
AppleApplicationConfig appleApplicationConfig
) {
this.oauthApplicationConfigMap = Map.of(
Provider.KAKAO, kakaoApplicationConfig,
Provider.GOOGLE, googleApplicationConfig,
Provider.APPLE, appleApplicationConfig
);
}

public OauthApplicationConfig getOauthApplicationConfig(Provider provider) {
return oauthApplicationConfigMap.get(provider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kr.co.fitapet.api.apis.oauth.mapper;

import kr.co.fitapet.infra.client.oauth.OauthApplicationConfig;
import kr.co.fitapet.infra.client.oauth.OauthClient;
import kr.co.fitapet.infra.client.oauth.dto.OIDCPublicKeyResponse;
import kr.co.fitapet.infra.client.oauth.provider.apple.AppleOauthClient;
import kr.co.fitapet.infra.client.oauth.provider.google.GoogleOauthClient;
import kr.co.fitapet.infra.client.oauth.provider.kakao.KakaoOauthClient;
import kr.co.fitapet.infra.client.oauth.type.Provider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class OauthClientMapper {
private final Map<Provider, OauthClient> oauthClientMap;
private final OauthApplicationConfigMapper oauthApplicationConfigMapper;

public OauthClientMapper(
KakaoOauthClient kakaoOauthClient,
GoogleOauthClient googleOauthClient,
AppleOauthClient appleOauthClient,
OauthApplicationConfigMapper oauthApplicationConfigMapper
) {
this.oauthClientMap = Map.of(
Provider.KAKAO, kakaoOauthClient,
Provider.GOOGLE, googleOauthClient,
Provider.APPLE, appleOauthClient
);
this.oauthApplicationConfigMapper = oauthApplicationConfigMapper;
}

public OIDCPublicKeyResponse getPublicKeyResponse(Provider provider) {
return oauthClientMap.get(provider).getOIDCPublicKey();
}

public OauthApplicationConfig getOauthApplicationConfig(Provider provider) {
return oauthApplicationConfigMapper.getOauthApplicationConfig(provider);
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
package kr.co.fitapet.api.apis.oauth.usecase;

import kr.co.fitapet.api.apis.auth.dto.SmsRes;
import kr.co.fitapet.api.apis.auth.mapper.JwtMapper;
import kr.co.fitapet.api.apis.auth.mapper.SmsRedisMapper;
import kr.co.fitapet.api.apis.oauth.dto.OauthSmsReq;
import kr.co.fitapet.api.apis.oauth.helper.OauthOIDCHelper;
import kr.co.fitapet.api.common.security.jwt.JwtProvider;
import kr.co.fitapet.api.common.security.jwt.consts.JwtType;
import kr.co.fitapet.api.common.security.jwt.dto.Jwt;
import kr.co.fitapet.api.common.security.jwt.dto.JwtSubInfo;
import kr.co.fitapet.api.common.security.jwt.dto.JwtUserInfo;
import kr.co.fitapet.api.common.security.jwt.dto.SmsOauthInfo;
import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorCode;
import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorException;
import kr.co.fitapet.api.common.security.jwt.qualifier.AccessTokenQualifier;
import kr.co.fitapet.api.common.security.jwt.qualifier.RefreshTokenQualifier;
import kr.co.fitapet.api.common.security.jwt.qualifier.SmsOauthTokenQualifier;
import kr.co.fitapet.common.annotation.UseCase;
import kr.co.fitapet.common.execption.GlobalErrorException;
import kr.co.fitapet.domain.common.redis.forbidden.ForbiddenTokenService;
import kr.co.fitapet.domain.common.redis.oauth.OIDCTokenService;
import kr.co.fitapet.domain.common.redis.sms.provider.SmsRedisProvider;
import kr.co.fitapet.domain.common.redis.sms.qualify.SmsRegisterQualifier;
import kr.co.fitapet.domain.common.redis.sms.type.SmsPrefix;
import kr.co.fitapet.domain.domains.member.domain.AccessToken;
import kr.co.fitapet.domain.domains.member.domain.Member;
import kr.co.fitapet.domain.domains.member.service.MemberSaveService;
import kr.co.fitapet.domain.domains.member.service.MemberSearchService;
Expand All @@ -31,11 +26,11 @@
import kr.co.fitapet.domain.domains.oauth.service.OauthSearchService;
import kr.co.fitapet.domain.domains.oauth.type.ProviderType;
import kr.co.fitapet.infra.client.oauth.OauthClient;
import kr.co.fitapet.infra.client.oauth.OauthClientMapper;
import kr.co.fitapet.api.apis.oauth.mapper.OauthClientMapper;
import kr.co.fitapet.infra.client.oauth.dto.OIDCDecodePayload;
import kr.co.fitapet.infra.client.oauth.dto.OIDCPublicKeyResponse;
import kr.co.fitapet.infra.client.oauth.environment.OauthApplicationConfig;
import kr.co.fitapet.infra.client.oauth.environment.OauthApplicationConfigMapper;
import kr.co.fitapet.infra.client.oauth.OauthApplicationConfig;
import kr.co.fitapet.api.apis.oauth.mapper.OauthApplicationConfigMapper;
import kr.co.fitapet.infra.client.oauth.type.Provider;
import kr.co.fitapet.infra.client.sms.snes.SmsProvider;
import kr.co.fitapet.infra.client.sms.snes.dto.SnesDto;
Expand All @@ -57,21 +52,13 @@ public class OauthUseCase {
private final MemberSearchService memberSearchService;

private final OauthOIDCHelper oauthOIDCHelper;
private final OauthApplicationConfigMapper oauthApplicationConfigMapper;
private final OauthClientMapper oauthClientMapper;

@AccessTokenQualifier
private final JwtProvider accessTokenProvider;
@RefreshTokenQualifier
private final JwtProvider refreshTokenProvider;
@SmsOauthTokenQualifier
private final JwtProvider smsOauthTokenProvider;
private final ForbiddenTokenService forbiddenTokenService;
private final JwtMapper jwtMapper;
private final SmsRedisMapper smsRedisMapper;

private final OIDCTokenService oidcTokenService;
private final SmsProvider smsProvider;
@SmsRegisterQualifier
private final SmsRedisProvider smsRedisHelper;

@Transactional
public Optional<Pair<Long, Jwt>> signInByOIDC(String id, String idToken, ProviderType provider, String nonce) {
Expand All @@ -81,7 +68,7 @@ public Optional<Pair<Long, Jwt>> signInByOIDC(String id, String idToken, Provide

if (oauthSearchService.isExistMember(id, provider)) {
Member member = oauthSearchService.findMemberByOauthIdAndProvider(id, provider);
return Optional.of(Pair.of(member.getId(), generateToken(JwtUserInfo.from(member))));
return Optional.of(Pair.of(member.getId(), jwtMapper.login(JwtUserInfo.from(member))));
} else {
oidcTokenService.saveOIDCToken(idToken, provider, id);
return Optional.empty();
Expand All @@ -90,38 +77,36 @@ public Optional<Pair<Long, Jwt>> signInByOIDC(String id, String idToken, Provide

@Transactional
public Pair<Long, Jwt> signUpByOIDC(String id, ProviderType provider, String requestOauthAccessToken, OauthSignUpReq req) {
String accessToken = smsOauthTokenProvider.resolveToken(requestOauthAccessToken);
JwtSubInfo subs = smsOauthTokenProvider.getSubInfoFromToken(accessToken);
String smsOauthToken = jwtMapper.resolveToken(requestOauthAccessToken, JwtType.SMS_OAUTH_TOKEN);
JwtSubInfo subs = jwtMapper.getSubInfoFromToken(smsOauthToken, JwtType.SMS_OAUTH_TOKEN);
String phone = getPhoneByTopic(subs.phoneNumber());

validateToken(accessToken, subs.phoneNumber(), provider);
validateToken(smsOauthToken, subs.phoneNumber(), provider);

String idToken = oidcTokenService.findOIDCToken(req.idToken()).getToken();
OIDCDecodePayload payload = getPayload(provider, idToken, req.nonce());

Member member = Member.builder().uid(req.uid()).name(req.name())
.phone(phone).isOauth(Boolean.TRUE).role(RoleType.USER).build();
memberSaveService.saveMember(member);

OauthAccount oauthAccount = OauthAccount.of(id, provider, payload.email());
oauthAccount.updateMember(member);
oidcTokenService.deleteOIDCToken(req.idToken());

forbiddenTokenService.register(
AccessToken.of(accessToken, subs.id(), smsOauthTokenProvider.getExpiryDate(accessToken))
);

jwtMapper.ban(smsOauthToken, JwtType.SMS_OAUTH_TOKEN);
log.info("success oauth signup member id : {} - oauth id : {} [provider: {}]",
member.getId(), oauthAccount.getOauthId(), oauthAccount.getProvider());
return Pair.of(member.getId(), generateToken(JwtUserInfo.from(member)));
return Pair.of(member.getId(), jwtMapper.login(JwtUserInfo.from(member)));
}

@Transactional
public SmsRes sendCode(OauthSmsReq dto, ProviderType provider) {
SnesDto.SensInfo smsInfo = smsProvider.sendCodeByPhoneNumber(SnesDto.Request.of(dto.to()));
String key = makeTopic(dto.to(), provider);

smsRedisHelper.saveSmsAuthToken(key, smsInfo.code(), SmsPrefix.OAUTH);
LocalDateTime expireTime = smsRedisHelper.getExpiredTime(key, SmsPrefix.OAUTH);
smsRedisMapper.saveSmsAuthToken(key, smsInfo.code(), SmsPrefix.OAUTH);
LocalDateTime expireTime = smsRedisMapper.getExpiredTime(key, SmsPrefix.OAUTH);
log.info("인증번호 만료 μ‹œκ°„: {}", expireTime);
return SmsRes.of(dto.to(), smsInfo.requestTime(), expireTime);
}
Expand All @@ -130,11 +115,11 @@ public SmsRes sendCode(OauthSmsReq dto, ProviderType provider) {
public Pair<Long, Jwt> checkCertificationNumber(OauthSmsReq req, String id, String code, ProviderType provider) {
String key = makeTopic(req.to(), provider);
log.info("key: {}", key);
if (!smsRedisHelper.isCorrectCode(key, code, SmsPrefix.OAUTH)) {
if (!smsRedisMapper.isCorrectCode(key, code, SmsPrefix.OAUTH)) {
log.warn("인증번호 뢈일치 -> μ‚¬μš©μž μž…λ ₯ 인증 번호 : {}", code);
throw new GlobalErrorException(SmsErrorCode.INVALID_AUTH_CODE);
}
smsRedisHelper.removeCode(key, SmsPrefix.OAUTH);
smsRedisMapper.removeCode(key, SmsPrefix.OAUTH);

if (memberSearchService.isExistByPhone(req.to())) {
Member member = memberSearchService.findByPhone(req.to());
Expand All @@ -144,19 +129,18 @@ public Pair<Long, Jwt> checkCertificationNumber(OauthSmsReq req, String id, Stri
oauthAccount.updateMember(member);
oidcTokenService.deleteOIDCToken(req.idToken());

return Pair.of(member.getId(), generateToken(JwtUserInfo.from(member)));
return Pair.of(member.getId(), jwtMapper.login(JwtUserInfo.from(member)));
}

return Pair.of(0L, Jwt.of(smsOauthTokenProvider.generateToken(SmsOauthInfo.of(id, key)), null));
return Pair.of(0L, Jwt.of(jwtMapper.generateToken(SmsOauthInfo.of(id, key), JwtType.SMS_OAUTH_TOKEN), null));
}

/**
* idToken을 톡해 payloadλ₯Ό κ°€μ Έμ˜¨λ‹€.
*/
private OIDCDecodePayload getPayload(ProviderType provider, String idToken, String nonce) {
OauthClient oauthClient = oauthClientMapper.getOauthClient(Provider.valueOf(provider.name()));
OauthApplicationConfig oauthApplicationConfig = oauthApplicationConfigMapper.getOauthApplicationConfig(Provider.valueOf(provider.name()));
OIDCPublicKeyResponse oidcPublicKeyResponse = oauthClient.getOIDCPublicKey();
OauthApplicationConfig oauthApplicationConfig = oauthClientMapper.getOauthApplicationConfig(Provider.valueOf(provider.name()));
OIDCPublicKeyResponse oidcPublicKeyResponse = oauthClientMapper.getPublicKeyResponse(Provider.valueOf(provider.name()));

return oauthOIDCHelper.getPayloadFromIdToken(
idToken, oauthApplicationConfig.getJwksUri(),
Expand All @@ -177,7 +161,7 @@ private String makeTopic(String phoneNumber, ProviderType provider) {
}

private void validateToken(String accessToken, String value, ProviderType provider) {
if (forbiddenTokenService.isForbidden(accessToken))
if (jwtMapper.isForbidden(accessToken))
throw new AuthErrorException(AuthErrorCode.FORBIDDEN_ACCESS_TOKEN, "forbidden access token");

ProviderType tokenProvider = getProviderByTopic(value);
Expand All @@ -192,11 +176,4 @@ private ProviderType getProviderByTopic(String topic) {
private String getPhoneByTopic(String topic) {
return topic.split("_")[1];
}

private Jwt generateToken(JwtSubInfo jwtSubInfo) {
return Jwt.builder()
.accessToken(accessTokenProvider.generateToken(jwtSubInfo))
.refreshToken(refreshTokenProvider.generateToken(jwtSubInfo))
.build();
}
}
1 change: 1 addition & 0 deletions fitapet-common/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## Common
1 change: 1 addition & 0 deletions fitapet-domain/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## Domain
Loading

0 comments on commit 870170e

Please sign in to comment.