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
6 changes: 4 additions & 2 deletions src/main/java/com/mycom/socket/auth/config/JWTProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
@ConfigurationProperties(prefix = "jwt")
public class JWTProperties {
private String secret;
private long accessTokenValidityInSeconds = 1800;
private String cookieName = "Authorization";
private long accessTokenValidityInSeconds;
private long refreshTokenValidityInSeconds;
private String accessTokenCookieName;
private String refreshTokenCookieName;
Comment on lines +13 to +16
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

ν•„μˆ˜ 속성에 λŒ€ν•œ μœ νš¨μ„± 검증 μΆ”κ°€ ν•„μš”

토큰 유효 κΈ°κ°„κ³Ό μΏ ν‚€ 이름은 ν•„μˆ˜ κ°’μ΄λ―€λ‘œ, 이에 λŒ€ν•œ μœ νš¨μ„± 검증이 ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜λŠ” 것을 μ œμ•ˆν•©λ‹ˆλ‹€:

+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;

 @Getter
 @Setter
 @Component
 @ConfigurationProperties(prefix = "jwt")
 public class JWTProperties {
     private String secret;
-    private long accessTokenValidityInSeconds;
-    private long refreshTokenValidityInSeconds;
-    private String accessTokenCookieName;
-    private String refreshTokenCookieName;
+    @Positive(message = "μ•‘μ„ΈμŠ€ 토큰 유효 기간은 μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€")
+    private long accessTokenValidityInSeconds;
+    @Positive(message = "λ¦¬ν”„λ ˆμ‹œ 토큰 유효 기간은 μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€")
+    private long refreshTokenValidityInSeconds;
+    @NotBlank(message = "μ•‘μ„ΈμŠ€ 토큰 μΏ ν‚€ 이름은 ν•„μˆ˜μž…λ‹ˆλ‹€")
+    private String accessTokenCookieName;
+    @NotBlank(message = "λ¦¬ν”„λ ˆμ‹œ 토큰 μΏ ν‚€ 이름은 ν•„μˆ˜μž…λ‹ˆλ‹€")
+    private String refreshTokenCookieName;
     private String issuer = "go_socket";
     private boolean secureCookie = false;
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private long accessTokenValidityInSeconds;
private long refreshTokenValidityInSeconds;
private String accessTokenCookieName;
private String refreshTokenCookieName;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JWTProperties {
private String secret;
@Positive(message = "μ•‘μ„ΈμŠ€ 토큰 유효 기간은 μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€")
private long accessTokenValidityInSeconds;
@Positive(message = "λ¦¬ν”„λ ˆμ‹œ 토큰 유효 기간은 μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€")
private long refreshTokenValidityInSeconds;
@NotBlank(message = "μ•‘μ„ΈμŠ€ 토큰 μΏ ν‚€ 이름은 ν•„μˆ˜μž…λ‹ˆλ‹€")
private String accessTokenCookieName;
@NotBlank(message = "λ¦¬ν”„λ ˆμ‹œ 토큰 μΏ ν‚€ 이름은 ν•„μˆ˜μž…λ‹ˆλ‹€")
private String refreshTokenCookieName;
private String issuer = "go_socket";
private boolean secureCookie = false;
}

private String issuer = "go_socket";
private boolean secureCookie = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mycom.socket.auth.controller;

import com.mycom.socket.auth.config.JWTProperties;
import com.mycom.socket.auth.dto.response.TokenResponse;
import com.mycom.socket.auth.jwt.JWTUtil;
import com.mycom.socket.auth.security.CookieUtil;
import com.mycom.socket.global.exception.BadRequestException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.Optional;

@RestController
@RequiredArgsConstructor
public class RefreshController {

private final JWTUtil jwtUtil;
private final CookieUtil cookieUtil;
private final JWTProperties jwtProperties;


@PostMapping("/refresh")
public TokenResponse refreshAccessToken(HttpServletRequest request, HttpServletResponse response) {
Optional<String> refreshTokenOpt = extractRefreshToken(request);
if (refreshTokenOpt.isEmpty()) {
return TokenResponse.of("λ¦¬ν”„λ ˆμ‹œ 토큰이 μ—†μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”.");
}

String refreshToken = refreshTokenOpt.get();
if (!jwtUtil.validateToken(refreshToken, "REFRESH_TOKEN")) {
response.addCookie(cookieUtil.createExpiredCookie(jwtProperties.getRefreshTokenCookieName()));
return TokenResponse.of("μœ νš¨ν•˜μ§€ μ•Šμ€ λ¦¬ν”„λ ˆμ‹œ ν† ν°μž…λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”.");
}

String email = jwtUtil.getEmail(refreshToken);
String newAccessToken = jwtUtil.createToken(email, jwtProperties.getAccessTokenValidityInSeconds(), "ACCESS_TOKEN");
String newRefreshToken = jwtUtil.createToken(email, jwtProperties.getRefreshTokenValidityInSeconds(), "REFRESH_TOKEN");

response.addCookie(cookieUtil.createAuthCookie(newAccessToken));
response.addCookie(cookieUtil.createRefreshCookie(newRefreshToken));

Comment on lines +41 to +47
Copy link

Choose a reason for hiding this comment

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

πŸ’‘ Codebase verification

토큰 관리 λ³΄μ•ˆ 취약점 확인됨

ν˜„μž¬ κ΅¬ν˜„μ—μ„œ λ‹€μŒκ³Ό 같은 λ³΄μ•ˆ 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€:

  • 이전 λ¦¬ν”„λ ˆμ‹œ 토큰이 λ¬΄νš¨ν™”λ˜μ§€ μ•Šμ•„ μž¬μ‚¬μš© κ°€λŠ₯
  • Redisκ°€ κ΅¬μ„±λ˜μ–΄ μžˆμœΌλ‚˜ 토큰 관리에 ν™œμš©λ˜μ§€ μ•ŠμŒ
  • λ™μ‹œ 토큰 κ°±μ‹  μš”μ²­μ— λŒ€ν•œ 처리 둜직 λΆ€μž¬

ꢌμž₯ 사항:

  1. Redisλ₯Ό ν™œμš©ν•œ 토큰 λΈ”λž™λ¦¬μŠ€νŠΈ κ΅¬ν˜„
  2. λ¦¬ν”„λ ˆμ‹œ 토큰 μž¬λ°œκΈ‰ μ‹œ 이전 토큰 μ¦‰μ‹œ λ¬΄νš¨ν™”
  3. 토큰 λ°œκΈ‰ 이λ ₯ 좔적을 μœ„ν•œ μ €μž₯μ†Œ κ΅¬ν˜„
  4. λ™μ‹œμ„± μ œμ–΄λ₯Ό μœ„ν•œ 락(Lock) λ©”μ»€λ‹ˆμ¦˜ λ„μž…
πŸ”— Analysis chain

토큰 μž¬λ°œκΈ‰ 둜직 검증 ν•„μš”

μƒˆλ‘œμš΄ 토큰 λ°œκΈ‰ μ‹œ λ‹€μŒ 사항듀을 확인해야 ν•©λ‹ˆλ‹€:

  1. 이전 λ¦¬ν”„λ ˆμ‹œ 토큰 λ¬΄νš¨ν™”
  2. 토큰 λ°œκΈ‰ 이λ ₯ 좔적
  3. λ™μ‹œ 토큰 κ°±μ‹  μš”μ²­ 처리
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for token revocation implementation
rg -l "TokenRevocationService|BlacklistService|InvalidatedTokenRepository"

Length of output: 76


Script:

#!/bin/bash
# Search for token-related implementations with broader patterns
rg -l "token.*repository|token.*service|token.*cache|token.*store"

# Search for security configurations
rg -l "SecurityConfig|WebSecurityConfig|JwtConfig"

# Search for Redis or cache configurations
rg -l "RedisConfig|CacheConfig|TokenCache"

Length of output: 364


Script:

#!/bin/bash
# Check Redis configuration implementation
cat src/main/java/com/mycom/socket/global/config/RedisConfig.java

# Check security configuration for token handling
cat src/main/java/com/mycom/socket/auth/config/SecurityConfig.java

# Search for Redis usage in token operations
rg -g "!*.{test,spec}.*" "redisTemplate|StringRedisTemplate" -A 3

Length of output: 7750

return TokenResponse.of(newAccessToken);
}

private Optional<String> extractRefreshToken(HttpServletRequest request) {
if (request.getCookies() == null) {
return Optional.empty();
}

return Arrays.stream(request.getCookies())
.filter(cookie -> jwtProperties.getRefreshTokenCookieName().equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mycom.socket.auth.dto.response;

public record TokenResponse(
String accessToken,
String message,
boolean success
) {
public static TokenResponse of(String message) {
return new TokenResponse(null, message, false);
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected void doFilterInternal(HttpServletRequest request,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = resolveTokenFromCookie(request);
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
if (StringUtils.hasText(token) && jwtUtil.validateToken(token, "ACCESS_TOKEN")) {
setAuthentication(token);
}
} catch (Exception e) {
Expand All @@ -46,7 +46,7 @@ private String resolveTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (jwtProperties.getCookieName().equals(cookie.getName())) {
if (jwtProperties.getAccessTokenCookieName().equals(cookie.getName())) {
return cookie.getValue();
}
}
Expand Down
116 changes: 94 additions & 22 deletions src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.mycom.socket.auth.jwt;

import com.mycom.socket.auth.config.JWTProperties;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -18,53 +21,122 @@ public class JWTUtil {
private final SecretKey secretKey;
private final JWTProperties jwtProperties;

/**
* JWTUtil μƒμ„±μž
* μ„€μ •λœ μ‹œν¬λ¦Ώ ν‚€λ₯Ό λ°”νƒ•μœΌλ‘œ HMAC-SHA μ•Œκ³ λ¦¬μ¦˜μš© SecretKeyλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
*
* @param jwtProperties JWT κ΄€λ ¨ 섀정값을 λ‹΄κ³  μžˆλŠ” ν”„λ‘œνΌν‹° 객체
*/
public JWTUtil(JWTProperties jwtProperties) {
this.jwtProperties = jwtProperties;
this.secretKey = Keys.hmacShaKeyFor(
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
);
}

/**
* JWT Parser 생성
* 토큰 검증 및 정보 μΆ”μΆœμ— μ‚¬μš©λ˜λŠ” 곡톡 Parserλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
*
* @return μ„€μ •λœ JWT Parser 객체
*/
private JwtParser createParser() {
return Jwts.parser()
.verifyWith(secretKey)
.requireIssuer(jwtProperties.getIssuer())
.build();
}


/**
* JWT 토큰 생성
* μ£Όμ–΄μ§„ 이메일과 μœ νš¨κΈ°κ°„μœΌλ‘œ μƒˆλ‘œμš΄ JWTλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
*
* @param email 토큰에 포함될 μ‚¬μš©μž 이메일
* @param validityInSeconds 토큰 유효 κΈ°κ°„ (초)
* @param accessToken
* @return μƒμ„±λœ JWT λ¬Έμžμ—΄
* @throws IllegalStateException 토큰 생성 쀑 였λ₯˜ λ°œμƒ μ‹œ
*/
public String createToken(String email) {
public String createToken(String email, long validityInSeconds, String accessToken) {
Date now = new Date();
Date validity = new Date(now.getTime() +
(jwtProperties.getAccessTokenValidityInSeconds() * 1000));

return Jwts.builder()
.issuer(jwtProperties.getIssuer())
.subject(email)
.issuedAt(now)
.expiration(validity)
.signWith(secretKey)
.compact();
Date validity = new Date(now.getTime() + (validityInSeconds * 1000));

try {
return Jwts.builder()
.issuer(jwtProperties.getIssuer())
.subject(email)
.issuedAt(now)
.expiration(validity)
.claim("type", accessToken)
.signWith(secretKey)
.compact();
} catch (JwtException e) {
log.error("토큰 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", e);
throw new IllegalStateException("토큰 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", e);
}
Comment on lines +61 to +77
Copy link

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

토큰 생성 λ³΄μ•ˆ κ°•ν™” ν•„μš”

토큰 생성 μ‹œ λ‹€μŒ λ³΄μ•ˆ κΈ°λŠ₯듀을 μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€:

  1. 토큰 ID (jti) ν΄λ ˆμž„ μΆ”κ°€
  2. Not Before (nbf) ν΄λ ˆμž„ μΆ”κ°€
  3. 토큰 μš©λ„λ³„ 청쀑(aud) ꡬ뢄
 return Jwts.builder()
     .issuer(jwtProperties.getIssuer())
     .subject(email)
     .issuedAt(now)
     .expiration(validity)
     .claim("type", accessToken)
+    .id(UUID.randomUUID().toString())
+    .notBefore(now)
+    .audience()
+        .add(accessToken.toLowerCase())
+        .and()
     .signWith(secretKey)
     .compact();
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String createToken(String email, long validityInSeconds, String accessToken) {
Date now = new Date();
Date validity = new Date(now.getTime() +
(jwtProperties.getAccessTokenValidityInSeconds() * 1000));
return Jwts.builder()
.issuer(jwtProperties.getIssuer())
.subject(email)
.issuedAt(now)
.expiration(validity)
.signWith(secretKey)
.compact();
Date validity = new Date(now.getTime() + (validityInSeconds * 1000));
try {
return Jwts.builder()
.issuer(jwtProperties.getIssuer())
.subject(email)
.issuedAt(now)
.expiration(validity)
.claim("type", accessToken)
.signWith(secretKey)
.compact();
} catch (JwtException e) {
log.error("토큰 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", e);
throw new IllegalStateException("토큰 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", e);
}
public String createToken(String email, long validityInSeconds, String accessToken) {
Date now = new Date();
Date validity = new Date(now.getTime() + (validityInSeconds * 1000));
try {
return Jwts.builder()
.issuer(jwtProperties.getIssuer())
.subject(email)
.issuedAt(now)
.expiration(validity)
.claim("type", accessToken)
.id(UUID.randomUUID().toString())
.notBefore(now)
.audience()
.add(accessToken.toLowerCase())
.and()
.signWith(secretKey)
.compact();
} catch (JwtException e) {
log.error("토큰 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", e);
throw new IllegalStateException("토큰 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", e);
}

}

/**
* 토큰 μœ νš¨μ„± 검증
* μ£Όμ–΄μ§„ 토큰이 μœ νš¨ν•œμ§€ κ²€μ‚¬ν•©λ‹ˆλ‹€. ν† ν°μ˜ μ„œλͺ…, 만료 μ—¬λΆ€, λ°œκΈ‰μž 등을 ν™•μΈν•©λ‹ˆλ‹€.
*
* @param token 검증할 JWT λ¬Έμžμ—΄
* @return 토큰이 μœ νš¨ν•˜λ©΄ true, κ·Έλ ‡μ§€ μ•ŠμœΌλ©΄ false
*/
public boolean validateToken(String token) {
try {
if (!StringUtils.hasText(token)) {
return false;
}
public boolean validateToken(String token, String expectedType) {
if (!StringUtils.hasText(token)) {
return false;
}

Jwts.parser()
try {
var claims = Jwts.parser()
.verifyWith(secretKey)
.requireIssuer(jwtProperties.getIssuer())
.build()
.parseSignedClaims(token);
return true;
} catch (Exception e) {
log.warn("JWT 토큰 검증 μ‹€νŒ¨", e);
.parseSignedClaims(token)
.getPayload();

// 토큰 νƒ€μž… 검증
String tokenType = claims.get("type", String.class);
if (!expectedType.equals(tokenType)) {
log.warn("잘λͺ»λœ 토큰 νƒ€μž…μž…λ‹ˆλ‹€. expected: {}, actual: {}", expectedType, tokenType);
return false;
}

return new Date().before(claims.getExpiration());
} catch (ExpiredJwtException e) {
log.warn("만료된 JWT ν† ν°μž…λ‹ˆλ‹€.");
return false;
} catch (JwtException e) {
log.warn("μœ νš¨ν•˜μ§€ μ•Šμ€ JWT ν† ν°μž…λ‹ˆλ‹€.", e);
return false;
}
Comment on lines +87 to 114
Copy link

Choose a reason for hiding this comment

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

πŸ’‘ Codebase verification

토큰 λ³΄μ•ˆ κ°•ν™” ꢌμž₯사항 확인

리뷰 μ˜κ²¬μ—μ„œ μ œμ‹œλœ λ³΄μ•ˆ κ°•ν™” ν•­λͺ©λ“€μ΄ ν˜„μž¬ κ΅¬ν˜„λ˜μ–΄ μžˆμ§€ μ•ŠμŒμ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μŒ μ‚¬ν•­λ“€μ˜ κ΅¬ν˜„μ„ ꢌμž₯λ“œλ¦½λ‹ˆλ‹€:

  1. Redisλ₯Ό ν™œμš©ν•œ 토큰 λΈ”λž™λ¦¬μŠ€νŠΈ κ΅¬ν˜„

    • κΈ°μ‘΄ RedisService에 토큰 λ¬΄νš¨ν™” κΈ°λŠ₯ μΆ”κ°€
    • λ‘œκ·Έμ•„μ›ƒ μ‹œ 토큰 λΈ”λž™λ¦¬μŠ€νŠΈ 등둝
  2. NBF (Not Before) ν΄λ ˆμž„ μΆ”κ°€

    • JWTUtil의 토큰 생성 μ‹œ .notBefore() μ„€μ •
    • validateToken() λ©”μ†Œλ“œμ—μ„œ μžλ™ 검증됨
  3. 토큰 ID (jti) ν΄λ ˆμž„ μΆ”κ°€ 및 좔적

    • 토큰 생성 μ‹œ 고유 ID λΆ€μ—¬
    • Redisλ₯Ό ν™œμš©ν•΄ μ‚¬μš©λœ 토큰 ID 좔적
πŸ”— Analysis chain

토큰 검증 둜직 κ°•ν™” ν•„μš”

λ‹€μŒ 검증 ν•­λͺ©λ“€μ„ μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€:

  1. 토큰 λΈ”λž™λ¦¬μŠ€νŠΈ 확인
  2. NBF (Not Before) ν΄λ ˆμž„ 검증
  3. 토큰 ID 쀑볡 μ‚¬μš© 확인
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for token blacklist implementation
rg -l "TokenBlacklist|RevokedToken|InvalidatedToken"

Length of output: 54


Script:

#!/bin/bash
# Search for Redis/Cache configurations and JWT-related security implementations
rg -l "Redis|Cache|TokenStore"

# Search for JWT claim handling
rg -A 5 "\.claim\(|\.getClaim|jti|nbf" 

# Search for security configuration files
fd -e java -e xml -e properties -e yml "Security|Auth|JWT" --type f

Length of output: 1424


Script:

#!/bin/bash
# Check Redis service implementation for token operations
cat src/main/java/com/mycom/socket/global/service/RedisService.java

# Check security configuration
cat src/main/java/com/mycom/socket/auth/config/SecurityConfig.java

Length of output: 5402

}

/**
* ν† ν°μ—μ„œ 이메일 μΆ”μΆœ
* ν† ν°μ˜ 만료 μ‹œκ°„ μΆ”μΆœ
*
* @param token JWT λ¬Έμžμ—΄
* @return ν† ν°μ˜ 만료 μ‹œκ°„
*/
private Date getExpirationFromToken(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
}


/**
* ν† ν°μ—μ„œ μ‚¬μš©μž 이메일 μΆ”μΆœ
* JWT의 subject ν΄λ ˆμž„μ—μ„œ μ‚¬μš©μž 이메일을 μΆ”μΆœν•©λ‹ˆλ‹€.
*
* @param token JWT λ¬Έμžμ—΄
* @return 토큰에 ν¬ν•¨λœ μ‚¬μš©μž 이메일
* @throws IllegalStateException ν† ν°μ—μ„œ 이메일을 μΆ”μΆœν•  수 μ—†λŠ” 경우
*/
public String getEmail(String token) {
return Jwts.parser()
Expand Down
67 changes: 53 additions & 14 deletions src/main/java/com/mycom/socket/auth/security/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,65 @@ public class CookieUtil {
private final JWTProperties jwtProperties;

/**
* 인증 μΏ ν‚€ 생성
* 곡톡 μΏ ν‚€ 생성 λ©”μ†Œλ“œ
* λͺ¨λ“  μ’…λ₯˜μ˜ μΏ ν‚€ 생성에 μ‚¬μš©λ˜λŠ” κΈ°λ³Έ λ©”μ†Œλ“œμž…λ‹ˆλ‹€.
*
* @param name μΏ ν‚€μ˜ 이름
* @param value 쿠킀에 μ €μž₯될 κ°’ (토큰)
* @param maxAge μΏ ν‚€μ˜ 유효 μ‹œκ°„ (초 λ‹¨μœ„)
* @param secure HTTPS ν”„λ‘œν† μ½œμ—μ„œλ§Œ 전솑 μ—¬λΆ€
* @return μƒμ„±λœ μΏ ν‚€ 객체
*/
public Cookie createAuthCookie(String token) {
Cookie cookie = new Cookie(jwtProperties.getCookieName(), token);
private Cookie createCookie(String name, String value, long maxAge, boolean secure) {
Cookie cookie = new Cookie(name, value);
cookie.setHttpOnly(true);
cookie.setSecure(jwtProperties.isSecureCookie());
cookie.setPath("/");
cookie.setMaxAge((int) jwtProperties.getAccessTokenValidityInSeconds());
cookie.setSecure(secure);
cookie.setPath("/api/auth");
cookie.setMaxAge((int) maxAge);
cookie.setAttribute("SameSite", "Strict"); //CSRF 곡격 λ°©μ§€ μ„€μ • μΆ”κ°€
return cookie;
}

/**
* 인증 μΏ ν‚€ 만료 처리
* Access Token을 μ €μž₯ν•˜λŠ” μΏ ν‚€ 생성
* ν΄λΌμ΄μ–ΈνŠΈ 인증에 μ‚¬μš©λ˜λŠ” Access Token을 쿠킀에 μ €μž₯ν•©λ‹ˆλ‹€.
*
* @param token JWT Access Token λ¬Έμžμ—΄
* @return Access Token이 μ €μž₯된 μΏ ν‚€
*/
public Cookie createExpiredAuthCookie() {
Cookie cookie = new Cookie(jwtProperties.getCookieName(), null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0); // μ¦‰μ‹œ 만료
return cookie;
public Cookie createAuthCookie(String token) {
return createCookie(
jwtProperties.getAccessTokenCookieName(),
token,
jwtProperties.getAccessTokenValidityInSeconds(),
jwtProperties.isSecureCookie()
);
}

/**
* Refresh Token을 μ €μž₯ν•˜λŠ” μΏ ν‚€ 생성
* Access Token μž¬λ°œκΈ‰μ— μ‚¬μš©λ˜λŠ” Refresh Token을 쿠킀에 μ €μž₯ν•©λ‹ˆλ‹€.
*
* @param token JWT Refresh Token λ¬Έμžμ—΄
* @return Refresh Token이 μ €μž₯된 μΏ ν‚€
*/
public Cookie createRefreshCookie(String token) {
return createCookie(
jwtProperties.getRefreshTokenCookieName(),
token,
jwtProperties.getRefreshTokenValidityInSeconds(),
jwtProperties.isSecureCookie()
);
}
Comment on lines +49 to +63
Copy link

Choose a reason for hiding this comment

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

πŸ’‘ Codebase verification

토큰 μœ νš¨κΈ°κ°„ 검증 둜직 κ΅¬ν˜„ λ°©μ•ˆ

λ¦¬ν”„λ ˆμ‹œ ν† ν°μ˜ 유효 기간이 μ•‘μ„ΈμŠ€ 토큰보닀 길도둝 보μž₯ν•˜κΈ° μœ„ν•΄ λ‹€μŒκ³Ό 같은 검증 둜직 μΆ”κ°€λ₯Ό μ œμ•ˆλ“œλ¦½λ‹ˆλ‹€:

  • @PostConstructλ₯Ό μ‚¬μš©ν•˜μ—¬ μ΄ˆκΈ°ν™” μ‹œμ μ— 토큰 μœ νš¨κΈ°κ°„μ„ 검증
  • Bean Validation을 ν™œμš©ν•˜μ—¬ 각 ν•„λ“œμ˜ μ΅œμ†Œκ°’ 검증 (@Min)
  • refreshTokenValidityInSecondsκ°€ accessTokenValidityInSeconds보닀 큰지 ν™•μΈν•˜λŠ” μ»€μŠ€ν…€ μœ νš¨μ„± 검사 μΆ”κ°€
πŸ”— Analysis chain

λ¦¬ν”„λ ˆμ‹œ 토큰 유효 κΈ°κ°„ 검증 μΆ”κ°€ κ³ λ €

λ¦¬ν”„λ ˆμ‹œ ν† ν°μ˜ 유효 기간이 μ•‘μ„ΈμŠ€ 토큰보닀 길게 μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜λŠ” 검증 둜직 μΆ”κ°€λ₯Ό κ³ λ €ν•΄λ³΄μ„Έμš”.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# JWTPropertiesμ—μ„œ 토큰 유효 κΈ°κ°„ 섀정을 확인
rg "tokenValidityInSeconds|refreshTokenValidityInSeconds" -A 2

Length of output: 383


Script:

#!/bin/bash
# Get complete JWTProperties class
cat src/main/java/com/mycom/socket/auth/config/JWTProperties.java

# Check application properties/yaml files
fd -e yml -e yaml -e properties --exec cat {}

Length of output: 1162


/**
* 만료된 μΏ ν‚€ 생성
* λ‘œκ·Έμ•„μ›ƒ λ˜λŠ” 토큰 λ¬΄νš¨ν™” μ‹œ κΈ°μ‘΄ μΏ ν‚€λ₯Ό λ§Œλ£Œμ‹œν‚€κΈ° μœ„ν•΄ μ‚¬μš©λ©λ‹ˆλ‹€.
*
* @param name λ§Œλ£Œμ‹œν‚¬ μΏ ν‚€μ˜ 이름
* @return μ¦‰μ‹œ λ§Œλ£Œλ˜λ„λ‘ μ„€μ •λœ μΏ ν‚€
*/
public Cookie createExpiredCookie(String name) {
return createCookie(name, null, 0, true);
}
}
20 changes: 17 additions & 3 deletions src/main/java/com/mycom/socket/auth/security/LoginFilter.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mycom.socket.auth.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mycom.socket.auth.config.JWTProperties;
import com.mycom.socket.auth.jwt.JWTUtil;
import com.mycom.socket.global.dto.ApiResponse;
import com.mycom.socket.auth.dto.request.LoginRequest;
Expand Down Expand Up @@ -28,6 +29,7 @@ public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final CookieUtil cookieUtil;
private final ObjectMapper objectMapper;
private final JWTProperties jwtProperties;

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
Expand All @@ -51,11 +53,23 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
MemberDetails memberDetails = (MemberDetails) authResult.getPrincipal();
Member member = memberDetails.getMember();

String token = jwtUtil.createToken(member.getEmail());
// JWT 토큰 생성
String accessToken = jwtUtil.createToken(
member.getEmail(),
jwtProperties.getAccessTokenValidityInSeconds(),
"ACCESS_TOKEN"
);
String refreshToken = jwtUtil.createToken(
member.getEmail(),
jwtProperties.getRefreshTokenValidityInSeconds(),
"REFRESH_TOKEN"
);

// μΏ ν‚€ 생성 및 μ„€μ •
Cookie authCookie = cookieUtil.createAuthCookie(token);
response.addCookie(authCookie);
Cookie accessTokenCookie = cookieUtil.createAuthCookie(accessToken); //μ•‘μ„ΈμŠ€ 토큰 μΏ ν‚€
Cookie refreshTokenCookie = cookieUtil.createRefreshCookie(refreshToken); //λ¦¬ν”„λ ˆμ‹œ 토큰 μΏ ν‚€
response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);
Comment on lines +69 to +72
Copy link

Choose a reason for hiding this comment

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

πŸ’‘ Codebase verification

μΏ ν‚€ λ³΄μ•ˆ 섀정이 λΆˆμ™„μ „ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같은 λ³΄μ•ˆ λ¬Έμ œκ°€ λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€:

  • SameSite 속성이 μ„€μ •λ˜μ–΄ μžˆμ§€ μ•Šμ•„ CSRF 곡격에 μ·¨μ•½ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • Pathκ°€ "/"둜 μ„€μ •λ˜μ–΄ μžˆμ–΄ ν•„μš” μ΄μƒμœΌλ‘œ 넓은 λ²”μœ„μ— μΏ ν‚€κ°€ λ…ΈμΆœλ©λ‹ˆλ‹€.

μˆ˜μ •μ΄ ν•„μš”ν•œ 파일:

  • src/main/java/com/mycom/socket/auth/security/CookieUtil.java

ꢌμž₯ 사항:

  1. SameSite=Strict λ˜λŠ” Lax 속성 μΆ”κ°€
  2. μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ‹€μ œ μš”κ΅¬μ‚¬ν•­μ— 맞게 Path λ²”μœ„ μΆ•μ†Œ
  3. 운영 ν™˜κ²½μ—μ„œ Secure ν”Œλž˜κ·Έκ°€ λ°˜λ“œμ‹œ ν™œμ„±ν™”λ˜λ„λ‘ μ„€μ • κ²€ν† 
πŸ”— Analysis chain

μΏ ν‚€ λ³΄μ•ˆ 섀정을 ν™•μΈν•΄μ£Όμ„Έμš”.

μΏ ν‚€ 생성 μ‹œ λ‹€μŒ λ³΄μ•ˆ 속성듀이 μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ–΄ μžˆλŠ”μ§€ 확인이 ν•„μš”ν•©λ‹ˆλ‹€:

  • HttpOnly
  • Secure
  • SameSite
  • Path
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# CookieUtil의 κ΅¬ν˜„μ„ ν™•μΈν•˜μ—¬ λ³΄μ•ˆ μ„€μ • 검증
rg -A 10 "createAuthCookie|createRefreshCookie" --type java

Length of output: 8548


Script:

#!/bin/bash
# Check createCookie implementation and JwtProperties
echo "=== CookieUtil.createCookie implementation ==="
ast-grep --pattern 'createCookie($$$) { $$$ }'

echo -e "\n=== JwtProperties configuration ==="
rg -A 5 "class JwtProperties" --type java

Length of output: 282


Script:

#!/bin/bash
# μΏ ν‚€ 생성 λ©”μ†Œλ“œμ™€ μ„€μ • 파일 검색
echo "=== Cookie creation implementation ==="
rg -A 15 "private.*createCookie" --type java

echo -e "\n=== Security and JWT configuration ==="
fd -e yml -e properties -e yaml | xargs rg -l "jwt|cookie|secure"

Length of output: 1755


// 둜그인 응닡 생성
LoginResponse loginResponse = new LoginResponse(member.getEmail(), member.getNickname());
Expand Down
Loading
Loading