ES /docs

[Integration] failed to refresh token for procore integration(524) - state: failed, error_message: T

RCA: [Integration] failed to refresh token for procore integration(524)

Error Log#

Datadog Logs

text
[Integration] failed to refresh token for procore integration(524) - state: failed, error_message: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-04-20T03:57:07.402Z
  • 최근 발생: 2026-04-20T03:57:07.402Z

Root Cause Summary#

Procore integration(524)의 access token이 2026-02-17에 만료된 후 약 2개월간 갱신되지 않은 상태에서, 사용자가 POST /api/v1/reviews/2pujqr/integrations/procore/access_token API를 호출하여 token refresh를 시도했다. 이때 ProcoreOperation.refresh_token이 Procore OAuth 서버에 만료된 refresh token을 전송했고, Procore가 invalid_grant (401 Unauthorized)로 거부했다. OAuth refresh token은 일정 기간 미사용 시 자동 만료되는데, 2개월간 사용되지 않아 이미 revoke된 상태였다. 이는 특정 integration(524)에 국한된 문제이며, 동일 시간대 다른 procore integration들은 정상적으로 token refresh에 성공했다.

Technical Analysis#

Code Path#

  1. Entry point — 사용자가 API 호출: POST /api/v1/reviews/2pujqr/integrations/procore/access_token
ruby
# app/controllers/api/v1/integrations_controller.rb:35-38
def access_token
  token = repository_instance.access_token
  render_json 200, token
end
  1. Token 만료 확인 — access token이 10분 이내 만료 예정이면 refresh 시도:
ruby
# app/repositories/integration_repository.rb:76-101
def access_token
  check_failed_state('Failed to get access token')

  # ...
  if @model.expired_at < 10.minutes.since
    refresh_token
  else
    # return cached token
  end
end

이 integration의 expired_at2026-02-17 20:58:20 UTC로, 이미 2개월 전에 만료되었으므로 refresh_token이 호출됨.

  1. Token refresh 시도 — Procore OAuth 서버에 refresh token 전송:
ruby
# app/repositories/integration_repository.rb:103-143
def refresh_token
  check_failed_state('Failed to get refresh token')
  # ...
  token = operation_class.refresh_token(@model.refresh_token, @model.region)
rescue StandardError => e
  @model.refresh_token_failed_at = DateTime.now
  @model.refresh_token_expired_at = nil
  @model.failed_state!
  Cupix::Logger.error("[Integration] failed to refresh token for ...")
  raise e
end
  1. Procore OAuth 호출 실패invalid_grant 응답:
ruby
# app/operations/procore_operation.rb:42-73
def self.refresh_token(refresh_token, region)
  data = {
    client_id: $OAUTH[:procore][:client_id],
    client_secret: $OAUTH[:procore][:client_secret],
    grant_type: 'refresh_token',
    refresh_token: refresh_token,
    redirect_uri: $OAUTH[:procore][:redirect_uri]
  }
  begin
    url = "#{$OAUTH[:procore][:site]}#{$OAUTH[:procore][:refresh_url]}"
    response = RestClient.post(url, data)
  rescue RestClient::Exception => e
    response = JSON.parse(e.response)
    Cupix::Logger.error("Procore refresh_token failed: #{response['error']}, ...")
    raise Cupix::Errors::Parameter.new(
      code: 'ARG10000',
      reason: "Procore Authentication failed: #{response['error']}",
      message: response['error_description']
    )
  end
end

Procore API가 invalid_grant를 반환하면 RestClient::Exception이 발생하고, Cupix::Errors::Parameter로 re-raise됨.

  1. 상태 전이 — integration 상태가 activefailed로 변경:
ruby
# app/repositories/integration_repository.rb:135-138
@model.refresh_token_failed_at = DateTime.now
@model.refresh_token_expired_at = nil
@model.refresh_token_response_body = token
@model.failed_state!

failed 상태로 전환되면 이후 check_failed_state guard에 의해 추가 refresh 시도가 차단됨 (integration_repository.rb:309-322).

Log Evidence#

동일 request에서 4개의 로그가 발생했으며, request_id: 8ee61752-6d7a-4b23-b283-8fb7733c3eec로 연결됨.

사용한 Datadog 쿼리:

text
service:cupixworks-api @request_id:8ee61752-6d7a-4b23-b283-8fb7733c3eec

Log 1 (info) — Token refresh 시도, access token이 2026-02-17에 만료됨을 확인:

text
[Integration] refresh token for procore integration(524) - state: active, expired_at: 2026-02-17 20:58:20 UTC, refresh_token_expired_at:

refresh_token_expired_at이 비어 있어 refresh token 만료 시점이 추적되지 않았음.

Log 2 (error) — Procore OAuth 서버가 refresh 요청 거부:

text
Procore refresh_token failed: invalid_grant, message: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. - error: 401 Unauthorized

Log 3 (error) — Integration 상태 failed로 전환 (클러스터 대표 에러):

text
[Integration] failed to refresh token for procore integration(524) - state: failed, error_message: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.

Log 4 (info) — 클라이언트에 400 응답:

text
[400] POST /api/v1/reviews/2pujqr/integrations/procore/access_token (Api::V1::IntegrationsController#access_token)

추가 검색 결과:

text
service:cupixworks-api "procore" "refresh" status:info

동일 시간대에 integration(1020), integration(141), integration(966), integration(689) 등 다수의 다른 procore integration들이 정상적으로 token refresh에 성공함. 이 문제는 integration(524)에만 국한됨.

text
service:cupixworks-api "integration(524)" status:error

최근 7일간 integration(524) 관련 에러는 이 1건만 존재.

Fix Recommendation#

즉시 조치 (Critical)#

이 에러는 수정이 필요한 버그가 아님. Procore OAuth refresh token이 장기간 미사용으로 만료(revoke)된 것은 정상적인 OAuth lifecycle이다. 사용자가 Procore에서 재인증(re-authorize)하면 새 token이 발급되어 integration이 복구된다.

다만 이 에러가 error level로 기록되어 error-sweeper에 포착된 것은 적절하지 않을 수 있다. OAuth token 만료로 인한 invalid_grant는 예상 가능한 운영 시나리오이므로 warn level이 더 적절하다.

  • app/repositories/integration_repository.rb:140Cupix::Logger.errorCupix::Logger.warn으로 변경 검토
  • app/operations/procore_operation.rb:57 — 마찬가지로 invalid_grant 응답에 대해 warn level 검토

단기 개선 (1주 이내)#

  • Refresh token 만료 추적 개선: refresh_token_expired_at이 비어 있는 경우가 있음 (Log 1에서 확인). Procore refresh token의 실제 만료 기간을 문서에서 확인하고, token 발급 시점에 refresh_token_expired_at을 정확히 설정하여 cron job(Cupix::Cron::Integration.renew_before_expiration)이 만료 전에 자동 갱신할 수 있도록 해야 함.
  • 현재 refresh_token_expired_atnil이면 cron의 refresh_token_due_to_expire scope에서 제외됨 (statable/integration.rb:12). 이로 인해 사용자가 수동으로 API를 호출할 때까지 token이 갱신되지 않고 방치될 수 있음.

장기 개선 (재발 방지)#

  • failed 상태의 integration에 대해 사용자에게 자동 알림(이메일 또는 in-app notification)을 보내 재인증을 유도하는 메커니즘 도입 검토
  • Integration 건강 상태 대시보드 구축: 오래 만료된 token을 가진 integration을 사전에 식별

Monitoring#

  • invalid_grant로 인한 token refresh 실패 빈도 모니터링:
text
service:cupixworks-api "refresh_token failed" "invalid_grant"
  • failed 상태의 integration 수 추적:
text
service:cupixworks-api "[Integration] failed to refresh token"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 이 에러는 단일 integration(524)에 국한된 OAuth token 만료로, 시스템 버그가 아닌 운영 시나리오임. 사용자 재인증으로 즉시 복구 가능. 로그 레벨 조정(errorwarn)이 유일한 코드 변경 권장 사항임.