Skip to content
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
109 changes: 109 additions & 0 deletions src/main/java/org/runimo/runimo/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.runimo.runimo.auth.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.auth.controller.request.AppleLoginRequest;
import org.runimo.runimo.auth.controller.request.AuthSignupRequest;
import org.runimo.runimo.auth.controller.request.KakaoLoginRequest;
import org.runimo.runimo.auth.service.OidcService;
import org.runimo.runimo.auth.service.SignUpUsecase;
import org.runimo.runimo.auth.service.TokenRefreshService;
import org.runimo.runimo.auth.service.dtos.AuthResponse;
import org.runimo.runimo.auth.service.dtos.SignupUserResponse;
import org.runimo.runimo.auth.service.dtos.TokenPair;
import org.runimo.runimo.common.response.SuccessResponse;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;

@Tag(name = "Auth API", description = "인증 관련 API 모음")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final OidcService oidcService;
private final TokenRefreshService tokenRefreshService;
private final SignUpUsecase signUpUsecase;

@Operation(summary = "카카오 소셜 로그인", description = "카카오 OIDC 토큰을 이용하여 로그인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "404", description = "등록되지 않은 사용자")
})
@PostMapping("/kakao")
public ResponseEntity<SuccessResponse<AuthResponse>> kakaoLogin(
@RequestBody KakaoLoginRequest request
) {
AuthResponse res = oidcService.kakaoLogin(request.oidcToken());
return ResponseEntity.ok().body(
SuccessResponse.of(
UserHttpResponseCode.LOGIN_SUCCESS,
res
)
);
}

@Operation(summary = "애플 소셜 로그인", description = "애플 OIDC 토큰을 이용하여 로그인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "404", description = "등록되지 않은 사용자")
})
@PostMapping("/apple")
public ResponseEntity<SuccessResponse<AuthResponse>> appleLogin(
@RequestBody final AppleLoginRequest request
) {
AuthResponse res = oidcService.appleLogin(request.authCode(), request.codeVerifier());
return ResponseEntity.ok().body(
SuccessResponse.of(
UserHttpResponseCode.LOGIN_SUCCESS,
res
)
);
}

@Operation(summary = "사용자 회원가입 및 로그인", description = "사용자가 OIDC 토큰을 사용하여 회원가입 후 로그인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "회원가입 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"),
@ApiResponse(responseCode = "409", description = "이미 존재하는 사용자")
})
@PostMapping("/signup")
public ResponseEntity<SuccessResponse<SignupUserResponse>> signupAndLogin(
@Valid @RequestBody AuthSignupRequest request) {
SignupUserResponse authResult = signUpUsecase.register(
request.toUserSignupCommand()
);
return ResponseEntity.created(URI.create("/api/v1/user" + authResult.userId()))
.body(SuccessResponse.of(
UserHttpResponseCode.SIGNUP_SUCCESS,
authResult)
);
}

@Operation(summary = "토큰 갱신", description = "리프레시 토큰을 사용하여 액세스 토큰을 갱신합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토큰 갱신 성공"),
@ApiResponse(responseCode = "401", description = "유효하지 않은 토큰")
})
@PostMapping("/refresh")
public ResponseEntity<SuccessResponse<TokenPair>> refreshToken(
@RequestHeader("Authorization") String refreshToken) {
String token = refreshToken.replace("Bearer ", "");
TokenPair newTokens = tokenRefreshService.refreshAccessToken(token);
return ResponseEntity.ok().body(
SuccessResponse.of(
UserHttpResponseCode.REFRESH_SUCCESS,
newTokens
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.runimo.runimo.auth.controller.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

@Schema(description = "애플 로그인 요청")
public record AppleLoginRequest(
@Schema(description = "인증 코드")
@NotBlank String authCode,
@Schema(description = "Code Verifier")
@NotBlank String codeVerifier
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.runimo.runimo.auth.controller.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.URL;
import org.runimo.runimo.auth.service.dtos.UserSignupCommand;

@Schema(description = "사용자 회원가입 요청 DTO")
public record AuthSignupRequest(
@Schema(description = "회원가입용 임시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI...")
@NotBlank String registerToken,

@Schema(description = "사용자 닉네임", example = "RunimoUser")
@NotBlank String nickname,

@Schema(description = "프로필 이미지 URL", example = "https://example.com/image.jpg")
@URL String imgUrl
) {
public UserSignupCommand toUserSignupCommand() {
return new UserSignupCommand(registerToken, nickname, imgUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.runimo.runimo.auth.controller.request;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "카카오 로그인 요청")
public record KakaoLoginRequest(
@Schema(description = "카카오 oidc토큰")
String oidcToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.user.exceptions;
package org.runimo.runimo.auth.exceptions;

import org.runimo.runimo.exceptions.BusinessException;
import org.runimo.runimo.exceptions.code.CustomResponseCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.runimo.runimo.auth.exceptions;

import org.runimo.runimo.exceptions.BusinessException;
import org.runimo.runimo.exceptions.code.CustomResponseCode;


public class UnRegisteredUserException extends BusinessException {
private final String temporalRegisterToken;

protected UnRegisteredUserException(CustomResponseCode errorCode, String temporalRegisterToken) {
super(errorCode);
this.temporalRegisterToken = temporalRegisterToken;
}

protected UnRegisteredUserException(CustomResponseCode errorCode, String logMessage, String temporalRegisterToken) {
super(errorCode, logMessage);
this.temporalRegisterToken = temporalRegisterToken;
}

public static UnRegisteredUserException of(CustomResponseCode errorCode, String token) {
return new UnRegisteredUserException(errorCode, token);
}

public String getTemporalRegisterToken() {
return temporalRegisterToken;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.user.exceptions;
package org.runimo.runimo.auth.exceptions;

import org.runimo.runimo.exceptions.BusinessException;
import org.runimo.runimo.exceptions.code.CustomResponseCode;
Expand All @@ -11,4 +11,8 @@ protected UserJwtException(CustomResponseCode errorCode) {
public UserJwtException(CustomResponseCode errorCode, String logMessage) {
super(errorCode, logMessage);
}

public static UserJwtException of(CustomResponseCode errorCode) {
return new UserJwtException(errorCode, errorCode.getLogMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private String extractToken(HttpServletRequest request) {

private boolean processToken(String jwtToken, HttpServletResponse response) throws IOException {
try {
String userId = jwtResolver.getUserIdFromAccessToken(jwtToken);
String userId = jwtResolver.getUserIdFromJwtToken(jwtToken);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userId,
null,
Expand Down
24 changes: 21 additions & 3 deletions src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.runimo.runimo.auth.exceptions.UserJwtException;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.enums.UserHttpResponseCode;

public class JwtResolver {

Expand All @@ -14,12 +17,27 @@ public JwtResolver(String jwtSecret) {
this.jwtSecret = jwtSecret;
}

public DecodedJWT verifyAccessToken(String token) throws JWTVerificationException {
public DecodedJWT verifyJwtToken(String token) throws JWTVerificationException {
return JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(ISSUER).build().verify(token);
}

public String getUserIdFromAccessToken(String token) throws JWTVerificationException {
DecodedJWT jwt = verifyAccessToken(token);
public String getUserIdFromJwtToken(String token) throws JWTVerificationException {
DecodedJWT jwt = verifyJwtToken(token);
return jwt.getSubject();
}

public RegisterTokenPayload getRegisterTokenPayload(String token) throws JWTVerificationException {
try {
DecodedJWT decodedJWT = verifyJwtToken(token);
String providerId = decodedJWT.getClaim("provider_id").asString();
SocialProvider socialProvider = SocialProvider.valueOf(
decodedJWT.getClaim("provider").asString()
);
return new RegisterTokenPayload(providerId, socialProvider);
} catch (JWTVerificationException e) {
throw UserJwtException.of(UserHttpResponseCode.TOKEN_INVALID);
}
}


}
22 changes: 20 additions & 2 deletions src/main/java/org/runimo/runimo/auth/jwt/JwtTokenFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.runimo.runimo.auth.service.dtos.TokenPair;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.service.dtos.TokenPair;

import java.util.Date;
import java.util.UUID;

public class JwtTokenFactory {

Expand All @@ -14,11 +16,13 @@ public class JwtTokenFactory {
private final String jwtSecret;
private final long jwtExpiration;
private final long jwtRefreshExpiration;
private final long tempJwtExpiration;

public JwtTokenFactory(String jwtSecret, long jwtExpiration, long jwtRefreshExpiration) {
public JwtTokenFactory(String jwtSecret, long jwtExpiration, long jwtRefreshExpiration, long tempJwtExpiration) {
this.jwtSecret = jwtSecret;
this.jwtExpiration = jwtExpiration;
this.jwtRefreshExpiration = jwtRefreshExpiration;
this.tempJwtExpiration = tempJwtExpiration;
}

public String generateAccessToken(String userPublicId) {
Expand Down Expand Up @@ -46,6 +50,20 @@ public String generateRefreshToken(String userPublicId) {
.sign(Algorithm.HMAC256(jwtSecret));
}

public String generateRegisterTemporalToken(String providerId, SocialProvider socialProvider) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + tempJwtExpiration);
return JWT.create()
.withSubject(UUID.randomUUID().toString())
.withIssuedAt(now)
.withExpiresAt(expiryDate)
.withIssuer(ISSUER)
.withClaim("provider_id", providerId)
.withClaim("provider", socialProvider.name())
.withClaim("tokenType", "register")
.sign(Algorithm.HMAC256(jwtSecret));
}

public TokenPair generateTokenPair(User user) {
String accessToken = generateAccessToken(user.getPublicId());
String refreshToken = generateRefreshToken(user.getPublicId());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.runimo.runimo.auth.jwt;

import org.runimo.runimo.user.domain.SocialProvider;

public record RegisterTokenPayload(
String providerId,
SocialProvider socialProvider
) {
}

This file was deleted.

This file was deleted.

Loading