SignupOperation#team_signup — missing federated user check
RCA: Failed to change password in Cognito
Error Log#
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.au가 POST /api/v1/signups/team 엔드포인트를 통해 팀 초대를 수락하는 과정에서, SignupOperation.team_signup이 user.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_team→signups_controller.rb:24-25
# 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상태로 확인됨.
# 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!로 상태를invited→active로 변경. 이save가after_update :update_cognito_user콜백을 트리거함.
# 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_changes에firstname이 포함되어 있어 콜백이 진행됨.Cupix::Aws::Cognito.update_user!가 호출됨.
# 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) 로그.
# 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_password→Cupix::Aws::Cognito.change_password(lib/cupix/aws/cognito.rb:162-172). Cognito에 사용자가 존재하지 않으므로admin_set_user_password호출이UserNotFoundException을 발생시킴.
# 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 사용자 존재를 확인하지 않는 것이 문제.
# 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):
Datadog query: service:cupixworks-api @http.request_id:14c07c9b-ab34-4e87-846a-992578d137a5
1. HTTP 요청 로그:
{
"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 업데이트 (거짓 양성):
{
"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 — 권한 캐시 플러시:
Flush cached permissions for User 1300
Deleting cached_permission on user 1300
4. ERROR — 비밀번호 변경 실패:
{
"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일간 패턴 확인:
Datadog query: service:cupixworks-api status:error "Failed to change password in Cognito"
결과: 14일간 이 에러는 1건만 발생. 반복 패턴 아님.
Fix Recommendation#
즉시 조치 (Critical)#
app/operations/signup_operation.rb:96—change_password호출 전에 Cognito 사용자 존재 여부를 확인하고, 없으면 생성하는 로직 추가.Cupix::Aws::Cognito.user_exists?(user.email)체크 후Cupix::Aws::Cognito.create_user!(user)호출. 이것은team_signup과self_signup모두에 적용 필요 (lines 96, 229).
단기 개선 (1주 이내)#
-
lib/cupix/aws/cognito/user.rb:74-83—update_cognito_user콜백에서update_user!가 early return한 경우와 실제 업데이트가 완료된 경우를 구분할 수 있도록,update_user!의 반환값을 확인하거나update_user!가 실제로 업데이트를 수행했을 때만 성공 로그를 남기도록 수정. 현재else블록은 예외가 없으면 무조건 실행되어 거짓 양성 로그를 생성함. -
lib/cupix/aws/cognito.rb:101-102—update_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_password와update_user!모두 Cognito API 호출 전에 일관된 존재 확인 패턴 적용. 현재update_user!는 존재 확인 후 early return하지만change_password는 확인 없이 바로 호출하여 동작이 불일치함.
Monitoring#
cognito_user_id가nil인active상태 사용자 모니터링:
Datadog query: service:cupixworks-api status:error "Failed to change password in Cognito"
Datadog query: service:cupixworks-api "Failed to create cognito user"
- Cognito 사용자 생성 실패율 추적을 위한 메트릭 추가 권장.
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard