Skip to content
Open
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
@@ -0,0 +1,63 @@
package com.example.solidconnection.admin.auth.controller;

import com.example.solidconnection.admin.auth.dto.AdminReissueResponse;
import com.example.solidconnection.admin.auth.dto.AdminSignInRequest;
import com.example.solidconnection.admin.auth.dto.AdminSignInResponse;
import com.example.solidconnection.admin.auth.dto.AdminSignInResult;
import com.example.solidconnection.admin.auth.service.AdminAuthService;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.common.exception.ErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/admin/auth")
@RequiredArgsConstructor
public class AdminAuthController {

private final AdminAuthService adminAuthService;
private final AdminRefreshTokenCookieManager adminRefreshTokenCookieManager;

@PostMapping("/sign-in")
public ResponseEntity<AdminSignInResponse> signIn(
@RequestBody @Valid AdminSignInRequest request,
HttpServletResponse response
) {
AdminSignInResult result = adminAuthService.signIn(request);
adminRefreshTokenCookieManager.setCookie(response, result.adminRefreshToken());
return ResponseEntity.ok(AdminSignInResponse.from(result.accessToken()));
}

@PostMapping("/reissue")
public ResponseEntity<AdminReissueResponse> reissue(HttpServletRequest request) {
String adminRefreshToken = adminRefreshTokenCookieManager.getAdminRefreshToken(request);
AdminReissueResponse reissueResponse = adminAuthService.reissue(adminRefreshToken);
return ResponseEntity.ok(reissueResponse);
}

@PostMapping("/sign-out")
public ResponseEntity<Void> signOut(
Authentication authentication,
HttpServletResponse response
) {
String accessToken = getAccessToken(authentication);
adminAuthService.signOut(accessToken);
adminRefreshTokenCookieManager.deleteCookie(response);
return ResponseEntity.ok().build();
}

private String getAccessToken(Authentication authentication) {
if (authentication == null || !(authentication.getCredentials() instanceof String accessToken)) {
throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "엑세스 토큰이 없습니다.");
}
return accessToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.solidconnection.admin.auth.controller;

import static com.example.solidconnection.common.exception.ErrorCode.ADMIN_REFRESH_TOKEN_NOT_EXISTS;

import com.example.solidconnection.admin.auth.controller.config.AdminRefreshTokenCookieProperties;
import com.example.solidconnection.auth.token.config.TokenProperties;
import com.example.solidconnection.common.exception.CustomException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AdminRefreshTokenCookieManager {

private static final String PATH = "/";

private final AdminRefreshTokenCookieProperties properties;
private final TokenProperties tokenProperties;

public void setCookie(HttpServletResponse response, String adminRefreshToken) {
Duration tokenExpireTime = tokenProperties.adminRefresh().expireTime();
long cookieMaxAge = tokenExpireTime.toSeconds();
setAdminRefreshTokenCookie(response, adminRefreshToken, cookieMaxAge);
}

public void deleteCookie(HttpServletResponse response) {
setAdminRefreshTokenCookie(response, "", 0);
}

private void setAdminRefreshTokenCookie(
HttpServletResponse response, String adminRefreshToken, long maxAge
) {
ResponseCookie cookie = ResponseCookie.from(properties.cookieName(), adminRefreshToken)
.httpOnly(true)
.secure(true)
.path(PATH)
.maxAge(maxAge)
.domain(properties.cookieDomain())
.sameSite(SameSite.LAX.attributeValue())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

기존 LAX 방식이었던 이유가 어떤 거였는지 알려주실 수 있나요?? 인증 정보가 관리지 웹(www.admins.solid-connection.com)과 서비스 웹(www.solid-connection.com)으로 완전 분리되었으니까 Strict를 해도 되지 않을까라는 생각이 들어 의견을 듣고 싶습니다!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

저 합류 이전이라 근거는 잘 모르겠습니다.

그리고 현재 쿠키 도메인이 .solid-connection.com 로 설정되어 있어, Strict을 사용해도 쿠키를 분리하는 효과를 기대하긴 어려울 거 같습니다 ..!

.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}

public String getAdminRefreshToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
throw new CustomException(ADMIN_REFRESH_TOKEN_NOT_EXISTS);
}

Cookie adminRefreshTokenCookie = Arrays.stream(cookies)
.filter(cookie -> properties.cookieName().equals(cookie.getName()))
.findFirst()
.orElseThrow(() -> new CustomException(ADMIN_REFRESH_TOKEN_NOT_EXISTS));

String adminRefreshToken = adminRefreshTokenCookie.getValue();
if (adminRefreshToken == null || adminRefreshToken.isBlank()) {
throw new CustomException(ADMIN_REFRESH_TOKEN_NOT_EXISTS);
}
return adminRefreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.admin.auth.controller.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "token.admin-refresh")
public record AdminRefreshTokenCookieProperties(
String cookieName,
String cookieDomain
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.admin.auth.dto;

import com.example.solidconnection.auth.domain.AccessToken;

public record AdminReissueResponse(
String accessToken
) {

public static AdminReissueResponse from(AccessToken accessToken) {
return new AdminReissueResponse(accessToken.token());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record AdminSignInRequest(
@NotBlank String email,
@NotBlank String password
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.auth.dto;

public record AdminSignInResponse(
String accessToken
) {

public static AdminSignInResponse from(String accessToken) {
return new AdminSignInResponse(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.solidconnection.admin.auth.dto;

import com.example.solidconnection.auth.domain.AccessToken;
import com.example.solidconnection.auth.domain.AdminRefreshToken;

public record AdminSignInResult(
String accessToken,
String adminRefreshToken
) {

public static AdminSignInResult of(
AccessToken accessToken,
AdminRefreshToken adminRefreshToken
) {
return new AdminSignInResult(accessToken.token(), adminRefreshToken.token());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.example.solidconnection.admin.auth.service;

import static com.example.solidconnection.common.exception.ErrorCode.ADMIN_REFRESH_TOKEN_EXPIRED;
import static com.example.solidconnection.common.exception.ErrorCode.NOT_ADMIN_USER;
import static com.example.solidconnection.common.exception.ErrorCode.SIGN_IN_FAILED;

import com.example.solidconnection.admin.auth.dto.AdminReissueResponse;
import com.example.solidconnection.admin.auth.dto.AdminSignInRequest;
import com.example.solidconnection.admin.auth.dto.AdminSignInResult;
import com.example.solidconnection.auth.domain.AccessToken;
import com.example.solidconnection.auth.domain.AdminRefreshToken;
import com.example.solidconnection.auth.exception.AuthException;
import com.example.solidconnection.auth.service.AuthTokenProvider;
import com.example.solidconnection.auth.token.TokenBlackListService;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.AuthType;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class AdminAuthService {

private final AuthTokenProvider authTokenProvider;
private final TokenBlackListService tokenBlackListService;
private final SiteUserRepository siteUserRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public AdminSignInResult signIn(AdminSignInRequest request) {
SiteUser siteUser = getEmailMatchingUserOrThrow(request.email());
validatePassword(request.password(), siteUser.getPassword());
validateAdminRole(siteUser);
resetQuitedAt(siteUser);
AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
AdminRefreshToken adminRefreshToken = authTokenProvider.generateAndSaveAdminRefreshToken(siteUser);
return AdminSignInResult.of(accessToken, adminRefreshToken);
}

private SiteUser getEmailMatchingUserOrThrow(String email) {
return siteUserRepository.findByEmailAndAuthType(email, AuthType.EMAIL)
.orElseThrow(() -> new CustomException(SIGN_IN_FAILED));
}

private void validatePassword(String rawPassword, String encodedPassword) {
if (!passwordEncoder.matches(rawPassword, encodedPassword)) {
throw new CustomException(SIGN_IN_FAILED);
}
}

private void validateAdminRole(SiteUser siteUser) {
if (!Role.ADMIN.equals(siteUser.getRole())) {
throw new CustomException(NOT_ADMIN_USER);
}
}

private void resetQuitedAt(SiteUser siteUser) {
if (siteUser.getQuitedAt() == null) {
return;
}
siteUser.setQuitedAt(null);
}

public AdminReissueResponse reissue(String requestedAdminRefreshToken) {
if (!authTokenProvider.isValidAdminRefreshToken(requestedAdminRefreshToken)) {
Comment thread
whqtker marked this conversation as resolved.
throw new AuthException(ADMIN_REFRESH_TOKEN_EXPIRED);
}
SiteUser siteUser = authTokenProvider.parseSiteUser(requestedAdminRefreshToken);
AccessToken newAccessToken = authTokenProvider.generateAccessToken(siteUser);
return AdminReissueResponse.from(newAccessToken);
}

public void signOut(String accessToken) {
tokenBlackListService.addToBlacklist(accessToken);
authTokenProvider.deleteAdminRefreshTokenByAccessToken(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.solidconnection.auth.domain;

public record AdminRefreshToken(
String token
) implements Token {

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

import com.example.solidconnection.auth.domain.AccessToken;
import com.example.solidconnection.auth.domain.AdminRefreshToken;
import com.example.solidconnection.auth.domain.RefreshToken;
import com.example.solidconnection.auth.domain.Subject;
import com.example.solidconnection.auth.token.config.TokenProperties;
Expand Down Expand Up @@ -54,6 +55,16 @@ public RefreshToken generateAndSaveRefreshToken(SiteUser siteUser) {
return tokenStorage.saveToken(subject, refreshToken);
}

public AdminRefreshToken generateAndSaveAdminRefreshToken(SiteUser siteUser) {
Subject subject = toSubject(siteUser);
String token = tokenProvider.generateToken(
subject,
tokenProperties.adminRefresh().expireTime()
);
AdminRefreshToken adminRefreshToken = new AdminRefreshToken(token);
return tokenStorage.saveToken(subject, adminRefreshToken);
}

/*
* 유효한 리프레시 토큰인지 확인한다.
* - 요청된 토큰과 같은 subject 의 리프레시 토큰을 조회한다.
Expand All @@ -66,11 +77,23 @@ public boolean isValidRefreshToken(String requestedRefreshToken) {
.orElse(false);
}

public boolean isValidAdminRefreshToken(String requestedAdminRefreshToken) {
Subject subject = tokenProvider.parseSubject(requestedAdminRefreshToken);
return tokenStorage.findToken(subject, AdminRefreshToken.class)
.map(foundToken -> Objects.equals(foundToken, requestedAdminRefreshToken))
.orElse(false);
}
Comment thread
whqtker marked this conversation as resolved.

public void deleteRefreshTokenByAccessToken(String accessToken) {
Subject subject = tokenProvider.parseSubject(accessToken);
tokenStorage.deleteToken(subject, RefreshToken.class);
}

public void deleteAdminRefreshTokenByAccessToken(String accessToken) {
Subject subject = tokenProvider.parseSubject(accessToken);
tokenStorage.deleteToken(subject, AdminRefreshToken.class);
}

public SiteUser parseSiteUser(String token) {
Subject subject = tokenProvider.parseSubject(token);
long siteUserId = Long.parseLong(subject.value());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.solidconnection.auth.token.config;

import com.example.solidconnection.auth.domain.AccessToken;
import com.example.solidconnection.auth.domain.AdminRefreshToken;
import com.example.solidconnection.auth.domain.RefreshToken;
import com.example.solidconnection.auth.domain.SignUpToken;
import com.example.solidconnection.auth.domain.Token;
Expand All @@ -13,6 +14,7 @@
public record TokenProperties(
TokenConfig access,
TokenConfig refresh,
TokenConfig adminRefresh,
TokenConfig signUp,
TokenConfig blackList
) {
Expand All @@ -32,6 +34,7 @@ public void init() {
tokenConfigs = Map.of(
AccessToken.class, access,
RefreshToken.class, refresh,
AdminRefreshToken.class, adminRefresh,
SignUpToken.class, signUp
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.example.solidconnection.common.exception;

import static com.example.solidconnection.common.exception.ErrorCode.ADMIN_REFRESH_TOKEN_EXPIRED;
import static com.example.solidconnection.common.exception.ErrorCode.DATA_INTEGRITY_VIOLATION;
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_INPUT;
import static com.example.solidconnection.common.exception.ErrorCode.JSON_PARSING_FAILED;
import static com.example.solidconnection.common.exception.ErrorCode.JWT_EXCEPTION;
import static com.example.solidconnection.common.exception.ErrorCode.NOT_DEFINED_ERROR;
import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;

import com.example.solidconnection.admin.auth.controller.AdminRefreshTokenCookieManager;
import com.example.solidconnection.auth.controller.RefreshTokenCookieManager;
import com.example.solidconnection.auth.exception.AuthException;
import com.example.solidconnection.common.response.ErrorResponse;
Expand All @@ -30,6 +32,7 @@
public class CustomExceptionHandler {

private final RefreshTokenCookieManager refreshTokenCookieManager;
private final AdminRefreshTokenCookieManager adminRefreshTokenCookieManager;

@ExceptionHandler(AuthException.class)
protected ResponseEntity<ErrorResponse> handleAuthException(
Expand All @@ -40,6 +43,9 @@ protected ResponseEntity<ErrorResponse> handleAuthException(
if (ex.getErrorCode().equals(REFRESH_TOKEN_EXPIRED)) {
refreshTokenCookieManager.deleteCookie(response);
}
if (ex.getErrorCode().equals(ADMIN_REFRESH_TOKEN_EXPIRED)) {
adminRefreshTokenCookieManager.deleteCookie(response);
}
ErrorResponse errorResponse = new ErrorResponse(ex);
return ResponseEntity
.status(ex.getCode())
Expand Down
Loading
Loading