Skip to content

Conversation

@jaelyangChoi
Copy link
Collaborator

@jaelyangChoi jaelyangChoi commented Aug 26, 2025

📌 관련 이슈 (필수)

이 PR이 어떤 이슈를 해결하는지 작성해주세요. 예시: closes #123, resolves #456

📝 작업 내용 (필수)

이번 PR에서 작업한 내용을 간략히 설명해주세요.

  • 토큰이 유효하지 않을 경우 400 또는 401 에러를 보내는데, 클라이언트에서 401 에러만 처리하고 있어서 401로 변경

💬 리뷰 참고 사항 (선택)

리뷰어가 참고할 만한 사항이나 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.

Summary by CodeRabbit

  • Bug Fixes

    • Authentication errors for invalid or expired tokens now return 401 Unauthorized instead of 400 Bad Request.
    • Token validation now distinguishes expired vs invalid tokens so clients receive accurate error responses.
    • Token reissue and revocation paths now surface consistent HTTP statuses and log unexpected server errors.
    • Error responses now include clearer error messages to help client handling.
  • Documentation

    • Expired-token messages updated with explicit next-step guidance.

@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Walkthrough

Error handling and HTTP status mappings were tightened across auth components. parseRefreshToken now attaches exception messages to CoreException; revokeUserTokens rethrows CoreException and logs other exceptions; JwtUtil lets ExpiredJwtException propagate; JwtFilter distinguishes expired vs invalid access tokens; three ErrorType entries now map to 401; two error messages were clarified; CoreException constructor now accepts a message.

Changes

Cohort / File(s) Summary
Auth Token Service
capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java
parseRefreshToken wraps specific JWT exceptions into CoreException(ErrorType, e.getMessage()); revokeUserTokens now rethrows CoreException, logs other unexpected exceptions, then maps them to INTERNAL_SERVER_ERROR.
JWT validation & filter
capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java, capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java
JwtUtil.isValid no longer swallows ExpiredJwtException (it propagates to caller). JwtFilter catches ExpiredJwtException and rejects with ACCESS_TOKEN_EXPIRED; invalid tokens use INVALID_ACCESS_TOKEN. Early returns added for invalid/expired tokens.
HTTP error mapping
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java
Changed HTTP status for INVALID_JWT, ACCESS_TOKEN_EXPIRED, and REFRESH_TOKEN_EXPIRED from BAD_REQUEST to UNAUTHORIZED (401).
Controller error response
capturecat-core/src/main/java/com/capturecat/core/api/auth/TokenController.java
reissue endpoint now uses HTTP status from CoreException (e.getErrorType().getStatus()) instead of always returning 400.
Error messages
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java
Updated messages: ACCESS_TOKEN_EXPIRED → "Access token이 만료됐습니다. 재발급 해주세요."; REFRESH_TOKEN_EXPIRED → "Refresh token이 만료됐습니다. 다시 로그인해주세요."
CoreException API usage
capturecat-core/src/main/java/com/capturecat/core/support/error/CoreException.java
Constructor usage changed to include message parameter: CoreException(ErrorType, String); call sites updated accordingly.
Tests
capturecat-core/src/test/java/com/capturecat/core/api/auth/TokenApiTest.java
Adjusted assertions: invalid refresh token expectation changed from 400 to 401.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant TC as TokenController
  participant TS as TokenService
  participant JT as JwtUtil
  participant JF as JwtFilter
  participant ET as CoreException/ErrorType

  rect rgb(240,248,255)
    Note over C,TC: Refresh token reissue flow
    C->>TC: POST /auth/reissue (refreshToken)
    TC->>TS: parseRefreshToken(refreshToken)
    TS->>JT: parse/validate(refreshToken)
    JT-->>TS: Claims or throws
    alt ExpiredJwtException
      TS->>ET: throw CoreException(REFRESH_TOKEN_EXPIRED, e.message)
      TC-->>C: 401 + ApiResponse.error(REFRESH_TOKEN_EXPIRED)
    else Signature/Malformed/IllegalArg
      TS->>ET: throw CoreException(INVALID_REFRESH_TOKEN, e.message)
      TC-->>C: Error response (status from ErrorType)
    else Success
      TS-->>TC: Claims
      TC-->>C: New tokens (200)
    end
  end

  rect rgb(245,255,250)
    Note over C,JF: Incoming request with access token
    C->>JF: Request with access token
    JF->>JT: isValid(token)
    JT-->>JF: true or throws
    alt Valid
      JF->>JF: continue filter chain
    else ExpiredJwtException
      JF->>ET: rejectInvalidToken(ACCESS_TOKEN_EXPIRED)
      JF-->>C: 401 (ACCESS_TOKEN_EXPIRED)
    else Signature/Malformed/IllegalArg
      JF->>ET: rejectInvalidToken(INVALID_ACCESS_TOKEN)
      JF-->>C: 401/400 per mapping
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • leeedohyun

Poem

I twitch my whiskers, logs all bright,
Expired and invalid — I mark them right.
401s now call, messages that guide,
Tokens reissued with care and pride.
Hop, hop, I guard each auth delight. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#97-redis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Member

@leeedohyun leeedohyun left a comment

Choose a reason for hiding this comment

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

뭔데 커밋이 이렇게 많나요?ㅋㅋㅋ

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java (1)

28-29: 401 for expired tokens: alignment with client behavior and RFC 6750 looks good

Changing ACCESS_TOKEN_EXPIRED and REFRESH_TOKEN_EXPIRED to UNAUTHORIZED (401) matches the PR goal and is semantically correct for Bearer token failures. Consider also returning a WWW-Authenticate header from the exception handler for better client interoperability.

If you want, I can propose a small addition in your global exception handler to set:
WWW-Authenticate: Bearer error="invalid_token", error_description="token expired"

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 140eef3 and 98b0a26.

📒 Files selected for processing (2)
  • capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java (2 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java (1 hunks)

Comment on lines +88 to 91
throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, e.getMessage());
} catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN);
throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, e.getMessage());
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Constructor consistency and message exposure: avoid leaking low-level JWT messages; update throws or restore convenience constructor

You now pass e.getMessage() into CoreException for JWT parsing errors. Two concerns:

  • If CoreException was changed to only accept (ErrorType, String), earlier calls in this class still use the single-arg constructor (Lines 71, 79, 85). That will not compile unless an overload remains.
  • Propagating library exception messages to clients can leak internal details. Prefer a sanitized, product-defined message (or null) and keep the detailed cause only in logs.

Action options:

  • Option A (recommended): keep a 1-arg convenience constructor in CoreException and use sanitized messages only when truly helpful to the client.
  • Option B: update all throw sites in this class to provide a sanitized message string.

Proposed minimal change here (sanitized messages), keeping your behavior:

- } catch (ExpiredJwtException e) {
-   throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, e.getMessage());
- } catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
-   throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, e.getMessage());
+ } catch (ExpiredJwtException e) {
+   throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, "Refresh token expired");
+ } catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
+   throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, "Invalid refresh token");

Follow-up edits outside the changed lines (apply if the 1-arg constructor was removed):

// Lines 71, 79, 85
throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, "Invalid refresh token");

Verification script to detect remaining single-arg usages:

Additionally, since this change starts passing messages through exceptions, ensure your global exception handler does not echo raw library texts to API responses. Keep details in logs, return standardized messages to clients.


🏁 Script executed:

#!/bin/bash
# Find single-arg CoreException(ErrorType ...) calls
rg -nP --glob '!**/build/**' 'new\s+CoreException\s*\(\s*ErrorType\.[A-Z0-9_]+\s*\)'

Length of output: 10013


TokenService: Sanitize JWT error messages

I’ve confirmed that CoreException still provides the single-arg constructor (there are numerous calls like new CoreException(ErrorType.XYZ) that compile successfully), so you don’t need to restore or re-add that overload.

However, you should avoid passing raw JWT exception messages back to clients. Please replace the e.getMessage() parameters in your catch blocks with clear, user-friendly text, and keep the underlying exception logged rather than exposed. For example in TokenService.java lines 88–91:

- } catch (ExpiredJwtException e) {
-   throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, e.getMessage());
- } catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
-   throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, e.getMessage());
+ } catch (ExpiredJwtException e) {
+   log.warn("Refresh token expired", e);
+   throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, "Refresh token has expired");
+ } catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
+   log.warn("Invalid refresh token", e);
+   throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, "Refresh token is invalid");

• Apply similar sanitization wherever you currently use e.getMessage() for security-sensitive errors.
• Ensure your global exception handler returns only standardized error bodies to API clients while logging full details internally.

📝 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
throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, e.getMessage());
} catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN);
throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, e.getMessage());
}
} catch (ExpiredJwtException e) {
log.warn("Refresh token expired", e);
throw new CoreException(ErrorType.REFRESH_TOKEN_EXPIRED, "Refresh token has expired");
} catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
log.warn("Invalid refresh token", e);
throw new CoreException(ErrorType.INVALID_REFRESH_TOKEN, "Refresh token is invalid");
}
🤖 Prompt for AI Agents
In
capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java
around lines 88-91, replace the use of e.getMessage() in the catch blocks with
concise, user-friendly error messages (e.g., "Refresh token expired" and
"Invalid refresh token") and stop returning raw JWT exception text to clients;
instead log the full exception (stacktrace) internally using the logger and
construct CoreException using the ErrorType plus the sanitized message, and then
apply the same sanitization pattern wherever e.getMessage() is used for
security-sensitive exceptions and ensure the global exception handler only
returns standardized error bodies to API clients while preserving full details
in internal logs.

Comment on lines +115 to 119
} catch (CoreException e) {
throw e;
} catch (Exception e) {
log.error("[revokeUserTokens] Unexpected error", e);
throw new CoreException(ErrorType.INTERNAL_SERVER_ERROR);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Unexpected-error mapping: include a safe message and consider preserving the cause; also redact token logs elsewhere

Two points:

  • If CoreException no longer has a 1-arg constructor, this throw will not compile. Even if it does, add a safe message to aid observability.
  • You’re already logging the full exception; avoid duplicating sensitive details in the client response.

Suggested tweak:

- } catch (Exception e) {
-   log.error("[revokeUserTokens] Unexpected error", e);
-   throw new CoreException(ErrorType.INTERNAL_SERVER_ERROR);
+ } catch (Exception e) {
+   log.error("[revokeUserTokens] Unexpected error", e);
+   throw new CoreException(ErrorType.INTERNAL_SERVER_ERROR, "Unexpected error while revoking tokens");

Security hygiene (outside these lines): you log full tokens at info level in parseRefreshToken (Line 75) and blacklistAccessToken (Line 155). Logging raw tokens is risky. Redact and downshift to debug:

// helper inside TokenService
private static String redact(String token) {
    if (token == null) return "null";
    int n = Math.min(token.length(), 10);
    return token.substring(0, n) + "...";
}

// then replace
log.debug("Refresh Token: {}", redact(refreshToken));
log.debug("Blacklist Token: {}", redact(accessToken));

You can also consider hashing tokens (e.g., SHA-256) when composing Redis keys to avoid storing raw tokens as key names.

To check for raw-token logging occurrences:


🏁 Script executed:

#!/bin/bash
rg -n -C1 'Refresh Token:|Blacklist Token:' 

Length of output: 743


🏁 Script executed:

#!/bin/bash
# Search for CoreException definition in the capturecat-core module
rg -n -C3 'class CoreException' capturecat-core/src/main/java

Length of output: 1699


🏁 Script executed:

#!/bin/bash
# Search for constructors in CoreException.java
rg -nP 'public CoreException\\(' capturecat-core/src/main/java/com/capturecat/core/support/error/CoreException.java

Length of output: 190


#!/bin/bash
# Correctly search for constructors in CoreException.java
rg -nP 'public\s+CoreException\s*\(' capturecat-core/src/main/java/com/capturecat/core/support/error/CoreException.java

#!/bin/bash
# List constructor declarations in CoreException.java
rg -n 'public CoreException\(' capturecat-core/src/main/java/com/capturecat/core/support/error/CoreException.java

#!/bin/bash
# Display the contents of CoreException.java to inspect available constructors
sed -n '1,200p' capturecat-core/src/main/java/com/capturecat/core/support/error/CoreException.java

🏁 Script executed:

#!/bin/bash
# Display CoreException.java to inspect constructors
sed -n '1,200p' capturecat-core/src/main/java/com/capturecat/core/support/error/CoreException.java

Length of output: 672


Secure token logging & improve exception messaging

Two updates in TokenService.java:

  1. Enhance exception mapping (lines 115–119)
    Use the existing two-arg CoreException constructor to include a safe, descriptive message without exposing stack details to clients:

     } catch (Exception e) {
       log.error("[revokeUserTokens] Unexpected error", e);
    -  throw new CoreException(ErrorType.INTERNAL_SERVER_ERROR);
    +  throw new CoreException(
    +    ErrorType.INTERNAL_SERVER_ERROR,
    +    "Unexpected error while revoking tokens"
    +  );
     }

    • CoreException already defines CoreException(ErrorType, String), so this compiles as-is.
    • If you need to preserve the original Throwable in the exception itself, consider adding a new constructor in CoreException that accepts a cause (e.g., public CoreException(ErrorType, String, Throwable) chaining to super(message, cause)).

  2. Prevent raw-token leakage (lines 75 & 155)
    Logging full tokens at info level risks leaking sensitive data. Define a helper to redact or truncate tokens, and shift these logs to debug:

    // helper inside TokenService
    private static String redact(String token) {
      if (token == null) return "null";
      int n = Math.min(token.length(), 10);
      return token.substring(0, n) + "...";
    }
    
    // around line 75
    - log.info("Refresh Token: {}", refreshToken);
    + log.debug("Refresh Token: {}", redact(refreshToken));
    
    // around line 155
    - log.info("Blacklist Token: {}", accessToken);
    + log.debug("Blacklist Token: {}", redact(accessToken));

These changes improve observability with a safe client message and block sensitive token data from higher-level logs.

🤖 Prompt for AI Agents
In
capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java
around lines 115-119 and also addressing logs at ~75 and ~155: replace the
generic rethrow on unexpected Exception with the two-arg
CoreException(ErrorType, String) to provide a safe client-facing message (e.g.,
"Unexpected error revoking tokens") and optionally add a new CoreException
constructor that accepts a cause if you want to preserve the original throwable;
additionally, stop logging raw tokens at info level by adding a private helper
that redacts/truncates tokens (return "null" for null, first N chars + "..."
otherwise), change existing log.info(...) calls that print tokens at lines ~75
and ~155 to log.debug(...) and use the redact helper to avoid leaking sensitive
token data.

LOGIN_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.INVALID_REQUEST, LogLevel.WARN),
VALIDATION_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
INVALID_JWT(HttpStatus.BAD_REQUEST, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Potentially incorrect ErrorCode for INVALID_JWT (mapped to BEAN_VALIDATION_FAIL)

INVALID_JWT currently uses ErrorCode.BEAN_VALIDATION_FAIL. This seems semantically wrong and could mislead clients that rely on ErrorCode for UX or analytics.

Preferred fix (if the enum exists):

- INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
+ INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_JWT, LogLevel.WARN),

Fallback (if INVALID_JWT doesn’t exist in ErrorCode):

- INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
+ INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_AUTH_TOKEN, LogLevel.WARN),

Run this to verify available ErrorCode constants and current usages:


🏁 Script executed:

#!/bin/bash
rg -nP 'enum\s+ErrorCode|INVALID_.*JWT|BEAN_VALIDATION_FAIL' -C2

Length of output: 3399


🏁 Script executed:

#!/bin/bash
# Print the first 200 lines of ErrorType enum for context
sed -n '1,200p' capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java

echo "----"

# Print the first 200 lines of ErrorCode enum for context
sed -n '1,200p' capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java

echo "----"

# Search for occurrences of INVALID_JWT in the codebase
rg -n 'INVALID_JWT' .

echo "----"

# Search for occurrences of INVALID_AUTH_TOKEN in the codebase
rg -n 'INVALID_AUTH_TOKEN' .

echo "----"

# Search for occurrences of INVALID_TOKEN in the codebase
rg -n 'INVALID_TOKEN' .

Length of output: 7110


Refactor INVALID_JWT to use an appropriate ErrorCode

Mapping INVALID_JWT to ErrorCode.BEAN_VALIDATION_FAIL is misleading—clients receiving this error will think there’s a validation issue with their input, not an expired or invalid JSON Web Token. Please update the enum to use a more semantically correct constant.

• In capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java (around line 25):

- INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
+ INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_TOKEN, LogLevel.WARN),

• If you’d prefer a JWT-specific code, add to ErrorCode.java:

INVALID_JWT("유효하지 않은 JWT입니다."),

and then map accordingly:

- INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.BEAN_VALIDATION_FAIL, LogLevel.WARN),
+ INVALID_JWT(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_JWT, LogLevel.WARN),

• Don’t forget to update any tests or error-handling logic that expect the old BEAN_VALIDATION_FAIL for this case.

🤖 Prompt for AI Agents
In
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java
around line 25, INVALID_JWT is incorrectly mapped to
ErrorCode.BEAN_VALIDATION_FAIL; change the mapping to a semantically correct
error code (e.g., ErrorCode.INVALID_JWT). If ErrorCode.INVALID_JWT does not
exist, add it to
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java
(e.g., INVALID_JWT("유효하지 않은 JWT입니다.")) and then update the enum entry in
ErrorType to use that constant; finally, run and update any tests or
error-handling logic that assumed the previous BEAN_VALIDATION_FAIL mapping.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
capturecat-core/src/main/java/com/capturecat/core/api/auth/TokenController.java (1)

37-38: 401 Mapping Verified; Confirm Exception Detail Handling

  • The HTTP status mapping for token-related errors in ErrorType.java (lines 28–31, 36–38) all use HttpStatus.UNAUTHORIZED as intended; the earlier verification script confirms consistent 401 responses.
  • In capturecat-core/src/main/java/com/capturecat/core/support/response/ApiResponse.java (lines 19–21), the error(ErrorType) overload constructs an ApiResponse with a null body and wraps only the ErrorType in ErrorMessage, omitting e.getMessage().

If you intend to surface sanitized exception details in your API responses:

  • You can leverage the existing overload error(ErrorType, Object... args) to pass formatted parameters into your ErrorMessage.
  • Or introduce a dedicated overload, e.g. error(ErrorType, String detail), to include a safe message string alongside the error type.
capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java (1)

73-82: Propagating ExpiredJwtException is correct; broaden invalid-token handling and clarify API semantics.

  • Keeping expiry as an exception and mapping it at the caller matches the new 401 behavior. Good.
  • Consider also treating UnsupportedJwtException as invalid (returns false), which covers unsupported/changed algs.
  • Minor nit: ExpiredJwtException import is unused in this class; safe to remove.
  • Optional hardening: isAccessToken/isRefreshToken can NPE if "type" claim is missing. Invert equals to avoid NPE.

Apply these diffs:

@@
-    } catch (SignatureException | MalformedJwtException | IllegalArgumentException e) {
+    } catch (SignatureException | MalformedJwtException | IllegalArgumentException | UnsupportedJwtException e) {
       return false;
     }

Additional edits outside the selected lines:

@@
-import io.jsonwebtoken.ExpiredJwtException;
+// import io.jsonwebtoken.ExpiredJwtException; // not used directly here; remove if not referenced elsewhere
+import io.jsonwebtoken.UnsupportedJwtException;

And to avoid possible NPEs when type claim is absent:

@@
-  public boolean isAccessToken(String accessToken) {
-    return getTokenType(accessToken).equals(TokenType.ACCESS.name());
-  }
+  public boolean isAccessToken(String accessToken) {
+    return TokenType.ACCESS.name().equals(getTokenType(accessToken));
+  }
@@
-  public boolean isRefreshToken(String refreshToken) {
-    return getTokenType(refreshToken).equals(TokenType.REFRESH.name());
-  }
+  public boolean isRefreshToken(String refreshToken) {
+    return TokenType.REFRESH.name().equals(getTokenType(refreshToken));
+  }
capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java (2)

51-58: Unify error handling in-filter; also catch CoreException from resolveToken.

Right now, a CoreException from resolveToken(authHeader) (Line 48) bypasses this try/catch and relies on global handlers, while invalid/expired tokens here are handled in-filter. For a single, predictable 401 path and fewer surprises in filter chains, include resolveToken and catch CoreException locally.

Also, the comment on Line 56 mentions “만료 시 …” but this branch handles generic invalid tokens; consider moving that comment to the ExpiredJwtException catch.

Proposed refactor:

@@
-    String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰
-
-    // 토큰 유효성 검사
-    try {
-      if (!jwtUtil.isAccessToken(accessToken)
-        || !jwtUtil.isValid(accessToken)
-        || tokenService.isBlacklisted(accessToken)) {
-        //만료 시 client에 즉시 응답. client는 재발급 요청 수행.
-        rejectInvalidToken(response, ErrorType.INVALID_ACCESS_TOKEN);
-        return;
-      }
-    } catch (ExpiredJwtException e) {
+    try {
+      String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰
+      // 토큰 유효성 검사
+      if (!jwtUtil.isAccessToken(accessToken)
+        || !jwtUtil.isValid(accessToken)
+        || tokenService.isBlacklisted(accessToken)) {
+        rejectInvalidToken(response, ErrorType.INVALID_ACCESS_TOKEN);
+        return;
+      }
+      // downstream needs username/role; consider parsing once and reusing claims if perf becomes a concern
+      request.setAttribute("ACCESS_TOKEN", accessToken);
+    } catch (ExpiredJwtException e) {
+      // 만료 시 client에 즉시 응답. client는 재발급 요청 수행.
       rejectInvalidToken(response, ErrorType.ACCESS_TOKEN_EXPIRED);
       return;
+    } catch (com.capturecat.core.support.error.CoreException e) {
+      rejectInvalidToken(response, e.getErrorType());
+      return;
     }

Optionally parse claims once to avoid repeated parsing (getUsername/getRole/isAccessToken/isValid each parse). If desired, we can draft that refactor next.


75-79: Set UTF-8 charset (Korean messages) and consider WWW-Authenticate for Bearer.

Error bodies contain Korean; set explicit UTF-8 to avoid client mis-decoding. Adding WWW-Authenticate improves interoperability for Bearer flows.

   private void rejectInvalidToken(HttpServletResponse response, ErrorType errorType) throws IOException {
     response.setStatus(HttpStatus.UNAUTHORIZED.value());
-    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+    response.setCharacterEncoding(java.nio.charset.StandardCharsets.UTF_8.name());
+    // Optional: advertise Bearer auth error context per RFC 6750
+    response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"" + errorType.name() + "\"");
     objectMapper.writeValue(response.getWriter(), ApiResponse.error(errorType));
   }

If you prefer to keep headers minimal, feel free to skip the WWW-Authenticate line.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 98b0a26 and a7c907d.

📒 Files selected for processing (4)
  • capturecat-core/src/main/java/com/capturecat/core/api/auth/TokenController.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java (2 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy-dev
🔇 Additional comments (1)
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java (1)

23-24: LGTM: clearer client guidance on expired tokens.

The updated messages explicitly direct the client to reissue or re-login, matching the new 401 semantics. Ensure clients don’t parse these human messages for logic; they should key off ErrorType or HTTP status.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
capturecat-core/src/test/java/com/capturecat/core/api/auth/TokenApiTest.java (2)

91-94: Rename test to reflect 401 (consistency/readability).

The method name still says returnsBadRequest while we now expect 401. Rename to avoid confusion for future readers.

-	@DisplayName("Refresh token 재발급 실패 - 잘못된 Refresh token으로 요청할 경우 에러 응답")
-	void reissue_withInvalidRefreshToken_returnsBadRequest() throws Exception {
+	@DisplayName("Refresh token 재발급 실패 - 잘못된 Refresh token 요청 시 401 Unauthorized 반환")
+	void reissue_withInvalidRefreshToken_returnsUnauthorized() throws Exception {

93-98: Strengthen negative assertions: ensure no auth headers are leaked on error.

On failure responses, asserting that neither Authorization nor Refresh headers are present helps prevent accidental token issuance on error paths.

 			.andExpect(status().isUnauthorized())
+			.andExpect(header().doesNotExist(HttpHeaders.AUTHORIZATION))
+			.andExpect(header().doesNotExist(JwtUtil.REFRESH_TOKEN_HEADER))
 			.andExpect(jsonPath("$.result").value("ERROR"))
 			.andExpect(jsonPath("$.error").exists());
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a7c907d and a016e97.

📒 Files selected for processing (1)
  • capturecat-core/src/test/java/com/capturecat/core/api/auth/TokenApiTest.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy-dev
🔇 Additional comments (1)
capturecat-core/src/test/java/com/capturecat/core/api/auth/TokenApiTest.java (1)

95-95: Expectation updated to 401 — matches PR objective.

Good change. Asserting Unauthorized here aligns the test with the new ErrorType-to-HTTP-status mapping and the client’s expectation of 401s for token problems.

@jaelyangChoi jaelyangChoi merged commit bbe7ef7 into develop Aug 26, 2025
6 checks passed
@jaelyangChoi jaelyangChoi deleted the feature/#97-redis branch August 26, 2025 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redis 도입

3 participants