Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

T-10855 [feat] GET /authorize API 구현 #260

Merged
merged 42 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
81c0a33
#T-10855 [feat] oauth config 추가
KWY0218 May 29, 2024
db6fa35
#T-10855 [feat] 소셜 로그인 시 필요한 의존성 추가
KWY0218 May 29, 2024
6900681
#T-10855 [feat] 구글 소셜 로그인 기능 구현
KWY0218 May 29, 2024
37019ad
#T-10855 [feat] 애플 소셜 로그인 구현
KWY0218 May 29, 2024
fed5aba
T-10855 [feat] 소셜 로그인 매니저 기능 구현
KWY0218 May 29, 2024
2a17ed4
#T-10855 [feat] 팀 별 oauth 정보가 저장되는 entity 정의
KWY0218 May 29, 2024
8b183a2
#T-10855 [feat] 플랫폼 인가코드 발급을 위한 secert key 정의
KWY0218 May 29, 2024
69d84e8
#T-10855 [feat] 인가 코드 발급 시 사용되는 상수 정의
KWY0218 May 29, 2024
f6f722c
#T-10855 [feat] 소셜 정보 조회 기능 구현
KWY0218 May 29, 2024
a12ef89
#T-10855 [feat] social type에 존재 여부 확인 기능 구현
KWY0218 May 29, 2024
87f5f62
#T-10855 [feat] api 모듈 내 jwt builder 의존성 추가
KWY0218 May 29, 2024
bb61d07
#T-10855 [feat] /authorize response body dto 정의
KWY0218 May 29, 2024
4fc2a2b
#T-10855 [feat] /authorize swagger 정의
KWY0218 May 29, 2024
c826462
#T-10855 [feat] /authorize controller 비즈니스 로직 구현
KWY0218 May 29, 2024
1fae6f0
#T-10855 [feat] auth service 구현체 생성 및 비즈니스 로직 구현
KWY0218 May 29, 2024
ebf4996
#T-10855 [chore] swagger 문서 내용 수정
KWY0218 May 29, 2024
98b4cd6
#T-10855 [feat] /authorize swagger 수정
KWY0218 May 30, 2024
2491109
#T-10855 [fix] end point 수정
KWY0218 May 30, 2024
7b2f524
#T-10855 [feat] 상수와 final 사이 개행문자 추가
KWY0218 May 30, 2024
9f5a36e
#T-10855 [chore] 상수 정의
KWY0218 May 30, 2024
8405fc1
#T-10855 [feat] payload 내 issuer aud 추가
KWY0218 May 30, 2024
6bf98a0
#T-10855 [chore] 지역변수 제거
KWY0218 May 31, 2024
fa4f889
#T-10855 [chore] 개행 문자 및 if 문 코딩 컨밴션 적용
KWY0218 May 31, 2024
7a42391
#T-10855 [fix] 팀 정보가 없을 시 throw 하도록 수정
KWY0218 May 31, 2024
2dcc511
#T-10855 [feat] auth exception error handler 등록
KWY0218 May 31, 2024
e28e609
#T-10855 [test] auth service 테스트 코드 작성
KWY0218 May 31, 2024
5b69e2d
#T-10855 [test] auth api controller 테스트 코드 작성
KWY0218 May 31, 2024
a06028b
#T-10855 [fix] 파라미터 null 검증 로직 제거
KWY0218 May 31, 2024
6830ab0
#T-10855 [feat] ci 내 apple key 파일 저장 기능 추가
KWY0218 May 31, 2024
4838ec1
#T-10855 [ci] apple key 디코딩을 위한 workflow 수정
KWY0218 May 31, 2024
f9e3268
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
39f5a2b
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
f04ec56
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
3ccaaad
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
e5a3acb
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
deea533
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
a3c1e2f
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
6402942
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
801b7ce
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
2a066de
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
37a0408
#T-10855 [ci] workflow 수정
KWY0218 May 31, 2024
a7c381b
#T-10855 [feat] cd workflow 수정
KWY0218 May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions operation-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ dependencies {
// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

// jwt builder library
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly "io.jsonwebtoken:jjwt-impl:0.11.2"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:0.11.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.sopt.makers.operation.auth.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.sopt.makers.operation.dto.BaseResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;

public interface AuthApi {
@Operation(
security = @SecurityRequirement(name = "Authorization"),
summary = "플랫폼 인가코드 반환 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "플랫폼 인가코드 반환 성공"
),
@ApiResponse(
responseCode = "400",
description = """
1. 쿼리 파라미터 중 데이터가 들어오지 않았습니다.\n
2. 유효하지 않은 social type 입니다.\n
3. 유효하지 않은 id token 입니다.\n
4. 유효하지 않은 social code 입니다.
"""
),
@ApiResponse(
responseCode = "404",
description = """
등록되지 않은 팀입니다.
등록된 소셜 정보가 없습니다.
"""
),
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
}
)
ResponseEntity<BaseResponse<?>> authorize(
@RequestParam String type,
@RequestParam String code,
@RequestParam String clientId,
@RequestParam String redirectUri
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.sopt.makers.operation.auth.api;

import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.auth.dto.response.AuthorizationCodeResponse;
import org.sopt.makers.operation.auth.service.AuthService;
import org.sopt.makers.operation.dto.BaseResponse;
import org.sopt.makers.operation.exception.AuthException;
import org.sopt.makers.operation.user.domain.SocialType;
import org.sopt.makers.operation.util.ApiResponseUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ConcurrentHashMap;

import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_SOCIAL_TYPE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_PARAMS;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_FOUNT_REGISTERED_TEAM;
import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GET_AUTHORIZATION_CODE;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class AuthApiController implements AuthApi {
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
private final ConcurrentHashMap<String, String> tempPlatformCode = new ConcurrentHashMap<>();
private final AuthService authService;

@Override
@GetMapping("/authorize")
public ResponseEntity<BaseResponse<?>> authorize(
@RequestParam String type,
@RequestParam String code,
@RequestParam String clientId,
@RequestParam String redirectUri
) {
if (checkParamsIsNull(type, code, clientId, redirectUri)) throw new AuthException(NOT_NULL_PARAMS);
if (authService.checkRegisteredTeamOAuthInfo(clientId, redirectUri)) throw new AuthException(NOT_FOUNT_REGISTERED_TEAM);
if (!SocialType.isContains(type)) throw new AuthException(INVALID_SOCIAL_TYPE);
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved

val userId = findUserIdBySocialTypeAndCode(type, code);
val platformCode = generatePlatformCode(userId);
return ApiResponseUtil.success(SUCCESS_GET_AUTHORIZATION_CODE, new AuthorizationCodeResponse(platformCode));
}

private boolean checkParamsIsNull(String... params) {
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
for (String param : params) {
if (param == null) return true;
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
}
return false;
}

private Long findUserIdBySocialTypeAndCode(String type, String code) {
val socialType = SocialType.valueOf(type);
val userSocialId = authService.getSocialUserInfo(socialType, code);
return authService.getUserId(socialType, userSocialId);
}

private String generatePlatformCode(Long userId) {
val platformCode = authService.generatePlatformCode(userId);
tempPlatformCode.putIfAbsent(platformCode, platformCode);
return platformCode;
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sopt.makers.operation.auth.dto.response;

public record AuthorizationCodeResponse(String platformCode) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.operation.auth.service;

import org.sopt.makers.operation.user.domain.SocialType;

public interface AuthService {
boolean checkRegisteredTeamOAuthInfo(String clientId, String redirectUri);

String getSocialUserInfo(SocialType type, String code);

Long getUserId(SocialType socialType, String userSocialId);

String generatePlatformCode(Long userId);
}
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.sopt.makers.operation.auth.service;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.xml.bind.DatatypeConverter;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.auth.repository.TeamOAuthInfoRepository;
import org.sopt.makers.operation.client.social.SocialLoginManager;
import org.sopt.makers.operation.config.ValueConfig;
import org.sopt.makers.operation.exception.AuthException;
import org.sopt.makers.operation.user.domain.SocialType;
import org.sopt.makers.operation.user.repository.identityinfo.UserIdentityInfoRepository;
import org.springframework.stereotype.Service;

import javax.crypto.spec.SecretKeySpec;
import java.time.ZoneId;
import java.util.Date;

import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_SOCIAL_CODE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_FOUND_USER_SOCIAL_IDENTITY_INFO;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private static final ZoneId KST = ZoneId.of("Asia/Seoul");
private final SocialLoginManager socialLoginManager;
private final TeamOAuthInfoRepository teamOAuthInfoRepository;
private final UserIdentityInfoRepository userIdentityInfoRepository;

private final ValueConfig valueConfig;
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved

@Override
public boolean checkRegisteredTeamOAuthInfo(String clientId, String redirectUri) {
return teamOAuthInfoRepository.existsByClientIdAndRedirectUri(clientId, redirectUri);
}

@Override
public String getSocialUserInfo(SocialType type, String code) {
val idToken = socialLoginManager.getIdTokenByCode(type, code);
if (idToken == null) throw new AuthException(INVALID_SOCIAL_CODE);
return socialLoginManager.getUserInfo(idToken);
}

@Override
public Long getUserId(SocialType socialType, String userSocialId) {
val userIdentityInfo = userIdentityInfoRepository.findBySocialTypeAndSocialId(socialType, userSocialId)
.orElseThrow(() -> new AuthException(NOT_FOUND_USER_SOCIAL_IDENTITY_INFO));
return userIdentityInfo.getUserId();
}

@Override
public String generatePlatformCode(Long userId) {
val platformCodeSecretKey = valueConfig.getPlatformCodeSecretKey();

val signatureAlgorithm = SignatureAlgorithm.HS256;
val secretKeyBytes = DatatypeConverter.parseBase64Binary(platformCodeSecretKey);
val signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName());
val exp = new Date().toInstant().atZone(KST)
.toLocalDateTime().plusMinutes(5).atZone(KST).toInstant();
return Jwts.builder()
.setSubject(Long.toString(userId))
.setExpiration(Date.from(exp))
.signWith(signingKey, signatureAlgorithm)
.compact();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.sopt.makers.operation.code.failure.auth;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.sopt.makers.operation.code.failure.FailureCode;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;

@Getter
@RequiredArgsConstructor
public enum AuthFailureCode implements FailureCode {
// 400
Comment on lines +13 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4

별 건 아니구 class와 내부 내용 사이에 개행!!

Suggested change
public enum AuthFailureCode implements FailureCode {
// 400
public enum AuthFailureCode implements FailureCode {
// 400

NOT_NULL_PARAMS(BAD_REQUEST, "쿼리 파라미터 중 데이터가 들어오지 않았습니다."),
INVALID_SOCIAL_TYPE(BAD_REQUEST, "유효하지 않은 social type 입니다."),
INVALID_ID_TOKEN(BAD_REQUEST, "유효하지 않은 id token 입니다."),
INVALID_SOCIAL_CODE(BAD_REQUEST, "유효하지 않은 social code 입니다."),
FAILURE_READ_PRIVATE_KEY(BAD_REQUEST, "Private key 읽기 실패"),
// 404
Comment on lines +19 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4

개행하면 조금 더 가독성 좋을 것 같습니다!!

Suggested change
FAILURE_READ_PRIVATE_KEY(BAD_REQUEST, "Private key 읽기 실패"),
// 404
FAILURE_READ_PRIVATE_KEY(BAD_REQUEST, "Private key 읽기 실패"),
// 404

NOT_FOUNT_REGISTERED_TEAM(NOT_FOUND, "등록되지 않은 팀입니다."),
NOT_FOUND_USER_SOCIAL_IDENTITY_INFO(NOT_FOUND, "등록된 소셜 정보가 없습니다."),
;

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.makers.operation.code.success.auth;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.sopt.makers.operation.code.success.SuccessCode;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.OK;

@Getter
@RequiredArgsConstructor
public enum AuthSuccessCode implements SuccessCode {
SUCCESS_GET_AUTHORIZATION_CODE(OK, "플랫폼 인가코드 발급 성공");
private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ public class ValueConfig {
private String playGroundURI;
@Value("${sopt.makers.playground.token}")
private String playGroundToken;
@Value("${oauth.apple.key.id}")
private String appleKeyId;
@Value("${oauth.apple.team.id}")
private String appleTeamId;
@Value("${oauth.apple.aud}")
private String appleAud;
@Value("${oauth.apple.sub}")
private String appleSub;
@Value("${oauth.google.client.id}")
private String googleClientId;
@Value("${oauth.google.client.secret}")
private String googleClientSecret;
@Value("${oauth.google.redirect.url}")
private String googleRedirectUrl;
@Value("${spring.jwt.secretKey.platform_code}")
private String platformCodeSecretKey;

private final int SUB_LECTURE_MAX_ROUND = 2;
private final int MAX_LECTURE_COUNT = 2;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.sopt.makers.operation.exception;

import lombok.Getter;
import org.sopt.makers.operation.code.failure.FailureCode;

@Getter
public class AuthException extends RuntimeException{
private final FailureCode failureCode;

public AuthException(FailureCode failureCode) {
super("[AuthException] : " + failureCode.getMessage());
this.failureCode = failureCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.sopt.makers.operation.auth.domain;

public enum Team {
PLAYGROUND, CREW, APP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.makers.operation.auth.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

import static jakarta.persistence.GenerationType.IDENTITY;

@Entity
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2

  1. 별 다른 Builder를 사용하지 않은 이유가 따로 있을까요??
  2. 해당 객체에 대한 Getter의 필요성은 없을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 해당 엔티티는 db를 통해 insert하기 때문에 save 등을 사용하지 않아 entity를 생성할 일이 없기 때문에 만들지 않았습니다

  2. getter 또한 jpa query문을 통해 충분히 검증할 수 있기 때문에 만들지 않았습니다

위 builder 및 getter 모두 추후 필요한 상황이 생긴다면 만들 것 같습니다.

public class TeamOAuthInfo {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(name = "client_id", nullable = false)
private String clientId;
@Column(name = "redirect_uri", nullable = false)
private String redirectUri;
@Column(name = "team", nullable = false)
@Enumerated(EnumType.STRING)
private Team team;
}
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.sopt.makers.operation.auth.repository;

import org.sopt.makers.operation.auth.domain.TeamOAuthInfo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TeamOAuthInfoRepository extends JpaRepository<TeamOAuthInfo, Long> {
boolean existsByClientIdAndRedirectUri(String clientId, String redirectUri);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package org.sopt.makers.operation.user.domain;

import java.util.Arrays;

public enum SocialType {
GOOGLE,
APPLE,
APPLE;


public static boolean isContains(String type) {
SocialType[] socialTypes = SocialType.values();
return Arrays.stream(socialTypes)
.anyMatch(socialType -> socialType.name().equals(type));
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.sopt.makers.operation.user.repository.identityinfo;

import org.sopt.makers.operation.user.domain.SocialType;
import org.sopt.makers.operation.user.domain.UserIdentityInfo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserIdentityInfoRepository extends JpaRepository<UserIdentityInfo, Long> {
Optional<UserIdentityInfo> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
}
10 changes: 10 additions & 0 deletions operation-external/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ dependencies {
implementation project(path: ':operation-domain')

implementation 'org.springframework.boot:spring-boot-starter-web'

implementation 'org.bouncycastle:bcprov-jdk18on:1.75'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.75'
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved

// jwt builder library
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly "io.jsonwebtoken:jjwt-impl:0.11.2"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:0.11.2"
// jwt payload read library
implementation "com.nimbusds:nimbus-jose-jwt:7.8.1"
}

test {
Expand Down
Loading
Loading