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#
- 2026-05-01T00:05:31Z — 동일 사용자(
sub: 95921906-...)의 Cognito user 캐시 적재 (1시간 TTL) - 2026-05-01T00:07:59Z — Access token 발급 (
iat: 1777594079) - 2026-05-01T01:07:59Z — Access token 만료 (
exp: 1777597679) - 2026-05-01T01:08:01Z — API 요청 도착, JWT 로컬 검증 통과 (캐시), Cognito user 캐시 만료로
GetUserAPI 호출 → 만료 토큰으로 인해 실패
Error Log#
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:20—verify_authenticated_request! - JWT 로컬 검증 (캐시):
lib/cupix/auth/verification.rb:55→verify_jwt_access_token! - 캐시 적중 시 만료 체크 우회:
lib/cupix/auth/verification.rb:218 - Cognito API 호출:
lib/cupix/auth/verification.rb:81→Cupix::Aws::Cognito.get_user_by_access_token - Failure point:
lib/cupix/aws/cognito.rb:209—client.get_user(access_token: access_token)
1단계: JWT 검증 캐싱 — 토큰이 유효할 때 캐시에 저장되며, 최소 300초 TTL이 적용된다:
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를 호출한다:
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 로그를 출력한다:
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 = 1→expires_in = 300.seconds→ 캐시 저장 - 토큰 만료 2초 후: 캐시 적중 →
VerifiedAccessToken반환 (만료 체크 우회) - User 캐시 만료:
get_user_by_access_token→client.get_user(expired_token)→NotAuthorizedException
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api "95921906-a69c-4c94-b17b-27a12d1d8b18"
Time range: 2026-04-30T23:00:00Z to 2026-05-01T02:00:00Z
핵심 로그 타임라인:
{"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에 만료).
{"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 캐시 만료 후 재호출 시도 — 이 직후 에러 발생:
{"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.error→Cupix::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#
service:cupixworks-api status:error "Cognito get_user_by_access_token failed"
이 쿼리로 동일 에러의 빈도를 추적. 현재 단건이므로 별도 알림 설정은 불필요하나, 빈도가 증가하면 캐시 TTL 수정의 우선순위를 높여야 한다.
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial