ES /docs

Cognito get_user_by_access_token failed: Invalid access token, #<Cupix::Auth::VerifiedAccessToken:0x

RCA: Cognito get_user_by_access_token failed: Invalid access token

Overview#

What Happened#

2026-05-01T01:08:01Z에 EU 리전(eu-central-1)의 cupixworks-api 서비스에서 Cognito GetUser API 호출이 "Invalid access token" 에러로 실패했다. JWT 토큰이 만료된 지 약 2초 후에 Cognito API를 호출하여 NotAuthorizedException이 발생한 단건 이벤트이다.

Quick Facts#

Field Value
exception.class Aws::CognitoIdentityProvider::Errors::NotAuthorizedException
exception.message Invalid access token
top_frame lib/cupix/aws/cognito.rb:209
env production, eu-central-1

Timeline#

  1. 2026-05-01T00:05:31Z — 동일 사용자(sub: 95921906-...)의 Cognito user 캐시 적재 (1시간 TTL)
  2. 2026-05-01T00:07:59Z — Access token 발급 (iat: 1777594079)
  3. 2026-05-01T01:07:59Z — Access token 만료 (exp: 1777597679)
  4. 2026-05-01T01:08:01Z — API 요청 도착, JWT 로컬 검증 통과 (캐시), Cognito user 캐시 만료로 GetUser API 호출 → 만료 토큰으로 인해 실패

Error Log#

Datadog Logs

text
Cognito get_user_by_access_token failed: Invalid access token, #<Cupix::Auth::VerifiedAccessToken:0x00007fc25db877a8 @access_token="eyJraWQiOi...", @decoded_access_token=[{"sub"=>"95921906-a69c-4c94-b17b-27a12d1d8b18", "iss"=>"https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_woHt84y4Y", "exp"=>1777597679, "iat"=>1777594079, ...}], @issuer=Cupix::Auth::Issuers::Cognito>

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-01T01:08:01.900Z
  • 최근 발생: 2026-05-01T01:08:01.900Z

단건 발생이며, 해당 사용자는 토큰 갱신 후 정상 접근 가능한 상태이므로 사용자 영향은 미미하다.

Root Cause Summary#

verify_jwt_access_token! 메서드가 JWT 검증 결과를 최소 300초 동안 캐시하면서, 토큰 만료 직전에 캐시된 VerifiedAccessToken이 만료 후에도 로컬 검증을 통과하게 된다. 동시에 get_user_by_access_token의 사용자 캐시(1시간 TTL)가 만료되어 Cognito GetUser API를 호출하게 되는데, 이때 이미 만료된 access token을 전달하므로 AWS Cognito가 NotAuthorizedException을 반환한다. 즉, 두 캐시 계층의 TTL 불일치로 인해 "로컬에서는 유효하지만 Cognito에서는 무효한" 토큰 상태가 발생하는 race condition이다.

Technical Analysis#

Code Path#

  • Entry point: lib/cupix/auth/verification.rb:20verify_authenticated_request!
  • JWT 로컬 검증 (캐시): lib/cupix/auth/verification.rb:55verify_jwt_access_token!
  • 캐시 적중 시 만료 체크 우회: lib/cupix/auth/verification.rb:218
  • Cognito API 호출: lib/cupix/auth/verification.rb:81Cupix::Aws::Cognito.get_user_by_access_token
  • Failure point: lib/cupix/aws/cognito.rb:209client.get_user(access_token: access_token)

1단계: JWT 검증 캐싱 — 토큰이 유효할 때 캐시에 저장되며, 최소 300초 TTL이 적용된다:

lib/cupix/auth/verification.rb:209-233ruby
def verify_jwt_access_token!(access_token: nil)
  jwt = JWT.decode(access_token, nil, false)
  iss = jwt[0]['iss']
  remaining_time = jwt[0]['exp'] - DateTime.now.to_i

  raise Cupix::Errors::Unauthorized.new(code: 'AUTH10030', reason: 'JWT has expired') unless remaining_time.positive?

  expires_in = [remaining_time, 300].max.seconds

  Rails.cache.fetch("cupix:auth:verification:verify_jwt_access_token:#{access_token}", expires_in: expires_in) do
    # ... 서명 검증 수행, VerifiedAccessToken 반환
  end
end

remaining_time이 양수이면 (아직 만료 전) 캐시에 저장한다. 이후 요청에서는 캐시 적중 시 remaining_time 체크를 건너뛴다 (line 214의 raise는 캐시 miss 시에만 실행).

2단계: Cognito user 캐시와 실제 API 호출 — user 캐시가 만료되면 실제 access token으로 Cognito를 호출한다:

lib/cupix/aws/cognito.rb:203-216ruby
def get_user_by_access_token(access_token: nil, sub: nil)
  sub ||= ::JWT.decode(access_token, nil, false).first['sub']

  Rails.cache.fetch(user_cache_key(sub), expires_in: 1.hour) do
    Cupix::Logger.info("Fetching an user from Cognito: #{sub}", ...)

    response = client.get_user(access_token: access_token)
  rescue ::Aws::CognitoIdentityProvider::Errors::NotAuthorizedException
    raise Cupix::Errors::Unauthorized.new(code: 'AUTH20013', reason: 'Invalid access token')
  else
    Cupix::Aws::Cognito::UserResponse.new(user_response_hash(response))
  end
end

3단계: 에러 처리와 로깅AUTH20013 예외가 verify_authenticated_request!의 rescue 블록에 잡혀 error 로그를 출력한다:

lib/cupix/auth/verification.rb:82-90ruby
rescue StandardError => e
  Cupix::Logger.error("Cognito get_user_by_access_token failed: #{e.message}, #{verified_access_token.inspect}",
                      class: 'Verification',
                      function: __method__,
                      module: 'Cupix::Auth',
                      error_type: e.class.name,
                      decoded_token_present: !verified_access_token.decoded_access_token.nil?)

  raise Cupix::Errors::Unauthorized.new(code: 'AUTH20009', reason: 'AccessToken has verified but user not found', ...)
end

Race condition 시나리오:

  • 토큰 만료 1초 전: verify_jwt_access_token! 캐시 miss → remaining_time = 1expires_in = 300.seconds → 캐시 저장
  • 토큰 만료 2초 후: 캐시 적중 → VerifiedAccessToken 반환 (만료 체크 우회)
  • User 캐시 만료: get_user_by_access_tokenclient.get_user(expired_token)NotAuthorizedException

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "95921906-a69c-4c94-b17b-27a12d1d8b18"
Time range: 2026-04-30T23:00:00Z to 2026-05-01T02:00:00Z

핵심 로그 타임라인:

json
{"timestamp": "2026-05-01 09:05:31 KST", "status": "info", "message": "Fetching an user from Cognito: 95921906-a69c-4c94-b17b-27a12d1d8b18", "class": "Cognito", "function": "get_user_by_access_token"}

위 로그는 user 캐시가 적재된 시점이다 (1시간 TTL → 10:05:31 KST에 만료).

json
{"timestamp": "2026-05-01 10:08:01 KST", "status": "info", "message": "Fetching an user from Cognito: 95921906-a69c-4c94-b17b-27a12d1d8b18", "class": "Cognito", "function": "get_user_by_access_token"}

User 캐시 만료 후 재호출 시도 — 이 직후 에러 발생:

json
{"timestamp": "2026-05-01 10:08:01 KST", "status": "error", "message": "Cognito get_user_by_access_token failed: Invalid access token, #<Cupix::Auth::VerifiedAccessToken:0x00007fc25db877a8 ...>", "class": "Verification", "function": "verify_authenticated_request!"}

토큰 내 시간 정보:

  • iat: 1777594079 (2026-05-01T00:07:59Z / 09:07:59 KST)
  • exp: 1777597679 (2026-05-01T01:07:59Z / 10:07:59 KST)
  • 에러 시각: 10:08:01 KST → 토큰 만료 후 2초

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 JWT 검증 캐시의 최소 300초 TTL로 만료된 토큰이 로컬 검증 통과, user 캐시 만료와 동시에 Cognito API 호출 시 실패 토큰 exp=10:07:59, 에러=10:08:01 (2초 차이); user 캐시는 09:05:31 적재 → ~10:05:31 만료; verify_jwt_access_token! 캐시 hit 시 exp 미검증 (verification.rb:218) Confirmed
H2 Cognito user pool 일시적 장애 (AWS 측 문제) EU 리전 단건 에러 동일 시간대 다른 사용자 에러 없음; 토큰 만료 시간과 정확히 일치하는 타이밍; 단건 발생 Rejected
H3 Cross-region 토큰 사용 (다른 리전의 user pool에서 발급된 토큰) 토큰 issuer가 eu-central-1_woHt84y4Y이며 서비스도 eu-central-1에서 운영 중; Issuers::Cognito.match?는 리전 무관 regex 사용 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

이 에러는 단건 발생이며, 토큰 만료 직후의 정상적인 edge case이다. 사용자는 자동으로 토큰을 갱신하여 재시도하므로 즉각적인 수정은 불필요하다. 다만 error 레벨 로깅은 과도하므로 warn으로 낮추는 것을 권장한다.

  • 파일: lib/cupix/auth/verification.rb:83
  • 변경: Cupix::Logger.errorCupix::Logger.warn (만료 토큰으로 인한 실패는 예상 가능한 시나리오)

단기 개선 (1주 이내)#

verify_jwt_access_token!의 캐시 TTL을 remaining_time 그대로 사용하도록 변경. 현재 [remaining_time, 300].max는 만료 임박 토큰을 300초간 유효하게 만든다.

  • 파일: lib/cupix/auth/verification.rb:216
  • 방향: expires_in = remaining_time.seconds (최소값 제거) 또는 get_user_by_access_token 호출 전 토큰 exp를 재검증하는 guard clause 추가

장기 개선 (재발 방지)#

get_user_by_access_token에서 Cognito API 호출 전에 access token의 exp 클레임을 확인하여, 이미 만료된 토큰으로 API를 호출하지 않도록 방어 로직 추가. 만료 시 즉시 AUTH10030 (JWT expired)을 반환하면 클라이언트가 refresh token으로 갱신할 수 있다.

Monitoring#

text
service:cupixworks-api status:error "Cognito get_user_by_access_token failed"

이 쿼리로 동일 에러의 빈도를 추적. 현재 단건이므로 별도 알림 설정은 불필요하나, 빈도가 증가하면 캐시 TTL 수정의 우선순위를 높여야 한다.

Risk Assessment#

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