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

#261_T-10856 [feat] /token API 구현 #262

Merged
merged 13 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved

@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;
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
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<>();
}
}
KWY0218 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading