Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
65b8073
chore: 파일명 끝 공백 제거
Jy000n Jun 28, 2026
f3443e2
chore: oauth2, redis, jwt 의존성 추가
Jy000n Jun 28, 2026
c16a213
git commit -m "feat: CORS 및 JWT 필터를 포함한 SecurityConfig 추가"
Jy000n Jun 28, 2026
3e85bde
git commit -m "feat: RedisConfig 추가"
Jy000n Jun 28, 2026
e52c701
Merge branch 'develop' of https://github.com/Team-Timo/Timo-Server in…
Jy000n Jun 29, 2026
012c19e
feat: 소셜 로그인을 위한 User 엔티티 설정
Jy000n Jun 29, 2026
fdb720e
chore: User 엔티티 파일 위치 변경
Jy000n Jun 29, 2026
2a6faf7
feat: 소셜 로그인을 위한 UserRepository 설정
Jy000n Jun 29, 2026
3f2bda6
feat: CustomOAuth2UserService 구현
Jy000n Jun 29, 2026
3e99a23
feat: CustomUserDetails 구현
Jy000n Jun 29, 2026
2f01497
feat: OAuthFailureHandler 구현
Jy000n Jun 29, 2026
1447905
feat: OAuthSuccessHandler 구현
Jy000n Jun 29, 2026
b7dce5a
feat: JwtTokenProvider 구현
Jy000n Jun 29, 2026
b1440b0
feat: RefreshTokenService 구현
Jy000n Jun 29, 2026
a0529cd
feat: JwtAuthenticationFilter 구현
Jy000n Jun 29, 2026
d5cea0e
fix: 오타 수정
Jy000n Jun 29, 2026
bc564f4
style: 불필요한 주석 제거
Jy000n Jun 29, 2026
f8fd683
chore: 불필요한 주석 제거
Jy000n Jun 29, 2026
f531a63
chore: 파일 끝에 개행문자 추가
Jy000n Jun 29, 2026
bef91e4
Merge branch 'feat/#9-google-oauth2-login' of https://github.com/Team…
Jy000n Jun 29, 2026
b6cb9b5
feat: access/refresh token 타입 검증 추가
Jy000n Jun 30, 2026
505eb23
feat: OAuth2 사용자 정보 검증 및 트랜잭션 처리 추가
Jy000n Jun 30, 2026
011a7a5
refactor: OAuth2 예외를 ErrorCode 기반으로 통일
Jy000n Jun 30, 2026
1f44da3
fix: Redis TTL 단위를 JWT refresh token 만료 시간 단위(s)와 일치하도록 수정
Jy000n Jun 30, 2026
cd4ca03
fix: refresh token null 비교 시 NPE 방지 처리
Jy000n Jun 30, 2026
91eb023
refactor: API JWT 인증 실패와 OAuth2 로그인 처리 분리
Jy000n Jun 30, 2026
e90308d
fix: 설정된 redirectUri를 사용, 쿠키 maxAge를 refresh token 만료 시간과 맞춤
Jy000n Jun 30, 2026
5da973e
fix: accessToken을 URL 쿼리 파라미터 대신 HttpOnly 쿠키로 전달하도록 수정
Jy000n Jun 30, 2026
9d40673
fix: JwtAuthenticationFilter에서 accessToken을 쿠키에서 읽도록 수정
Jy000n Jun 30, 2026
bf208c3
fix: CSRF 공격을 방지하기 위해 인증 쿠키의 SameSite를 Strict로 설정
Jy000n Jun 30, 2026
22a203a
fix: 환경 변수로 쿠키의 secure 플래그를 외부 설정으로 분리
Jy000n Jun 30, 2026
5de9d41
fix: 순환 의존성을 해결하기 위해 JwtAuthenticationEntryPoint 분리
Jy000n Jun 30, 2026
eabe1d3
fix: accessToken과 refreshToken의 쿠키 maxAge를 각각 분리하여 설정
Jy000n Jun 30, 2026
dcc9ce2
chore: 불필요한 import문 제거
Jy000n Jun 30, 2026
faa733c
refactor: OAuthFailureHandler ObjectMapper 직접 생성 → Bean 주입으로 변경
Jy000n Jun 30, 2026
c1db021
fix: accessToken을 응답 본문(response body)으로 전달하고 Authorization 헤더에서 읽도록 수정
Jy000n Jul 1, 2026
f3afcb7
refactor: User 엔티티 변수명 변경 (picture->imageUrl)
Jy000n Jul 2, 2026
88edebb
refactor: User 엔티티의 Provider enum을 별도 파일로 분리
Jy000n Jul 2, 2026
d97344d
refactor: User 생성자를 private Builder로 제한
Jy000n Jul 2, 2026
02a1696
fix: enum Provider 형식 enum으로 수정
Jy000n Jul 2, 2026
548ef81
refactor: JWT subject를 email에서 userId로 변경
Jy000n Jul 2, 2026
9c32021
refactor: OAuth2 사용자 생성 시 Provider enum 사용
Jy000n Jul 2, 2026
58197dc
fix: redirectUri로의 리다이렉트 누락 추가
Jy000n Jul 2, 2026
33f38dd
fix: OAuthSuccessHandler에서 refreshToken의 SET_COOKIE 헤더 누락 추가
Jy000n Jul 2, 2026
52671a5
refactor: 중복된 에러 응답 로직 제거를 위해 AuthErrorResponseWriter 추출
Jy000n Jul 2, 2026
ecb5853
feat: OAUTH_LOGIN_FAILED 에러 코드 추가 및 OAuthFailureHandler에 적용
Jy000n Jul 2, 2026
8d5761c
feat: 안전한 accessToken 발급을 위한 일회용 인증 코드 구현
Jy000n Jul 2, 2026
770cfa0
feat: 안전한 accessToken 발급을 위한 일회용 인증 코드 구현
Jy000n Jul 2, 2026
18d23a3
feat: 일회용 인증 코드로 accessToken을 발급하는 GET /api/auth/token API 추가
Jy000n Jul 2, 2026
56893ba
fix: 인증코드 조회/삭제 원자적 처리
Jy000n Jul 2, 2026
f2adfd0
fix: AuthController에서 AuthErrorResponseWriter를 사용하도록 에러 응답 형식 통일
Jy000n Jul 2, 2026
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
File renamed without changes.
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

Expand Down
50 changes: 50 additions & 0 deletions src/main/java/com/Timo/Timo/domain/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.Timo.Timo.domain.user.entity;

import com.Timo.Timo.domain.user.enums.Provider;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String email;

private String name;
private String imageUrl;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Provider provider;

@Builder
private User(String email, String name, String imageUrl, Provider provider) {
this.email = email;
this.name = name;
this.imageUrl = imageUrl;
this.provider = provider;
}

public void update(String name, String imageUrl){
this.name = name;
this.imageUrl = imageUrl;
}

}
5 changes: 5 additions & 0 deletions src/main/java/com/Timo/Timo/domain/user/enums/Provider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.Timo.Timo.domain.user.enums;

public enum Provider {
GOOGLE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.Timo.Timo.domain.user.repository;

import com.Timo.Timo.domain.user.entity.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.Timo.Timo.global.auth.controller;

import com.Timo.Timo.global.auth.handler.AuthErrorResponseWriter;
import com.Timo.Timo.global.auth.service.AuthCodeService;
import com.Timo.Timo.global.exception.code.ErrorCode;
import com.Timo.Timo.global.jwt.provider.JwtTokenProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Comment on lines +14 to +16

@coderabbitai coderabbitai Bot Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify token endpoint callers and security matcher coverage before switching GET to POST.
rg -nP -C3 '(/api/auth/token|requestMatchers\s*\(|csrf|cors)' src/main/java

Repository: Team-Timo/Timo-Server

Length of output: 4087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== AuthController outline ==\n'
ast-grep outline src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java --view expanded || true

printf '\n== SecurityConfig outline ==\n'
ast-grep outline src/main/java/com/Timo/Timo/global/config/SecurityConfig.java --view expanded || true

printf '\n== AuthController relevant lines ==\n'
sed -n '1,220p' src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java | cat -n

printf '\n== SecurityConfig relevant lines ==\n'
sed -n '1,220p' src/main/java/com/Timo/Timo/global/config/SecurityConfig.java | cat -n

printf '\n== Token endpoint callers / references ==\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**' '/api/auth/token|getAndDelete\(|accessToken|Cache-Control|Pragma' .

Repository: Team-Timo/Timo-Server

Length of output: 7970


토큰 교환 엔드포인트는 POST로 바꾸고 응답 캐시를 막아 주세요.
현재 GET으로 인증 코드를 소모한 뒤 accessToken을 내려서, 브라우저 히스토리/캐시/프리페치/접근 로그에 값이 남을 수 있습니다. 응답에는 Cache-Control: no-store(필요하면 Pragma: no-cache도) 를 추가해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java`
around lines 14 - 16, The token exchange endpoint in AuthController is currently
exposed as a GET, which can leak the authorization code and accessToken via
history/cache/prefetch/logs; change the exchange handler to use POST instead of
GET, and update the response handling to set Cache-Control: no-store (and
Pragma: no-cache if needed). Locate the endpoint mapped in AuthController and
adjust the request mapping/response building so the token exchange path no
longer returns sensitive data through a cacheable GET response.

Source: Path instructions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p2) accessToken을 발급하는 엔드포인트라서 코드래빗 리뷰처럼 post 메소드가 더 어울릴 것 같네요.
get은 히스토리가 남아서 보안상 위험할 수도 있을 것 같아요!!

그리고 swagger 처리 해주셔야 합니다!!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for existing Swagger/OpenAPI usage =="
rg -nP -C2 '`@Tag`\(|`@Operation`\(|springdoc|SpringDoc|swagger' src/main/java --iglob '*.java' || true

echo "== build.gradle swagger deps =="
rg -n 'springdoc|swagger' build.gradle || true

echo "== AuthController full content =="
cat -n src/main/java/com/Timo/Timo/global/auth/controller/AuthController.java

echo "== SecurityConfig permitAll rules =="
rg -n 'permitAll|requestMatchers' src/main/java/com/Timo/Timo/global/config/SecurityConfig.java -A1 -B1

echo "== Other controllers for style reference =="
fd -e java -i 'controller' src/main/java

Length of output: 4615


@laura-jung 맞습니다! 아래처럼 반영하는 걸 제안드립니다.

  • GETPOST로 변경 (히스토리/캐시 노출 방지)
  • 응답에 Cache-Control: no-store 헤더 추가
  • springdoc-openapi가 이미 의존성에 포함되어 있으니 (build.gradle) @Tag, @Operation, @Parameter 애노테이션으로 Swagger 문서화 추가

SecurityConfigrequestMatchers("/api/auth/token")은 경로 기준이라 메서드를 바꿔도 별도 설정 변경은 필요 없습니다.

Task: AuthController /api/auth/token을 POST로 변경 + 캐시 방지 헤더 + Swagger 문서화


The requested changes have been implemented and a pull request has been created: View PR

import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthCodeService authCodeService;
private final JwtTokenProvider jwtTokenProvider;
private final AuthErrorResponseWriter authErrorResponseWriter;
private final ObjectMapper objectMapper;

@GetMapping("/token")
public void token(
@RequestParam String code,
HttpServletRequest request,
HttpServletResponse response
) throws IOException {

String userId = authCodeService.getAndDelete(code);

if (userId == null) {
authErrorResponseWriter.write(response, ErrorCode.INVALID_AUTH_CODE, request.getRequestURI());
return;
}

String accessToken = jwtTokenProvider.generateAccessToken(Long.parseLong(userId));

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(
objectMapper.writeValueAsString(Map.of("accessToken", accessToken))
);
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/Timo/Timo/global/auth/dto/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.Timo.Timo.global.auth.dto;

import com.Timo.Timo.domain.user.entity.User;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

@Getter
public class CustomUserDetails implements OAuth2User, UserDetails {

private final User user;
private final Map<String, Object> attributes;

public CustomUserDetails(User user, Map<String, Object> attributes){
this.user = user;
this.attributes = attributes;
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public String getName() {
return user.getEmail();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}

@Override
public String getPassword(){
return null;
}

@Override
public String getUsername(){
return user.getEmail();
}
Comment on lines +44 to +47

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

getUsername과 getName모두 getEmail을 가져오는 것 같은데 두개가 뭐가 다른건가요?

@Jy000n Jy000n Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

저도 이 부분 정말 헷갈렸는데요,, 둘 다 사용자를 식별하는 용도이긴 하지만, 역할이 조금 다르다고 합니다!

현재 해당 클래스는 구글 로그인으로 받아온 사용자를 Spring Security가 이해할 수 있는 로그인 객체로 변환해 주는 어댑터 역할을 하며, OAuth2UserUserDetails인터페이스를 모두 구현하고 있습니다.

  • getName() -> OAuth2User 인터페이스 구현 (OAuth2 로그인할 때 제공자(구글)이 사용하는 사용자 식별 = '이 OAuth2 사용자의 식별자가 뭐야?')
  • getUsername() -> UserDetails 인터페이스 구현 (Spring Security가 내부적으로 사용자를 식별할 때 사용하는 값 = '현재 로그인한 사람이 누구야?')

이었는데 아래 코드리뷰와 관련해서 식별자 값을 email->userId로 변경 했으니 참고해주시면 감사하겠습니다:)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

완전 이해되었습니다 꼼꼼한 설명 감사합니다.


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.Timo.Timo.global.auth.handler;

import com.Timo.Timo.global.exception.code.ErrorCode;
import com.Timo.Timo.global.exception.dto.ErrorDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AuthErrorResponseWriter {

private final ObjectMapper objectMapper;

public void write(HttpServletResponse response, ErrorCode errorCode, String path)
throws IOException {

ErrorDto errorDto = new ErrorDto(
LocalDateTime.now(),
errorCode.getHttpStatus().value(),
errorCode.getCode(),
errorCode.getMessage(),
path
);

response.setStatus(errorCode.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorDto));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.Timo.Timo.global.auth.handler;

import com.Timo.Timo.global.exception.code.ErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final AuthErrorResponseWriter authErrorResponseWriter;

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
authErrorResponseWriter.write(response, ErrorCode.UNAUTHORIZED, request.getRequestURI());
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p3) 지금 보았을때 위의 jwtAuthenticationEntryPoint와 oauthFailureHandler의 코드가 겹치는 부분이 많은 것 같아요! 따로 writer로 분리해서 작성하면 중복코드 사용을 줄일 수 있지 않을까하는 생각입니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

오홍 인지하지 못하고 있었는데 ErrorDto 생성 → status 설정 → contentType 설정 → write 하는 로직이 동일하게 반복되고 있었네요! AuthErrorResponseWriter로 분리하여 중복을 제거하고 이후 에러 응답 형식이 바뀌더라도 한 곳만 수정하면 되도록 개선해야겠습니다-!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.Timo.Timo.global.auth.handler;

import com.Timo.Timo.global.exception.code.ErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

private final AuthErrorResponseWriter authErrorResponseWriter;

@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception
) throws IOException {
authErrorResponseWriter.write(response, ErrorCode.OAUTH_LOGIN_FAILED, request.getRequestURI());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.Timo.Timo.global.auth.handler;

import com.Timo.Timo.global.auth.dto.CustomUserDetails;
import com.Timo.Timo.global.auth.service.AuthCodeService;
import com.Timo.Timo.global.auth.service.RefreshTokenService;
import com.Timo.Timo.global.jwt.provider.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

@Component
@RequiredArgsConstructor
public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;
private final AuthCodeService authCodeService;

@Value("${app.oauth2.redirect-uri}")
private String redirectUri;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이부분은 아직 구현되지 않아서 따로 사용되지 않는거겠죠...?

@Jy000n Jy000n Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

redirectUri 선언만 해두고 실제로 사용하는 코드가 누락되어 있었네요.. 테스트용 코드 작성하느라 수정하면서 실수로 지운 것 같습니다.. getRedirectStrategy().sendRedirect()로 리다이렉트 처리 다시 추가해놓겠습니다!


@Value("${app.auth.cookie-secure:false}")
private boolean cookieSecure;

@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Long userId = userDetails.getUser().getId();

String refreshToken = jwtTokenProvider.generateRefreshToken(userId);
refreshTokenService.save(String.valueOf(userId), refreshToken);

ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true)
.secure(cookieSecure)
.path("/")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p4) Refresh Token 쿠키가 path("/")로 설정되어 모든 API 요청에 함께 전송되는것 같아요. 실제 사용되는 재발급·로그아웃 API로 경로를 제한하면 토큰의 불필요한 전송과 노출 범위를 줄일 수 있을 것 같습니다. /api/auth 정도로 제한하는 것은 어떨까요?

.maxAge(Duration.ofSeconds(jwtTokenProvider.getRefreshTokenExpiry()))
.sameSite("Strict")
.build();
Comment on lines +47 to +53

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p2) refreshCookie를 생성하고 있지만 응답 헤더에 추가하는 코드가 없어 실제 클라이언트로 쿠키가 내려가지 않을 것 같습니다.
response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()) 처리가 필요한 것 같습니다. 현재 상태라면 refresh token가 redis에는 저장되지만 브라우저 쿠키에는 저장되지 않아 재발급이 실패할 수 있을 것 같습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

헉 맞습니다! 마지막에 accessToken 전달 방식을 쿠키로 잘못 전달하고 있었어서 Body 응답으로 변경하는 과정에서 refreshToken 쿠키 헤더 추가 코드가 누락되었던 것 같습니다.

꼼꼼하게 봐주셔서 감사합니다! 바로 수정하겠습니다~

response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString());

String code = authCodeService.generateAndSave(String.valueOf(userId));

String redirectUrl = UriComponentsBuilder.fromUriString(redirectUri)
.queryParam("code", code)
.build().toUriString();

getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.Timo.Timo.global.auth.service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthCodeService {

private final RedisTemplate<String, String> redisTemplate;

private static final String KEY_PREFIX = "auth:code:";
private static final long CODE_EXPIRY_SECONDS = 30L;

public String generateAndSave(String userId) {
String code = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(
KEY_PREFIX + code,
userId,
CODE_EXPIRY_SECONDS,
TimeUnit.SECONDS
);
return code;
}

public String getAndDelete(String code) {
return redisTemplate.opsForValue().getAndDelete(KEY_PREFIX + code);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading
Loading