ES /docs

SignupOperation#team_signup — missing federated user check

RCA: Failed to change password in Cognito

Error Log#

Datadog Logs

text
Failed to change password in Cognito: matthew.smith479@det.nsw.edu.au

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-04-10T03:31:06.391Z
  • 최근 발생: 2026-04-10T03:31:06.391Z

Root Cause Summary#

nswgov 테넌트의 초대받은 사용자 matthew.smith479@det.nsw.edu.auPOST /api/v1/signups/team 엔드포인트를 통해 팀 초대를 수락하는 과정에서, SignupOperation.team_signupuser.active_state!를 호출하여 after_update :update_cognito_user 콜백이 트리거되었습니다. 이 콜백은 Cupix::Aws::Cognito.update_user!를 호출했으나, 해당 사용자가 Cognito user pool에 존재하지 않아 user_exists? 체크에서 false를 반환하고 아무 작업 없이 early return했습니다. 그럼에도 Ruby의 begin/rescue/else 구조 특성상 예외가 발생하지 않았으므로 else 블록이 실행되어 "User updated in Cognito"라는 잘못된 성공 로그가 기록되었습니다. 이후 change_password가 호출되었을 때, 사용자가 Cognito에 실제로 존재하지 않으므로 AWS SDK가 UserNotFoundException ("User does not exist.")을 반환하여 HTTP 500 에러가 발생했습니다. 근본 원인은 초대(invited) 상태의 사용자에 대해 Cognito 사용자가 생성되지 않은 상태에서 비밀번호 변경을 시도한 것이며, change_password 호출 전에 Cognito 사용자 존재 여부를 확인하거나 생성하는 로직이 없다는 점입니다.

Technical Analysis#

Code Path#

  • Entry point: Api::V1::SignupsController#signup_on_teamsignups_controller.rb:24-25
ruby
# app/controllers/api/v1/signups_controller.rb:24-25
def signup_on_team
  user = SignupOperation.team_signup(params)
  • Step 1: SignupOperation.team_signup에서 사용자 조회 및 상태 검증 (signup_operation.rb:55-68). 사용자는 invited 상태로 확인됨.
ruby
# app/operations/signup_operation.rb:55-68
user = UserRepository.find_by(team: team, email: params[:email])
# ...
unless %w[created invited].include? user.state
  raise Cupix::Errors::Parameter.new(code: 'ARG10063', reason: 'User email is already active')
end
  • Step 2: 사용자 정보 업데이트 및 상태 전환 (signup_operation.rb:76-81). set_default_info에서 firstname을 "Matt" → "Matthew"로 변경하고, active_state!로 상태를 invitedactive로 변경. 이 saveafter_update :update_cognito_user 콜백을 트리거함.
ruby
# app/operations/signup_operation.rb:76-81
set_default_info(user, params)
user.confirmation_token = nil
user.invitation_token = nil
user.joined_at = user.confirmed_at = DateTime.now
user.active_state!
  • Step 3: after_update 콜백에서 update_cognito_user 실행 (lib/cupix/aws/cognito/user.rb:74-83). saved_changesfirstname이 포함되어 있어 콜백이 진행됨. Cupix::Aws::Cognito.update_user!가 호출됨.
ruby
# lib/cupix/aws/cognito/user.rb:74-83
def update_cognito_user
  return if saved_changes.blank? || (saved_changes.keys & %w[firstname lastname locale]).blank?

  flush_cognito_user_cache
  Cupix::Aws::Cognito.update_user!(self)
rescue StandardError => e
  Cupix::Logger.error("Failed to update cognito user: #{self.email}", ...)
else
  Cupix::Logger.info("User updated in Cognito: #{email}", ..., changes: saved_changes)
end
  • Step 4 (핵심 문제 지점): Cupix::Aws::Cognito.update_user!에서 user_exists? 체크 (lib/cupix/aws/cognito.rb:101-124). 사용자가 Cognito에 존재하지 않아 line 102에서 early return. 예외가 발생하지 않았으므로, 콜백의 else 블록이 실행되어 "User updated in Cognito" 로그가 기록됨 — 이는 거짓 양성(false positive) 로그.
ruby
# lib/cupix/aws/cognito.rb:101-124
def update_user!(user)
  return unless user_exists?(user.email)  # ← false 반환, early return

  client.admin_update_user_attributes(...)
  Cupix::Logger.info("User updated in Cognito: #{user.email}", ...)
end
  • Failure point: SignupOperation.change_passwordCupix::Aws::Cognito.change_password (lib/cupix/aws/cognito.rb:162-172). Cognito에 사용자가 존재하지 않으므로 admin_set_user_password 호출이 UserNotFoundException을 발생시킴.
ruby
# lib/cupix/aws/cognito.rb:162-172
def change_password(user, password)
  response = client.admin_set_user_password(
    user_pool_id: $AWS.fetch(:cognito).fetch(:user_pool_id),
    username: user.email,
    password: password,
    permanent: true
  )
rescue StandardError => e
  Cupix::Logger.error("Failed to change password in Cognito: #{user.email}", ..., error: e.message)
  raise Cupix::Errors::System.new(code: 'SYS20000', reason: 'Failed to change password')
end
  • 사용자가 Cognito에 없는 이유: after_create :create_cognito_user 콜백 (lib/cupix/aws/cognito/user.rb:10)이 사용자 생성 시 Cognito 사용자를 만들지만, 이 사용자는 초대(invited) 상태로 생성되었을 때 skip_cognito_user_creation 플래그가 설정되었거나, Cognito 생성이 실패하여 rescue 블록에서 에러만 로그하고 넘어갔을 가능성이 있음 (lib/cupix/aws/cognito/user.rb:68-69). 어느 쪽이든, 팀 가입 시점에서 Cognito 사용자 존재를 확인하지 않는 것이 문제.
ruby
# lib/cupix/aws/cognito/user.rb:10, 54-72
after_create :create_cognito_user, unless: :skip_cognito_user_creation
# ...
def create_cognito_user
  # ...
rescue StandardError => e
  Cupix::Logger.error("Failed to create cognito user: #{self.email}", ...)
  # ← 예외를 삼키고 넘어감 — Cognito 사용자 없이도 앱 사용자는 생성됨
end

Log Evidence#

전체 요청 트레이스 (request_id: 14c07c9b-ab34-4e87-846a-992578d137a5):

text
Datadog query: service:cupixworks-api @http.request_id:14c07c9b-ab34-4e87-846a-992578d137a5

1. HTTP 요청 로그:

json
{
  "method": "POST",
  "path": "/api/v1/signups/team",
  "controller": "Api::V1::SignupsController#signup_on_team",
  "status": 500,
  "duration": "408.64ms",
  "params": {
    "firstname": "Matthew",
    "lastname": "Smith",
    "email": "matthew.smith479@det.nsw.edu.au",
    "team_domain": "sinsw"
  },
  "error": {
    "reason": "Failed to change password",
    "code": "SYS20000",
    "class": "Cupix::Errors::System"
  },
  "tenant": "nswgov",
  "region": "ap-southeast-2"
}

2. INFO — 사용자 Cognito 업데이트 (거짓 양성):

json
{
  "message": "User updated in Cognito: matthew.smith479@det.nsw.edu.au",
  "function": "update_cognito_user",
  "class": "Cognito",
  "changes": "state: invited -> active, firstname: Matt -> Matthew, joined_at updated, confirmed_at updated, encrypted_password changed, invitation_token: 166385 cleared"
}

3. INFO — 권한 캐시 플러시:

text
Flush cached permissions for User 1300
Deleting cached_permission on user 1300

4. ERROR — 비밀번호 변경 실패:

json
{
  "message": "Failed to change password in Cognito: matthew.smith479@det.nsw.edu.au",
  "function": "change_password",
  "module": "Cupix::Aws",
  "class": "Cognito",
  "error.msg": "User does not exist."
}

14일간 패턴 확인:

text
Datadog query: service:cupixworks-api status:error "Failed to change password in Cognito"

결과: 14일간 이 에러는 1건만 발생. 반복 패턴 아님.

Fix Recommendation#

즉시 조치 (Critical)#

  • app/operations/signup_operation.rb:96change_password 호출 전에 Cognito 사용자 존재 여부를 확인하고, 없으면 생성하는 로직 추가. Cupix::Aws::Cognito.user_exists?(user.email) 체크 후 Cupix::Aws::Cognito.create_user!(user) 호출. 이것은 team_signupself_signup 모두에 적용 필요 (lines 96, 229).

단기 개선 (1주 이내)#

  • lib/cupix/aws/cognito/user.rb:74-83update_cognito_user 콜백에서 update_user!가 early return한 경우와 실제 업데이트가 완료된 경우를 구분할 수 있도록, update_user!의 반환값을 확인하거나 update_user!가 실제로 업데이트를 수행했을 때만 성공 로그를 남기도록 수정. 현재 else 블록은 예외가 없으면 무조건 실행되어 거짓 양성 로그를 생성함.

  • lib/cupix/aws/cognito.rb:101-102update_user! 메서드에서 사용자가 존재하지 않을 때 로그를 남기거나, 반환값을 통해 호출자에게 알려야 함. 현재는 return unless user_exists?로 아무 표시 없이 종료됨.

장기 개선 (재발 방지)#

  • 초대 사용자 생성 시 Cognito 사용자 생성 실패를 silent하게 처리하지 않도록 create_cognito_user 콜백의 rescue 블록 (lib/cupix/aws/cognito/user.rb:68-69) 재검토. Cognito 사용자 생성은 핵심 의존성이므로 실패 시 명확한 상태 추적 필요 (예: cognito_user_id가 nil인 사용자에 대한 모니터링).

  • change_passwordupdate_user! 모두 Cognito API 호출 전에 일관된 존재 확인 패턴 적용. 현재 update_user!는 존재 확인 후 early return하지만 change_password는 확인 없이 바로 호출하여 동작이 불일치함.

Monitoring#

  • cognito_user_idnilactive 상태 사용자 모니터링:
text
Datadog query: service:cupixworks-api status:error "Failed to change password in Cognito"
text
Datadog query: service:cupixworks-api "Failed to create cognito user"
  • Cognito 사용자 생성 실패율 추적을 위한 메트릭 추가 권장.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard