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
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,31 @@

public interface TokenRotationService {

void issueRefreshToken(String userId, HttpServletResponse response);
/**
* Issues a refresh token for the specified user and writes it to the provided HTTP response.
*
* @param userId the identifier of the user to issue the refresh token for
* @param response the HTTP response to which the refresh token will be written
*/
void issueRefreshToken(String userId, HttpServletResponse response);

RotateTokenResponse issueTokens(String userId, HttpServletResponse response);
/**
* Issues new access and refresh tokens for the specified user and writes token-related data to the provided HTTP response.
*
* @param userId the identifier of the user for whom tokens are issued
* @param response the HTTP response to which token data (e.g., headers or cookies) will be written
* @return a RotateTokenResponse containing the issued access token, refresh token, and associated metadata
*/
RotateTokenResponse issueTokens(String userId, HttpServletResponse response);

RotateTokenResponse rotateTokens(String refreshToken, HttpServletResponse response);
/**
* Rotate access and refresh tokens using the provided refresh token.
*
* Generates new tokens for the user associated with the given refresh token and may write token data (for example headers or cookies) to the HTTP response.
*
* @param refreshToken the refresh token used to validate the session and produce new tokens
* @param response the HTTP response to which token data may be written
* @return a RotateTokenResponse containing the newly issued access token, refresh token, and related metadata
*/
RotateTokenResponse rotateTokens(String refreshToken, HttpServletResponse response);
}

Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,29 @@ public class SecurityConfig {
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final PrincipalOAuth2UserService principalOauth2UserService;

/**
* Exposes the AuthenticationManager from the provided AuthenticationConfiguration as a Spring bean.
*
* @param authenticationConfiguration the AuthenticationConfiguration to obtain the AuthenticationManager from
* @return the configured AuthenticationManager for the application
* @throws Exception if the AuthenticationManager cannot be obtained from the provided configuration
*/
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

/**
* Configures and returns the application's HTTP security filter chain.
*
* <p>The chain enforces stateless sessions, disables CSRF, form login, and HTTP Basic,
* injects a JWT authentication filter, configures OAuth2 login with a custom user service
* and success handler, and provides custom JSON responses for unauthorized and forbidden errors.
*
* @return the configured SecurityFilterChain
* @throws Exception if the HttpSecurity configuration cannot be built
*/
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http,
AuthenticationManager authenticationManager) throws Exception {
Expand Down Expand Up @@ -66,6 +83,13 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http,
return http.build();
}

/**
* Writes a 401 Unauthorized JSON error response to the provided HttpServletResponse.
*
* @param response the HttpServletResponse to modify; its status is set to 401 and a JSON
* body with an authorization error message is written
* @throws IOException if an I/O error occurs while writing the response
*/
public void createUnauthorizedResponse(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
Expand All @@ -74,6 +98,15 @@ public void createUnauthorizedResponse(HttpServletResponse response) throws IOEx
);
}

/**
* Send a 403 Forbidden JSON response indicating insufficient access.
*
* Sets the HTTP status to 403, the content type to "application/json;charset=UTF-8",
* and writes a JSON object containing the `NOT_ENOUGH_ACCESS` message.
*
* @param response the HTTP response to modify
* @throws IOException if writing to the response fails
*/
public void createForbiddenResponse(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
private final UserIdArgumentResolver userIdArgumentResolver;
private final QAskerProperties qAskerProperties;

/**
* Configure global CORS settings for all request paths.
*
* <p>Allows requests from the frontend development and deployed origins, permits
* GET, POST, PUT and DELETE methods, enables credentials, and sets preflight
* cache duration to 3600 seconds.</p>
*
* @param registry the CorsRegistry to configure
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
Expand All @@ -26,8 +35,14 @@ public void addCorsMappings(CorsRegistry registry) {
.maxAge(3600);
}

/**
* Registers the configured UserIdArgumentResolver with Spring MVC's argument resolver list.
*
* @param resolvers the mutable list of HandlerMethodArgumentResolver instances provided by Spring MVC;
* the UserIdArgumentResolver will be appended to this list
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userIdArgumentResolver);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final TokenRotationService tokenRotationService;
private final QAskerProperties qAskerProperties;

/**
* Handles post-login actions after successful OAuth2 authentication.
*
* Issues a refresh token for the authenticated user and redirects the client to the frontend login redirect URL.
*
* @param request the HTTP servlet request for the current authentication
* @param response the HTTP servlet response used to issue the refresh token and perform the redirect
* @param authentication the completed Authentication containing the authenticated principal
* @throws IOException if sending the redirect or writing to the response fails
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public class PrincipalOAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

/**
* Load an OAuth2 user from the external provider, create and persist a local User if one does not exist, and return a UserPrincipal.
*
* @param userRequest the OAuth2 user request containing client registration and token information
* @return a UserPrincipal containing the existing or newly created User and the provider's user attributes
*/
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest)
throws OAuth2AuthenticationException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,39 @@ public class AuthController {
private final TokenRotationService tokenRotationService;
private final LogoutService logoutService;

/**
* Health-check endpoint for the auth controller that responds with HTTP 200 OK.
*
* @return ResponseEntity with HTTP 200 OK and no body.
*/
@GetMapping("/test")
public ResponseEntity<?> test() {
System.out.println("test 성공");
return ResponseEntity.ok().build();
}

/**
* Rotate authentication tokens using the refresh token stored in the "refresh_token" cookie and send the new tokens in the response.
*
* @param request the incoming HTTP request which should contain the "refresh_token" cookie
* @param response the HTTP response where the rotated tokens (e.g., new cookies) will be written
* @return a RotateTokenResponse containing the newly issued tokens and related metadata
*/
@PostMapping("/refresh")
public ResponseEntity<RotateTokenResponse> refresh(HttpServletRequest request,
HttpServletResponse response) {
var rtCookie = CookieUtil.getCookie(request, "refresh_token").orElse(null);
return ResponseEntity.ok(tokenRotationService.rotateTokens(rtCookie.getValue(), response));
}

/**
* Invalidates the current user's authentication (clears session/cookies as applicable) and returns success.
*
* @return an HTTP 200 OK response with an empty body
*/
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request, HttpServletResponse response) {
logoutService.logout(request, response);
return ResponseEntity.ok().build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,37 @@ public class RefreshToken extends CreatedAt {
private String rtHash;
private Instant expiresAt;

/**
* Create a RefreshToken for the specified user with its stored hash and expiration time.
*
* @param userId the user's identifier (primary key)
* @param rtHash the stored refresh token hash
* @param expiresAt the instant when the token expires; may be {@code null} to indicate no expiration
*/
public RefreshToken(String userId, String rtHash, Instant expiresAt) {
this.userId = userId;
this.rtHash = rtHash;
this.expiresAt = expiresAt;
}

/**
* Replace the stored refresh token hash and its expiration timestamp.
*
* @param newRtHash the new refresh token hash to store
* @param newExpiresAt the new expiration timestamp; may be {@code null} to indicate no expiration
*/
public void rotate(String newRtHash, Instant newExpiresAt) {
this.rtHash = newRtHash;
this.expiresAt = newExpiresAt;
}

/**
* Check whether the refresh token has passed its expiration time.
*
* @param now the reference time to compare against the token's expiration
* @return `true` if `expiresAt` is before `now`, `false` otherwise
*/
public boolean isExpired(Instant now) {
return expiresAt != null && expiresAt.isBefore(now);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ public class User extends CreatedAt {
private String provider;
private String nickname;

/**
* Creates a User entity with the specified identifiers and profile attributes.
*
* @param userId the unique identifier for the user
* @param password accepted for builder compatibility; not stored on the entity
* @param role the user's role (e.g., "USER", "ADMIN")
* @param provider the authentication provider (e.g., "google", "github")
* @param nickname the user's display name
*/
@Builder
public User(String userId, String password, String role, String provider, String nickname) {
this.userId = userId;
this.role = role;
this.provider = provider;
this.nickname = nickname;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,35 @@ public class UserPrincipal implements OAuth2User {
private User user;
private Map<String, Object> attributes;

/**
* Principal name that identifies the application user.
*
* @return the user's identifier (`userId`) as a String
*/
@Override
public String getName() {
return user.getUserId();
}

/**
* Provides the OAuth2 attributes associated with this principal.
*
* @return the attributes map supplied by the OAuth2 provider
*/
@Override
public Map<String, Object> getAttributes() {
return attributes;
}

/**
* Provide authorities granted to the user based on the user's role.
*
* @return a collection containing a single {@link GrantedAuthority} whose authority equals the user's role
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<GrantedAuthority>();
collect.add(() -> user.getRole());
return collect;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,11 @@
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {

Optional<RefreshToken> findByRtHash(String rtHash);
}
/**
* Finds a RefreshToken by its hashed token value.
*
* @param rtHash the hashed refresh token string to look up
* @return an Optional containing the matching RefreshToken if present, otherwise an empty Optional
*/
Optional<RefreshToken> findByRtHash(String rtHash);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,23 @@
@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {

/**
* Determine whether a controller method parameter should be resolved as a user ID.
*
* @param parameter the method parameter to inspect
* @return `true` if the parameter is annotated with `@UserId` and its type is `String`, `false` otherwise
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserId.class)
&& parameter.getParameterType().equals(String.class);
}

/**
* Resolve the current authenticated user's ID for a method argument annotated with {@code @UserId}.
*
* @return the resolved userId string if the request is authenticated and the principal exposes a userId; `null` if unauthenticated, anonymous, or the principal does not provide a userId
*/
@Override
public @Nullable Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
Expand All @@ -47,4 +58,4 @@ public boolean supportsParameter(MethodParameter parameter) {
}
return null;
}
}
}
Loading