Skip to content

Commit

Permalink
Merge pull request #262 from sopt-makers/feat/#261_T-10856
Browse files Browse the repository at this point in the history
#261_T-10856 [feat] /token API 구현
  • Loading branch information
KWY0218 authored Jun 4, 2024
2 parents 4f2dab2 + dea7924 commit 35d3a67
Show file tree
Hide file tree
Showing 14 changed files with 515 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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.auth.dto.request.AccessTokenRequest;
import org.sopt.makers.operation.dto.BaseResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down Expand Up @@ -45,4 +46,37 @@ ResponseEntity<BaseResponse<?>> authorize(
@RequestParam String clientId,
@RequestParam String redirectUri
);

@Operation(
security = @SecurityRequirement(name = "Authorization"),
summary = "인증 토큰 발급 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "토큰 발급 성공"
),
@ApiResponse(
responseCode = "400",
description = """
1. grantType 데이터가 들어오지 않았습니다.\n
2. 유효하지 않은 grantType 입니다.\n
3. 플랫폼 인가코드가 들어오지 않았습니다.\n
4. 이미 사용한 플랫폼 인가코드입니다.\n
5. 리프레쉬 토큰이 들어오지 않았습니다.
"""
),
@ApiResponse(
responseCode = "401",
description = """
1. 만료된 플랫폼 인가 코드입니다.\n
2. 만료된 리프레쉬 토큰입니다.
"""
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
}
)
ResponseEntity<BaseResponse<?>> token(AccessTokenRequest accessTokenRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,49 @@

import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.auth.dto.request.AccessTokenRequest;
import org.sopt.makers.operation.auth.dto.response.AuthorizationCodeResponse;
import org.sopt.makers.operation.auth.dto.response.TokenResponse;
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.jwt.JwtTokenProvider;
import org.sopt.makers.operation.user.domain.SocialType;
import org.sopt.makers.operation.util.ApiResponseUtil;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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.EXPIRED_PLATFORM_CODE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.EXPIRED_REFRESH_TOKEN;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_GRANT_TYPE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_SOCIAL_TYPE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_FOUNT_REGISTERED_TEAM;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_CODE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_GRANT_TYPE;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_REFRESH_TOKEN;
import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.USED_PLATFORM_CODE;
import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GENERATE_TOKEN;
import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GET_AUTHORIZATION_CODE;
import static org.sopt.makers.operation.jwt.JwtTokenType.PLATFORM_CODE;
import static org.sopt.makers.operation.jwt.JwtTokenType.REFRESH_TOKEN;

@RestController
@RequiredArgsConstructor
public class AuthApiController implements AuthApi {

private final ConcurrentHashMap<String, String> tempPlatformCode = new ConcurrentHashMap<>();
private static final String AUTHORIZATION_CODE_GRANT_TYPE = "authorizationCode";
private static final String REFRESH_TOKEN_GRANT_TYPE = "refreshToken";

private final ConcurrentHashMap<String, String> tempPlatformCode;
private final AuthService authService;
private final JwtTokenProvider jwtTokenProvider;

@Override
@GetMapping("/api/v1/authorize")
Expand All @@ -46,14 +66,69 @@ public ResponseEntity<BaseResponse<?>> authorize(
return ApiResponseUtil.success(SUCCESS_GET_AUTHORIZATION_CODE, new AuthorizationCodeResponse(platformCode));
}

@Override
@PostMapping(
path = "/api/v1/token",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<BaseResponse<?>> token(AccessTokenRequest accessTokenRequest) {
if (accessTokenRequest.isNullGrantType()) {
throw new AuthException(NOT_NULL_GRANT_TYPE);
}

val grantType = accessTokenRequest.grantType();
if (!(grantType.equals(AUTHORIZATION_CODE_GRANT_TYPE) || grantType.equals(REFRESH_TOKEN_GRANT_TYPE))) {
throw new AuthException(INVALID_GRANT_TYPE);
}

val tokenResponse = grantType.equals(AUTHORIZATION_CODE_GRANT_TYPE) ?
generateTokenResponseByAuthorizationCode(accessTokenRequest) : generateTokenResponseByRefreshToken(accessTokenRequest);
return ApiResponseUtil.success(SUCCESS_GENERATE_TOKEN, tokenResponse);
}

private TokenResponse generateTokenResponseByAuthorizationCode(AccessTokenRequest accessTokenRequest) {
if (accessTokenRequest.isNullCode()) {
throw new AuthException(NOT_NULL_CODE);
}
if (!tempPlatformCode.contains(accessTokenRequest.code())) {
throw new AuthException(USED_PLATFORM_CODE);
}
tempPlatformCode.remove(accessTokenRequest.code());

if (!jwtTokenProvider.validatePlatformCode(accessTokenRequest.code(), accessTokenRequest.clientId(), accessTokenRequest.redirectUri())) {
throw new AuthException(EXPIRED_PLATFORM_CODE);
}

val authentication = jwtTokenProvider.getAuthentication(accessTokenRequest.code(), PLATFORM_CODE);
return generateTokenResponse(authentication);
}

private TokenResponse generateTokenResponseByRefreshToken(AccessTokenRequest accessTokenRequest) {
if (accessTokenRequest.isNullRefreshToken()) {
throw new AuthException(NOT_NULL_REFRESH_TOKEN);
}
if (!jwtTokenProvider.validateTokenExpiration(accessTokenRequest.refreshToken(), REFRESH_TOKEN)) {
throw new AuthException(EXPIRED_REFRESH_TOKEN);
}

val authentication = jwtTokenProvider.getAuthentication(accessTokenRequest.refreshToken(), REFRESH_TOKEN);
return generateTokenResponse(authentication);
}

private TokenResponse generateTokenResponse(Authentication authentication) {
val accessToken = jwtTokenProvider.generateAccessToken(authentication);
val refreshToken = jwtTokenProvider.generateRefreshToken(authentication);
return TokenResponse.of(accessToken, refreshToken);
}

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(String clientId, String redirectUri, Long userId) {
val platformCode = authService.generatePlatformCode(clientId, redirectUri, userId);
val platformCode = jwtTokenProvider.generatePlatformCode(clientId, redirectUri, userId);
tempPlatformCode.putIfAbsent(platformCode, platformCode);
return platformCode;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.sopt.makers.operation.auth.dto.request;

public record AccessTokenRequest(
String grantType,
String clientId,
String redirectUri,
String code,
String refreshToken
) {
public boolean isNullGrantType() {
return grantType == null;
}

public boolean isNullCode() {
return code == null;
}

public boolean isNullRefreshToken() {
return refreshToken == null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.makers.operation.auth.dto.response;

public record TokenResponse(String tokenType, String accessToken, String refreshToken) {
private static final String BEARER_TOKEN_TYPE = "Bearer";

public static TokenResponse of(String accessToken, String refreshToken) {
return new TokenResponse(BEARER_TOKEN_TYPE, accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,4 @@ public interface AuthService {
String getSocialUserInfo(SocialType type, String code);

Long getUserId(SocialType socialType, String userSocialId);

String generatePlatformCode(String clientId, String redirectUri, Long userId);
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
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;

@Override
public boolean checkRegisteredTeamOAuthInfo(String clientId, String redirectUri) {
Expand All @@ -51,22 +40,4 @@ public Long getUserId(SocialType socialType, String userSocialId) {
.orElseThrow(() -> new AuthException(NOT_FOUND_USER_SOCIAL_IDENTITY_INFO));
return userIdentityInfo.getUserId();
}

@Override
public String generatePlatformCode(String clientId, String redirectUri, 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()
.setIssuer(clientId)
.setAudience(redirectUri)
.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,14 @@
package org.sopt.makers.operation.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ConcurrentHashMap;

@Configuration
public class ConcurrentHashMapConfig {
@Bean
public ConcurrentHashMap<String, String> registerConcurrentHashMap() {
return new ConcurrentHashMap<>();
}
}
Loading

0 comments on commit 35d3a67

Please sign in to comment.