Skip to content

Conversation

@kidonge
Copy link
Collaborator

@kidonge kidonge commented Dec 30, 2025

비밀번호를 입력하여 정상적으로 방 입장 시에 JWT 토큰을 발급하여 관리하도록 하였습니다.
비정상적인 루트인 url를 통해 방 입장 시도 시 토큰을 확인하여 유효성 검사 로직을 추가하였습니다.

Summary by Sourcery

비밀번호로 보호되는 채팅방에 안전하게 입장할 수 있도록 JWT 기반의 방 액세스 토큰을 추가하고, 방 참가 요청 시 액세스를 검증합니다.

New Features:

  • 사용자가 비밀 채팅방의 비밀번호를 성공적으로 검증했을 때, 단기간만 유효한 JWT 방 액세스 토큰을 발급합니다.
  • 비밀 방에 참가할 때, AJAX 요청에 X-Room-Token 헤더를 통해 방별 액세스 토큰을 포함하고 이를 검증합니다.

Enhancements:

  • 방 액세스 토큰을 위해 전용 JWT 코어 프로바이더와 키 설정을 도입합니다.
  • 잘못되었거나 누락된 방 액세스 토큰을 전용 예외로 처리하고, 표준화된 에러 응답 코드 40061을 사용합니다.
  • 사용자 정의 헤더 X-Room-Token을 허용하도록 CORS 구성을 확장합니다.
  • 프런트엔드 방 입장 플로우를 업데이트하여 방 액세스 토큰을 저장·전파·처리하고, 토큰이 유효하지 않을 경우 사용자에게 리다이렉트 동작을 수행합니다.
Original summary in English

Summary by Sourcery

Add JWT-based room access tokens to secure entry into password-protected chat rooms and validate access on room join requests.

New Features:

  • Issue short-lived JWT room access tokens when a user successfully validates a password for a secret chat room.
  • Include a per-room access token in AJAX requests via an X-Room-Token header and validate it on room join for secret rooms.

Enhancements:

  • Introduce a shared JWT core provider and key configuration specifically for room access tokens.
  • Handle invalid or missing room access tokens with a dedicated exception and standardized error response code 40061.
  • Extend CORS configuration to allow the custom X-Room-Token header.
  • Update front-end room entry flows to store, propagate, and react to room access tokens, redirecting users when tokens are invalid.

@kidonge kidonge requested review from SeJonJ and taesikk December 30, 2025 05:25
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 30, 2025

Reviewer's Guide

전용 JWT 기반 방 입장 토큰 플로우를 추가하여, 비밀번호 입력 시 단기(Short‑lived) 토큰을 발급하고 이후 방 재입장 시에는 URL 직접 접근을 신뢰하지 않고 이 토큰을 검증하도록 변경했으며, 이에 맞춰 백엔드, 프론트엔드, CORS 및 에러 처리 로직을 수정합니다.

JWT 기반 방 입장 플로우 시퀀스 다이어그램

sequenceDiagram
    actor User
    participant Browser
    participant FrontendApp as NodeJS_Frontend
    participant ChatRoomController
    participant ChatRoomService
    participant JwtRoomProvider
    participant JwtCoreProvider
    participant RedisService
    participant ExceptionController

    rect rgb(230,230,250)
        User->>Browser: Open room password popup
        Browser->>FrontendApp: Submit roomId and roomPwd
        FrontendApp->>ChatRoomController: POST /api/room/{roomId}/validatePwd
        ChatRoomController->>ChatRoomService: validatePwd(email, roomId, roomPwd)
        ChatRoomService->>RedisService: getRedisDataByDataType(roomId, CHATROOM)
        RedisService-->>ChatRoomService: ChatRoom
        ChatRoomService->>ChatRoomService: Check password and user count
        alt Password and user count valid
            ChatRoomService->>JwtRoomProvider: create(roomId, email)
            JwtRoomProvider->>JwtCoreProvider: create(subject=email, claims, expireMs)
            JwtCoreProvider-->>JwtRoomProvider: jwtToken
            JwtRoomProvider-->>ChatRoomService: jwtToken
            ChatRoomService-->>ChatRoomController: {isValidate=true, token}
            ChatRoomController-->>FrontendApp: Response with token
            FrontendApp->>Browser: Store token in sessionStorage.roomAccessToken
        else Invalid password or room full
            ChatRoomService-->>ChatRoomController: {isValidate=false}
            ChatRoomController-->>FrontendApp: Response without token
            FrontendApp->>User: Show error toast
        end
    end

    rect rgb(220,245,220)
        User->>Browser: Click join room (URL or button)
        Browser->>FrontendApp: Request to join
        FrontendApp->>Browser: Read sessionStorage.roomAccessToken
        FrontendApp->>ChatRoomController: GET /api/room/{roomId}/join
        Note over FrontendApp,ChatRoomController: Sends headers Authorization and X-Room-Token
        ChatRoomController->>ChatRoomService: findRoomById(roomId)
        ChatRoomService->>RedisService: getRedisDataByDataType(roomId, CHATROOM)
        RedisService-->>ChatRoomService: ChatRoom
        ChatRoomService-->>ChatRoomController: ChatRoom
        alt Room is secretChk = true
            ChatRoomController->>JwtRoomProvider: validate(roomToken, roomId)
            JwtRoomProvider->>JwtCoreProvider: parse(token)
            JwtCoreProvider-->>JwtRoomProvider: Claims
            JwtRoomProvider->>JwtRoomProvider: Verify type ROOM_ACCESS and roomId
            alt Token invalid
                JwtRoomProvider-->>ChatRoomController: throw InvalidRoomAccessException
                ChatRoomController->>ExceptionController: InvalidRoomAccessException
                ExceptionController-->>FrontendApp: 400 {code:40061}
                FrontendApp->>Browser: Redirect to roomlist.html
            else Token valid
                JwtRoomProvider-->>ChatRoomController: success
                ChatRoomController->>FrontendApp: Join room response
                FrontendApp->>User: Enter room
            end
        else Room not secretChk
            ChatRoomController->>FrontendApp: Join room without token validation
        end
    end
Loading

잘못된 방 입장 토큰에 대한 에러 처리 시퀀스 다이어그램

sequenceDiagram
    participant FrontendApp as NodeJS_Frontend
    participant ChatRoomController
    participant JwtRoomProvider
    participant JwtCoreProvider
    participant ExceptionController

    FrontendApp->>ChatRoomController: GET /api/room/{roomId}/join with invalid or missing X-Room-Token
    ChatRoomController->>JwtRoomProvider: validate(token, roomId)
    alt Token null or empty
        JwtRoomProvider-->>ChatRoomController: throw InvalidRoomAccessException
    else Token present but invalid
        JwtRoomProvider->>JwtCoreProvider: parse(token)
        JwtCoreProvider-->>JwtRoomProvider: Exception during parse or wrong claims
        JwtRoomProvider-->>ChatRoomController: throw InvalidRoomAccessException
    end
    ChatRoomController->>ExceptionController: InvalidRoomAccessException
    ExceptionController-->>FrontendApp: 400 {code:40061, message:Invalid or missing room access token}
    FrontendApp->>FrontendApp: Detect code 40061
    FrontendApp->>FrontendApp: Alert user and redirect to roomlist.html
Loading

JWT 방 입장 및 관련 서비스에 대한 업데이트된 클래스 다이어그램

classDiagram
    class ChatRoomService {
        - RedisService redisService
        - ChatKafkaProducer chatKafkaProducer
        - JwtRoomProvider jwtRoomProvider
        + Map validatePwd(String email, String roomId, String roomPwd)
    }

    class ChatRoomController {
        - ChatRoomService chatRoomService
        - RoutingInstanceProvider instanceProvider
        - RedisService redisService
        - UserService userService
        - JwtRoomProvider jwtRoomProvider
        + ResponseEntity createRoom(...)
        + ResponseEntity joinRoom(String roomId, String authorization, String roomToken, HttpServletRequest request, HttpServletResponse response)
        + ResponseEntity validatePwd(String roomId, String roomPwd)
    }

    class ExceptionController {
        + Map handleInvalidRoomAccess()
    }

    class InvalidRoomAccessException {
        + InvalidRoomAccessException(String message)
    }

    ExceptionController <|-- InvalidRoomAccessException

    class JwtRoomProvider {
        - long EXPIRE_MS
        - JwtCoreProvider jwtCore
        + JwtRoomProvider(Key key)
        + String create(String roomId, String userId)
        + void validate(String token, String roomId)
    }

    class JwtCoreProvider {
        - Key key
        + JwtCoreProvider(Key key)
        + String create(String subject, Map claims, long expireMs)
        + Claims parse(String token)
    }

    class JwtKeyConfig {
        + Key roomJwtKey(String secret)
    }

    class JwtTokenType {
        <<enumeration>>
        ROOM_ACCESS
    }

    class ChatRoom {
        + String roomId
        + String roomPwd
        + int userCount
        + int maxUserCnt
        + boolean secretChk
    }

    class RedisService {
        + ChatRoom getRedisDataByDataType(String key, DataType dataType, Class clazz)
    }

    class DataType {
        <<enumeration>>
        CHATROOM
    }

    class UserService
    class RoutingInstanceProvider
    class ChatKafkaProducer

    ChatRoomService --> JwtRoomProvider : uses
    ChatRoomService --> RedisService : reads ChatRoom
    ChatRoomService --> ChatRoom : validates password

    ChatRoomController --> ChatRoomService : service calls
    ChatRoomController --> JwtRoomProvider : validate room token
    ChatRoomController --> UserService : get oauth info
    ChatRoomController --> RoutingInstanceProvider

    JwtRoomProvider --> JwtCoreProvider : delegates
    JwtRoomProvider --> JwtTokenType : uses ROOM_ACCESS
    JwtCoreProvider --> JwtKeyConfig : gets Key bean
    JwtKeyConfig --> JwtRoomProvider : provides roomJwtKey

    ExceptionController <.. ChatRoomController : handles InvalidRoomAccessException
    InvalidRoomAccessException --> ExceptionController : inner static

    ChatRoomService --> ChatKafkaProducer
Loading

파일 단위 변경 사항

Change Details Files
방 입장 토큰을 위한 전용 JWT 프로바이더와 서명 키 도입
  • 설정 가능한 서명 Key를 사용해 공통 JWT 생성/파싱을 담당하는 JwtCoreProvider 추가
  • 방 전용 토큰을 위한 ROOM_ACCESS 타입을 가진 JwtTokenType enum 추가
  • jwt.room.secret에서 파생된 @bean roomJwtKey를 노출하는 JwtKeyConfig 설정 추가
  • roomId 클레임을 포함하는 ROOM_ACCESS 토큰을 생성하고, roomId 및 토큰 타입을 기준으로 토큰을 검증하는 JwtRoomProvider 추가
springboot-backend/src/main/java/webChat/security/jwt/core/JwtCoreProvider.java
springboot-backend/src/main/java/webChat/security/jwt/JwtTokenType.java
springboot-backend/src/main/java/webChat/security/jwt/config/JwtKeyConfig.java
springboot-backend/src/main/java/webChat/security/jwt/JwtRoomProvider.java
채팅방 플로우에 방용 JWT 프로바이더를 연결하고 시크릿 방에 토큰 검증을 강제
  • JwtRoomProvider를 ChatRoomService와 ChatRoomController에 주입
  • validatePwd가 email을 인자로 받도록 변경하고, isValidate를 계산한 뒤 성공 시 JwtRoomProvider로 방 입장 토큰을 발급해 검증 결과와 함께 반환
  • joinRoom에서 선택적 X-Room-Token 헤더를 읽고, 시크릿 방(secretChk)인 경우 JwtRoomProvider를 통해 roomId에 대해 토큰을 검증한 뒤에만 진행
  • validatePwd 엔드포인트가 인증된 사용자 이메일을 서비스로 전달하도록 변경
springboot-backend/src/main/java/webChat/service/chatroom/ChatRoomService.java
springboot-backend/src/main/java/webChat/controller/ChatRoomController.java
잘못되었거나 누락된 방 입장 토큰을 전용 예외와 에러 코드로 처리
  • BadRequestException을 상속하는 InvalidRoomAccessException 정의
  • InvalidRoomAccessException을 처리하는 @ExceptionHandler를 추가하고, 잘못되었거나 누락된 방 입장 토큰에 대해 코드 40061과 일반적인 메시지를 반환
springboot-backend/src/main/java/webChat/controller/ExceptionController.java
프론트엔드 AJAX 호출 및 UI 플로우에 방 입장 토큰 전파
  • tokenAjax를 확장하여, sessionStorage.roomAccessToken이 존재할 경우 이를 X-Room-Token 헤더에 포함
  • RoomPopup에서 비밀번호 검증 성공 시 isValidate === true를 요구하고, 반환된 토큰을 sessionStorage.roomAccessToken에 저장한 뒤 성공 UI를 표시
  • RoomSettingsPopup에서 비밀번호 검증 성공 시 토큰이 존재하면 sessionStorage.roomAccessToken에 저장한 후 방 정보를 다시 로딩
  • Kurento 등록 에러 핸들러에서 백엔드 코드 40061을 잘못된 방 입장으로 간주하고, 알림을 띄운 뒤 방 목록으로 리다이렉트
nodejs-frontend/static/js/common/ajaxUtil.js
nodejs-frontend/static/js/popup/room_popup.js
nodejs-frontend/static/js/popup/room_settings_popup.js
nodejs-frontend/static/js/rtc/kurento-service.js
CORS를 통해 방 입장 토큰 헤더 허용
  • 브라우저가 크로스 오리진 요청에서 방 입장 토큰 헤더를 전송할 수 있도록, CORS 설정에 allowedHeaders로 X-Room-Token을 추가
springboot-backend/src/main/java/webChat/config/SslConfig.java

연결 가능성이 있는 이슈


Tips and commands

Interacting with Sourcery

  • Trigger a new review: PR에 @sourcery-ai review라고 코멘트하여 새 리뷰를 트리거합니다.
  • Continue discussions: Sourcery의 리뷰 코멘트에 직접 답글을 달아 논의를 이어갑니다.
  • Generate a GitHub issue from a review comment: 리뷰 코멘트에서 이슈를 생성해 달라고 요청하거나, 해당 리뷰 코멘트에 @sourcery-ai issue라고 답글을 달아 이슈를 생성할 수 있습니다.
  • Generate a pull request title: PR 제목 어디에나 @sourcery-ai를 적어 언제든지 제목 생성을 요청할 수 있습니다. PR에 @sourcery-ai title이라고 코멘트해 제목을 (재)생성할 수도 있습니다.
  • Generate a pull request summary: PR 본문 어디에나 @sourcery-ai summary를 적어, 원하는 위치에 PR 요약을 생성할 수 있습니다. PR에 @sourcery-ai summary라고 코멘트해 요약을 (재)생성할 수도 있습니다.
  • Generate reviewer's guide: PR에 @sourcery-ai guide라고 코멘트해 리뷰어 가이드를 (재)생성합니다.
  • Resolve all Sourcery comments: PR에 @sourcery-ai resolve라고 코멘트해 모든 Sourcery 코멘트를 해결 처리합니다. 이미 모든 코멘트를 반영했고 더 이상 보고 싶지 않을 때 유용합니다.
  • Dismiss all Sourcery reviews: PR에 @sourcery-ai dismiss라고 코멘트해 기존 Sourcery 리뷰를 모두 해제합니다. 새 리뷰를 처음부터 받고 싶을 때 특히 유용합니다. 이때 @sourcery-ai review 코멘트를 잊지 말고 추가해 새 리뷰를 트리거하세요.

Customizing Your Experience

dashboard에서 다음을 설정할 수 있습니다:

  • Sourcery가 생성하는 PR 요약, 리뷰어 가이드 등 리뷰 기능을 활성화/비활성화
  • 리뷰 언어 변경
  • 커스텀 리뷰 지침 추가/삭제/수정
  • 기타 리뷰 설정 조정

Getting Help

Original review guide in English

Reviewer's Guide

Adds a dedicated JWT-based room access token flow so that entering a password grants a short‑lived token, and subsequent room joins validate this token instead of trusting direct URL access, with corresponding backend, frontend, CORS, and error handling changes.

Sequence diagram for JWT-based room access flow

sequenceDiagram
    actor User
    participant Browser
    participant FrontendApp as NodeJS_Frontend
    participant ChatRoomController
    participant ChatRoomService
    participant JwtRoomProvider
    participant JwtCoreProvider
    participant RedisService
    participant ExceptionController

    rect rgb(230,230,250)
        User->>Browser: Open room password popup
        Browser->>FrontendApp: Submit roomId and roomPwd
        FrontendApp->>ChatRoomController: POST /api/room/{roomId}/validatePwd
        ChatRoomController->>ChatRoomService: validatePwd(email, roomId, roomPwd)
        ChatRoomService->>RedisService: getRedisDataByDataType(roomId, CHATROOM)
        RedisService-->>ChatRoomService: ChatRoom
        ChatRoomService->>ChatRoomService: Check password and user count
        alt Password and user count valid
            ChatRoomService->>JwtRoomProvider: create(roomId, email)
            JwtRoomProvider->>JwtCoreProvider: create(subject=email, claims, expireMs)
            JwtCoreProvider-->>JwtRoomProvider: jwtToken
            JwtRoomProvider-->>ChatRoomService: jwtToken
            ChatRoomService-->>ChatRoomController: {isValidate=true, token}
            ChatRoomController-->>FrontendApp: Response with token
            FrontendApp->>Browser: Store token in sessionStorage.roomAccessToken
        else Invalid password or room full
            ChatRoomService-->>ChatRoomController: {isValidate=false}
            ChatRoomController-->>FrontendApp: Response without token
            FrontendApp->>User: Show error toast
        end
    end

    rect rgb(220,245,220)
        User->>Browser: Click join room (URL or button)
        Browser->>FrontendApp: Request to join
        FrontendApp->>Browser: Read sessionStorage.roomAccessToken
        FrontendApp->>ChatRoomController: GET /api/room/{roomId}/join
        Note over FrontendApp,ChatRoomController: Sends headers Authorization and X-Room-Token
        ChatRoomController->>ChatRoomService: findRoomById(roomId)
        ChatRoomService->>RedisService: getRedisDataByDataType(roomId, CHATROOM)
        RedisService-->>ChatRoomService: ChatRoom
        ChatRoomService-->>ChatRoomController: ChatRoom
        alt Room is secretChk = true
            ChatRoomController->>JwtRoomProvider: validate(roomToken, roomId)
            JwtRoomProvider->>JwtCoreProvider: parse(token)
            JwtCoreProvider-->>JwtRoomProvider: Claims
            JwtRoomProvider->>JwtRoomProvider: Verify type ROOM_ACCESS and roomId
            alt Token invalid
                JwtRoomProvider-->>ChatRoomController: throw InvalidRoomAccessException
                ChatRoomController->>ExceptionController: InvalidRoomAccessException
                ExceptionController-->>FrontendApp: 400 {code:40061}
                FrontendApp->>Browser: Redirect to roomlist.html
            else Token valid
                JwtRoomProvider-->>ChatRoomController: success
                ChatRoomController->>FrontendApp: Join room response
                FrontendApp->>User: Enter room
            end
        else Room not secretChk
            ChatRoomController->>FrontendApp: Join room without token validation
        end
    end
Loading

Sequence diagram for error handling on invalid room access token

sequenceDiagram
    participant FrontendApp as NodeJS_Frontend
    participant ChatRoomController
    participant JwtRoomProvider
    participant JwtCoreProvider
    participant ExceptionController

    FrontendApp->>ChatRoomController: GET /api/room/{roomId}/join with invalid or missing X-Room-Token
    ChatRoomController->>JwtRoomProvider: validate(token, roomId)
    alt Token null or empty
        JwtRoomProvider-->>ChatRoomController: throw InvalidRoomAccessException
    else Token present but invalid
        JwtRoomProvider->>JwtCoreProvider: parse(token)
        JwtCoreProvider-->>JwtRoomProvider: Exception during parse or wrong claims
        JwtRoomProvider-->>ChatRoomController: throw InvalidRoomAccessException
    end
    ChatRoomController->>ExceptionController: InvalidRoomAccessException
    ExceptionController-->>FrontendApp: 400 {code:40061, message:Invalid or missing room access token}
    FrontendApp->>FrontendApp: Detect code 40061
    FrontendApp->>FrontendApp: Alert user and redirect to roomlist.html
Loading

Updated class diagram for JWT room access and related services

classDiagram
    class ChatRoomService {
        - RedisService redisService
        - ChatKafkaProducer chatKafkaProducer
        - JwtRoomProvider jwtRoomProvider
        + Map validatePwd(String email, String roomId, String roomPwd)
    }

    class ChatRoomController {
        - ChatRoomService chatRoomService
        - RoutingInstanceProvider instanceProvider
        - RedisService redisService
        - UserService userService
        - JwtRoomProvider jwtRoomProvider
        + ResponseEntity createRoom(...)
        + ResponseEntity joinRoom(String roomId, String authorization, String roomToken, HttpServletRequest request, HttpServletResponse response)
        + ResponseEntity validatePwd(String roomId, String roomPwd)
    }

    class ExceptionController {
        + Map handleInvalidRoomAccess()
    }

    class InvalidRoomAccessException {
        + InvalidRoomAccessException(String message)
    }

    ExceptionController <|-- InvalidRoomAccessException

    class JwtRoomProvider {
        - long EXPIRE_MS
        - JwtCoreProvider jwtCore
        + JwtRoomProvider(Key key)
        + String create(String roomId, String userId)
        + void validate(String token, String roomId)
    }

    class JwtCoreProvider {
        - Key key
        + JwtCoreProvider(Key key)
        + String create(String subject, Map claims, long expireMs)
        + Claims parse(String token)
    }

    class JwtKeyConfig {
        + Key roomJwtKey(String secret)
    }

    class JwtTokenType {
        <<enumeration>>
        ROOM_ACCESS
    }

    class ChatRoom {
        + String roomId
        + String roomPwd
        + int userCount
        + int maxUserCnt
        + boolean secretChk
    }

    class RedisService {
        + ChatRoom getRedisDataByDataType(String key, DataType dataType, Class clazz)
    }

    class DataType {
        <<enumeration>>
        CHATROOM
    }

    class UserService
    class RoutingInstanceProvider
    class ChatKafkaProducer

    ChatRoomService --> JwtRoomProvider : uses
    ChatRoomService --> RedisService : reads ChatRoom
    ChatRoomService --> ChatRoom : validates password

    ChatRoomController --> ChatRoomService : service calls
    ChatRoomController --> JwtRoomProvider : validate room token
    ChatRoomController --> UserService : get oauth info
    ChatRoomController --> RoutingInstanceProvider

    JwtRoomProvider --> JwtCoreProvider : delegates
    JwtRoomProvider --> JwtTokenType : uses ROOM_ACCESS
    JwtCoreProvider --> JwtKeyConfig : gets Key bean
    JwtKeyConfig --> JwtRoomProvider : provides roomJwtKey

    ExceptionController <.. ChatRoomController : handles InvalidRoomAccessException
    InvalidRoomAccessException --> ExceptionController : inner static

    ChatRoomService --> ChatKafkaProducer
Loading

File-Level Changes

Change Details Files
Introduce dedicated JWT provider and signing key for room-access tokens
  • Add JwtCoreProvider for shared JWT creation/parsing using a configurable signing Key
  • Add JwtTokenType enum with ROOM_ACCESS type for room-specific tokens
  • Add JwtKeyConfig configuration to expose a @bean roomJwtKey derived from jwt.room.secret
  • Add JwtRoomProvider to create ROOM_ACCESS tokens with roomId claim and validate tokens against roomId and token type
springboot-backend/src/main/java/webChat/security/jwt/core/JwtCoreProvider.java
springboot-backend/src/main/java/webChat/security/jwt/JwtTokenType.java
springboot-backend/src/main/java/webChat/security/jwt/config/JwtKeyConfig.java
springboot-backend/src/main/java/webChat/security/jwt/JwtRoomProvider.java
Wire room JWT provider into chat room flow and enforce token on secret rooms
  • Inject JwtRoomProvider into ChatRoomService and ChatRoomController
  • Change validatePwd to accept email, compute isValidate, and on success issue a room access token via JwtRoomProvider and return it along with validation result
  • In joinRoom, read optional X-Room-Token header and, for secret rooms, validate it against the roomId via JwtRoomProvider before proceeding
  • Adjust validatePwd endpoint to pass the authenticated user email into the service
springboot-backend/src/main/java/webChat/service/chatroom/ChatRoomService.java
springboot-backend/src/main/java/webChat/controller/ChatRoomController.java
Handle invalid or missing room access tokens with a dedicated exception and error code
  • Define InvalidRoomAccessException as a BadRequestException subclass
  • Add @ExceptionHandler for InvalidRoomAccessException returning code 40061 and a generic message for invalid/missing room access tokens
springboot-backend/src/main/java/webChat/controller/ExceptionController.java
Propagate room access token through frontend AJAX calls and UI flows
  • Extend tokenAjax to include X-Room-Token header sourced from sessionStorage.roomAccessToken when present
  • On successful password validation in RoomPopup, require isValidate === true, then store the returned token into sessionStorage.roomAccessToken and show success UI
  • On successful password validation in RoomSettingsPopup, store token into sessionStorage.roomAccessToken when present before reloading room info
  • In Kurento registration error handler, treat backend code 40061 as invalid room access, show an alert, and redirect to the room list
nodejs-frontend/static/js/common/ajaxUtil.js
nodejs-frontend/static/js/popup/room_popup.js
nodejs-frontend/static/js/popup/room_settings_popup.js
nodejs-frontend/static/js/rtc/kurento-service.js
Allow room access token header through CORS
  • Update CORS configuration to allow X-Room-Token in allowedHeaders so browsers can send the room access token header in cross-origin requests
springboot-backend/src/main/java/webChat/config/SslConfig.java

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 4개의 이슈를 발견했고, 몇 가지 상위 수준 피드백을 남깁니다:

  • JwtCoreProvider는 @component로 애노테이션되어 있지만 JwtRoomProvider는 Spring을 거치지 않고 new로 직접 인스턴스화하고 있습니다. 이는 일관성이 없고 Spring 빈 와이어링과 충돌할 수 있습니다. JwtCoreProvider에서 @component를 제거하고 단순 유틸리티로 취급하거나, 적절한 key/qualifier를 사용해 공유 JwtCoreProvider 빈을 주입하는 방식 중 하나로 정리하는 것을 고려해 주세요.
  • ChatRoomService.validatePwd가 이제 원시 타입의 Map<String, Object>를 반환하는데, 이로 인해 백엔드와 프론트엔드 사이의 계약이 불투명하고 깨지기 쉬워졌습니다. isValidate, token 필드를 가진 작은 DTO를 도입하면 API가 더 명확해지고 타입 안정성이 높아질 것입니다.
  • 프론트엔드에서 roomAccessToken이 sessionStorage에 전역으로 저장되고 tokenAjax에서 모든 요청에 자동으로 첨부되고 있지만, 방을 떠날 때 이 값이 지워지지 않는 것으로 보입니다. 헤더 사용 범위를 방 입장 관련 호출로만 한정하고, 사용자가 방에서 나갈 때 토큰을 명시적으로 제거하는 것을 고려해 주세요.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- JwtCoreProvider is annotated as a @Component but JwtRoomProvider bypasses Spring and instantiates it with `new`, which is inconsistent and may conflict with Spring’s bean wiring; consider either removing @Component from JwtCoreProvider and treating it as a plain utility or injecting a shared JwtCoreProvider bean with the appropriate key/qualifier.
- ChatRoomService.validatePwd now returns a raw `Map<String, Object>` which makes the contract between backend and frontend opaque and brittle; introducing a small DTO (e.g., with `isValidate` and `token` fields) would make the API clearer and type-safe.
- On the frontend, `roomAccessToken` is stored globally in sessionStorage and automatically attached in `tokenAjax` for all requests, but it does not appear to be cleared when leaving a room; consider scoping the header usage only to room-join related calls and explicitly removing the token when the user exits a room.

## Individual Comments

### Comment 1
<location> `springboot-backend/src/main/java/webChat/security/jwt/core/JwtCoreProvider.java:31-35` </location>
<code_context>
+                .compact();
+    }
+
+    public Claims parse(String token) {
+        return Jwts.parser()
+                .setSigningKey(key)
+                .build()
+                .parseClaimsJws(token)
+                .getBody();
+    }
</code_context>

<issue_to_address>
**issue (bug_risk):** The JJWT parser usage (`Jwts.parser().build()`) looks inconsistent with current library APIs and may not compile.

Please confirm the jjwt version in use. For jjwt 0.11+ this should be updated to `Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)` rather than relying on `parser()`, which no longer supports `.build()` and will not compile.
</issue_to_address>

### Comment 2
<location> `springboot-backend/src/main/java/webChat/security/jwt/core/JwtCoreProvider.java:12-17` </location>
<code_context>
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class JwtRoomProvider {
+    private static final long EXPIRE_MS = 1000L * 60 * 30;
</code_context>

<issue_to_address>
**suggestion:** JwtCoreProvider is annotated as a Spring component but is instantiated manually, which can be misleading.

Since JwtRoomProvider calls `new JwtCoreProvider(key)` directly, the `@Component` on JwtCoreProvider is effectively unused and suggests it’s managed by Spring when it isn’t. Please either remove `@Component` or refactor to inject JwtCoreProvider as a Spring bean with the key provided via configuration.
</issue_to_address>

### Comment 3
<location> `springboot-backend/src/main/java/webChat/security/jwt/JwtRoomProvider.java:38-55` </location>
<code_context>
+        return jwtCore.create(userId, claims, EXPIRE_MS);
+    }
+
+    public void validate(String token, String roomId) throws ExceptionController.InvalidRoomAccessException {
+        try {
+            if (StringUtil.isNullOrEmpty(token)) {
+                throw new ExceptionController.InvalidRoomAccessException("Invalid access");
+            }
+            Claims claims = jwtCore.parse(token);
+
+            if (!JwtTokenType.ROOM_ACCESS.name().equals(claims.get("type"))) {
+                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
+            }
+
+            String tokenRoomId = claims.get("roomId", String.class);
+            if (!roomId.equals(tokenRoomId)) {
+                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
+            }
+        } catch (Exception e) {
+            throw new ExceptionController.InvalidRoomAccessException("Invalid access");
+        }
</code_context>

<issue_to_address>
**suggestion:** The broad `catch (Exception)` rewraps `InvalidRoomAccessException`, losing the more specific failure reasons.

The inner `InvalidRoomAccessException` branches end up unused because the outer `catch (Exception e)` always wraps them as a generic "Invalid access". If you need the specific messages, either rethrow `InvalidRoomAccessException` unchanged or narrow the catch to only parsing/runtime exceptions so existing `InvalidRoomAccessException`s can bubble up.

```suggestion
    public void validate(String token, String roomId) throws ExceptionController.InvalidRoomAccessException {
        if (StringUtil.isNullOrEmpty(token)) {
            throw new ExceptionController.InvalidRoomAccessException("Invalid access");
        }

        try {
            Claims claims = jwtCore.parse(token);

            if (!JwtTokenType.ROOM_ACCESS.name().equals(claims.get("type"))) {
                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
            }

            String tokenRoomId = claims.get("roomId", String.class);
            if (!roomId.equals(tokenRoomId)) {
                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
            }
        } catch (RuntimeException e) {
            // Token parsing or claim access failed: treat as generic invalid access
            throw new ExceptionController.InvalidRoomAccessException("Invalid access");
        }
```
</issue_to_address>

### Comment 4
<location> `nodejs-frontend/static/js/popup/room_popup.js:195-198` </location>
<code_context>

         let successCallback = function(result) {
-            if (result?.data && result?.result === 'success') {
+            if (result?.data?.isValidate === true) {
+                var roomToken = result.data.token;
+                sessionStorage.setItem('roomAccessToken', roomToken);
                 self.showToast('방에 정상적으로 입장했습니다!', 'success');
                 $('#enterRoomModal').modal('hide');

</code_context>

<issue_to_address>
**issue (bug_risk):** The password validation success handler no longer shows feedback when the password is wrong.

This change removed the `result?.result === 'success'` check and now only handles `result?.data?.isValidate === true`. When `isValidate` is `false` (wrong password/over capacity), this callback does nothing, so the user gets no error and the modal stays open. Add an `else` branch to show an error toast (e.g. as in `room_settings_popup.js`) or reuse existing invalid‑password error handling so failures are surfaced to the user.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • JwtCoreProvider is annotated as a @component but JwtRoomProvider bypasses Spring and instantiates it with new, which is inconsistent and may conflict with Spring’s bean wiring; consider either removing @component from JwtCoreProvider and treating it as a plain utility or injecting a shared JwtCoreProvider bean with the appropriate key/qualifier.
  • ChatRoomService.validatePwd now returns a raw Map<String, Object> which makes the contract between backend and frontend opaque and brittle; introducing a small DTO (e.g., with isValidate and token fields) would make the API clearer and type-safe.
  • On the frontend, roomAccessToken is stored globally in sessionStorage and automatically attached in tokenAjax for all requests, but it does not appear to be cleared when leaving a room; consider scoping the header usage only to room-join related calls and explicitly removing the token when the user exits a room.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- JwtCoreProvider is annotated as a @Component but JwtRoomProvider bypasses Spring and instantiates it with `new`, which is inconsistent and may conflict with Spring’s bean wiring; consider either removing @Component from JwtCoreProvider and treating it as a plain utility or injecting a shared JwtCoreProvider bean with the appropriate key/qualifier.
- ChatRoomService.validatePwd now returns a raw `Map<String, Object>` which makes the contract between backend and frontend opaque and brittle; introducing a small DTO (e.g., with `isValidate` and `token` fields) would make the API clearer and type-safe.
- On the frontend, `roomAccessToken` is stored globally in sessionStorage and automatically attached in `tokenAjax` for all requests, but it does not appear to be cleared when leaving a room; consider scoping the header usage only to room-join related calls and explicitly removing the token when the user exits a room.

## Individual Comments

### Comment 1
<location> `springboot-backend/src/main/java/webChat/security/jwt/core/JwtCoreProvider.java:31-35` </location>
<code_context>
+                .compact();
+    }
+
+    public Claims parse(String token) {
+        return Jwts.parser()
+                .setSigningKey(key)
+                .build()
+                .parseClaimsJws(token)
+                .getBody();
+    }
</code_context>

<issue_to_address>
**issue (bug_risk):** The JJWT parser usage (`Jwts.parser().build()`) looks inconsistent with current library APIs and may not compile.

Please confirm the jjwt version in use. For jjwt 0.11+ this should be updated to `Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)` rather than relying on `parser()`, which no longer supports `.build()` and will not compile.
</issue_to_address>

### Comment 2
<location> `springboot-backend/src/main/java/webChat/security/jwt/core/JwtCoreProvider.java:12-17` </location>
<code_context>
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class JwtRoomProvider {
+    private static final long EXPIRE_MS = 1000L * 60 * 30;
</code_context>

<issue_to_address>
**suggestion:** JwtCoreProvider is annotated as a Spring component but is instantiated manually, which can be misleading.

Since JwtRoomProvider calls `new JwtCoreProvider(key)` directly, the `@Component` on JwtCoreProvider is effectively unused and suggests it’s managed by Spring when it isn’t. Please either remove `@Component` or refactor to inject JwtCoreProvider as a Spring bean with the key provided via configuration.
</issue_to_address>

### Comment 3
<location> `springboot-backend/src/main/java/webChat/security/jwt/JwtRoomProvider.java:38-55` </location>
<code_context>
+        return jwtCore.create(userId, claims, EXPIRE_MS);
+    }
+
+    public void validate(String token, String roomId) throws ExceptionController.InvalidRoomAccessException {
+        try {
+            if (StringUtil.isNullOrEmpty(token)) {
+                throw new ExceptionController.InvalidRoomAccessException("Invalid access");
+            }
+            Claims claims = jwtCore.parse(token);
+
+            if (!JwtTokenType.ROOM_ACCESS.name().equals(claims.get("type"))) {
+                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
+            }
+
+            String tokenRoomId = claims.get("roomId", String.class);
+            if (!roomId.equals(tokenRoomId)) {
+                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
+            }
+        } catch (Exception e) {
+            throw new ExceptionController.InvalidRoomAccessException("Invalid access");
+        }
</code_context>

<issue_to_address>
**suggestion:** The broad `catch (Exception)` rewraps `InvalidRoomAccessException`, losing the more specific failure reasons.

The inner `InvalidRoomAccessException` branches end up unused because the outer `catch (Exception e)` always wraps them as a generic "Invalid access". If you need the specific messages, either rethrow `InvalidRoomAccessException` unchanged or narrow the catch to only parsing/runtime exceptions so existing `InvalidRoomAccessException`s can bubble up.

```suggestion
    public void validate(String token, String roomId) throws ExceptionController.InvalidRoomAccessException {
        if (StringUtil.isNullOrEmpty(token)) {
            throw new ExceptionController.InvalidRoomAccessException("Invalid access");
        }

        try {
            Claims claims = jwtCore.parse(token);

            if (!JwtTokenType.ROOM_ACCESS.name().equals(claims.get("type"))) {
                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
            }

            String tokenRoomId = claims.get("roomId", String.class);
            if (!roomId.equals(tokenRoomId)) {
                throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
            }
        } catch (RuntimeException e) {
            // Token parsing or claim access failed: treat as generic invalid access
            throw new ExceptionController.InvalidRoomAccessException("Invalid access");
        }
```
</issue_to_address>

### Comment 4
<location> `nodejs-frontend/static/js/popup/room_popup.js:195-198` </location>
<code_context>

         let successCallback = function(result) {
-            if (result?.data && result?.result === 'success') {
+            if (result?.data?.isValidate === true) {
+                var roomToken = result.data.token;
+                sessionStorage.setItem('roomAccessToken', roomToken);
                 self.showToast('방에 정상적으로 입장했습니다!', 'success');
                 $('#enterRoomModal').modal('hide');

</code_context>

<issue_to_address>
**issue (bug_risk):** The password validation success handler no longer shows feedback when the password is wrong.

This change removed the `result?.result === 'success'` check and now only handles `result?.data?.isValidate === true`. When `isValidate` is `false` (wrong password/over capacity), this callback does nothing, so the user gets no error and the modal stays open. Add an `else` branch to show an error toast (e.g. as in `room_settings_popup.js`) or reuse existing invalid‑password error handling so failures are surfaced to the user.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +31 to +35
public Claims parse(String token) {
return Jwts.parser()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): JJWT 파서 사용 방식(Jwts.parser().build())이 현재 라이브러리 API와 일치하지 않아 컴파일되지 않을 수 있습니다.

사용 중인 jjwt 버전을 확인해 주세요. jjwt 0.11+ 를 사용 중이라면, parser()에 의존하지 말고 Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)로 업데이트해야 합니다. parser()는 더 이상 .build()를 지원하지 않으며, 이대로는 컴파일되지 않습니다.

Original comment in English

issue (bug_risk): The JJWT parser usage (Jwts.parser().build()) looks inconsistent with current library APIs and may not compile.

Please confirm the jjwt version in use. For jjwt 0.11+ this should be updated to Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token) rather than relying on parser(), which no longer supports .build() and will not compile.

Comment on lines +195 to 198
if (result?.data?.isValidate === true) {
var roomToken = result.data.token;
sessionStorage.setItem('roomAccessToken', roomToken);
self.showToast('방에 정상적으로 입장했습니다!', 'success');
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): 비밀번호 검증 성공 핸들러가 이제 비밀번호가 틀렸을 때 피드백을 보여주지 않습니다.

이 변경으로 result?.result === 'success' 체크가 제거되고, result?.data?.isValidate === true 인 경우만 처리하게 되었습니다. isValidatefalse(비밀번호 오류/정원 초과 등)인 경우 이 콜백은 아무 것도 하지 않기 때문에, 사용자는 아무런 에러도 보지 못하고 모달은 열린 상태로 남습니다. room_settings_popup.js와 같이 에러 토스트를 보여주는 else 분기를 추가하거나, 기존의 비밀번호 오류 처리 로직을 재사용해서 실패 시에도 사용자에게 명확히 노출되도록 해 주세요.

Original comment in English

issue (bug_risk): The password validation success handler no longer shows feedback when the password is wrong.

This change removed the result?.result === 'success' check and now only handles result?.data?.isValidate === true. When isValidate is false (wrong password/over capacity), this callback does nothing, so the user gets no error and the modal stays open. Add an else branch to show an error toast (e.g. as in room_settings_popup.js) or reuse existing invalid‑password error handling so failures are surfaced to the user.

@SeJonJ
Copy link
Owner

SeJonJ commented Dec 30, 2025

@kidonge sourcery 가 남긴 내용 확인 부탁드립니다(수정 필요 여부, 필요하다면 수정도!)
@taesikk 코드 리뷰 확인 부탁드립니다

Copy link
Collaborator

@taesikk taesikk left a comment

Choose a reason for hiding this comment

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

훌륭합니다

@SeJonJ
Copy link
Owner

SeJonJ commented Jan 6, 2026

@kidonge 최근에 수정된 내용이 있어, 메인 브렌치에서 pull 받은 후 머지 부탁드립니다

@SeJonJ SeJonJ added the enhancement New feature or request label Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants