Skip to content
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.STRICT.attributeValue())
.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
Expand Up @@ -49,7 +49,7 @@ private void setRefreshTokenCookie(
.path(PATH)
.maxAge(maxAge)
.domain(properties.cookieDomain())
.sameSite(SameSite.LAX.attributeValue())
.sameSite(SameSite.STRICT.attributeValue())
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
Expand Down
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
Loading
Loading