ES /docs

Procore refresh_token failed: invalid_grant, message: The provided authorization grant is invalid, e

RCA: Procore refresh_token failed: invalid_grant

Error Log#

Datadog Logs

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

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-04-20T03:57:07.402Z
  • 최근 발생: 2026-04-20T03:57:07.402Z
  • 영향 범위: calderstewart 팀(ID: 141)의 사용자 Tyler Bichan(ID: 5275)이 Procore integration(ID: 524)을 통해 access token을 요청할 때 400 에러가 반환됨. review key 2pujqr에서의 단일 요청 실패. 다른 팀(예: naylorlove)의 Procore 통합은 정상 동작 확인됨.

Root Cause Summary#

Procore integration(524)의 OAuth access token이 expired_at: 2026-02-17 20:58:20 UTC로, 요청 시점 기준 62일 이상 만료된 상태였다. 사용자가 POST /api/v1/reviews/2pujqr/integrations/procore/access_token을 호출하면서 IntegrationRepository#access_token이 만료 검사(expired_at < 10.minutes.since)를 통해 자동으로 refresh_token을 시도했으나, Procore OAuth 서버가 오래 만료된 refresh token에 대해 invalid_grant (401)를 반환했다. 핵심 원인은 Procore가 TOKEN_REFRESH_REQUIRED_PROVIDERS에 포함되지 않아 refresh_token_expired_at이 항상 nil로 설정되며, 이로 인해 cron job(Cupix::Cron::Integration.renew_before_expiration)의 refresh_token_due_to_expire 스코프에 걸리지 않아 사전 갱신이 이루어지지 않는 구조적 문제가 있다.

Technical Analysis#

Code Path#

  • Entry point: Api::V1::IntegrationsController#access_tokenapp/controllers/api/v1/integrations_controller.rb:35
ruby
# app/controllers/api/v1/integrations_controller.rb:35-38
def access_token
  token = repository_instance.access_token
  render_json 200, token
end
  • 만료 검사 및 refresh 호출: IntegrationRepository#access_tokenapp/repositories/integration_repository.rb:76-101
ruby
# app/repositories/integration_repository.rb:89-91
if @model.expired_at < 10.minutes.since
  refresh_token  # expired_at이 62일 전이므로 조건 충족 → refresh 시도
else
  { provider: @model.provider, access_token: @model.access_token, ... }
end

Integration 524의 expired_at2026-02-17 20:58:20 UTC이므로 10.minutes.since(현재 시각 + 10분)보다 과거 → refresh 호출.

  • Refresh 실행: IntegrationRepository#refresh_tokenapp/repositories/integration_repository.rb:103-170
ruby
# app/repositories/integration_repository.rb:106-128
Cupix::Logger.info("[Integration] refresh token for #{@model.provider} integration(#{@model.id}) - state: #{@model.state}, expired_at: #{@model.expired_at}, refresh_token_expired_at: #{@model.refresh_token_expired_at}")
# ... provider dispatch → ProcoreOperation
token = operation_class.refresh_token(@model.refresh_token, @model.region)
  • Procore API 호출: ProcoreOperation.refresh_tokenapp/operations/procore_operation.rb:42-73
ruby
# app/operations/procore_operation.rb:42-62
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']}, message: #{response['error_description']} - error: #{e.message}")
    raise Cupix::Errors::Parameter.new(
      code: 'ARG10000',
      reason: "Procore Authentication failed: #{response['error']}",
      message: response['error_description']
    )
  end
  # ...
end

Failure point: app/operations/procore_operation.rb:53RestClient.post가 Procore OAuth 서버로부터 401 invalid_grant 응답을 받아 RestClient::Exception 발생.

  • 실패 후 상태 전이: app/repositories/integration_repository.rb:130-142
ruby
# app/repositories/integration_repository.rb:130-142
rescue StandardError => e
  uncheck_refresh_request
  raise e if e.code == 'ARG10060'
  @model.refresh_token_failed_at = DateTime.now
  @model.refresh_token_expired_at = nil
  @model.refresh_token_response_body = token  # 주의: token은 undefined (rescue 블록에서는 line 128이 실패하여 token 미할당)
  @model.failed_state!
  Cupix::Logger.error("[Integration] failed to refresh token for #{@model.provider} integration(#{@model.id}) - state: #{@model.state}, error_message: #{e.message}")
  raise e
end

Integration이 failed 상태로 전이되고, 이후 동일 integration으로의 요청은 check_failed_state에서 차단됨.

  • 사전 갱신 누락 원인: TOKEN_REFRESH_REQUIRED_PROVIDERS에 Procore 미포함
ruby
# config/initializers/integration.rb:1
TOKEN_REFRESH_REQUIRED_PROVIDERS = %w[bim360].freeze
ruby
# app/repositories/integration_repository.rb:145-149
if TOKEN_REFRESH_REQUIRED_PROVIDERS.include?(@model.provider)
  @model.refresh_token_expired_at = 14.days.since  # bim360만 해당
else
  @model.refresh_token_expired_at = nil  # procore → 항상 nil
end
ruby
# app/models/concerns/statable/integration.rb:12
scope :refresh_token_due_to_expire, -> { not_failed.where.not(refresh_token_expired_at: nil).where('refresh_token_expired_at < ?', 1.days.since) }

refresh_token_expired_atnil이므로 cron job의 refresh_token_due_to_expire 스코프에서 제외됨 → 사전 갱신 불가.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api status:error Procore
Time: 2026-04-20T02:57:00Z ~ 2026-04-20T04:30:00Z
text
service:cupixworks-api Procore
Time: 2026-04-20T03:50:00Z ~ 2026-04-20T04:10:00Z

실패 요청 타임라인 (Integration 524, request ID: 8ee61752-6d7a-4b23-b283-8fb7733c3eec):

  1. 03:57:07.402Z [INFO] — refresh 시도 전 상태 로그:
text
[Integration] refresh token for procore integration(524) - state: active, expired_at: 2026-02-17 20:58:20 UTC, refresh_token_expired_at:

expired_at이 2월 17일로 62일 만료 상태. refresh_token_expired_at은 비어있음(nil).

  1. 03:57:07.402Z [ERROR] — Procore OAuth 서버 응답 실패:
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
  1. 03:57:07.402Z [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.
  1. 03:57:07.645Z [INFO] — HTTP 응답 400 반환:
text
Controller: Api::V1::IntegrationsController#access_token
POST /api/v1/reviews/2pujqr/integrations/procore/access_token → 400
Duration: 385.81ms
User: tyler.bichan@calderstewart.co.nz (ID: 5275)
Team: calderstewart (ID: 141)
error.class: Cupix::Errors::Parameter, error.code: ARG10000

성공 사례 비교 (Integration 966, 10분 후):

text
[Integration] refresh token for procore integration(966) - state: active, expired_at: 2026-04-20 04:03:39 UTC, refresh_token_expired_at:

→ token이 4분 전 만료 → refresh 성공 → 200 응답. Procore OAuth 서버 자체에는 문제 없음 확인.

과거 이력 검색:

text
service:cupixworks-api status:error invalid_grant
Time: 2026-04-13T00:00:00Z ~ 2026-04-20T04:30:00Z

→ 지난 7일간 invalid_grant 에러는 이 1건뿐. 단발성 이벤트.

Fix Recommendation#

즉시 조치 (Critical)#

이 에러는 62일간 미사용된 Procore integration의 refresh token이 자연 만료되어 발생한 예상 가능한 운영 시나리오이다. 사용자가 Procore에서 재인증(re-authorize)하면 해결되므로, 즉시 코드 수정은 불필요하다.

로그 레벨 변경 고려: invalid_grant 에러는 사용자의 OAuth 토큰이 만료/취소된 경우 발생하는 것으로, 시스템 장애가 아닌 사용자 조치가 필요한 상황이다. error 레벨보다 warn 레벨이 적절할 수 있다.

  • app/operations/procore_operation.rb:57Cupix::Logger.errorCupix::Logger.warn 검토
  • app/repositories/integration_repository.rb:140Cupix::Logger.errorCupix::Logger.warn 검토

단기 개선 (1주 이내)#

  1. Procore를 TOKEN_REFRESH_REQUIRED_PROVIDERS에 추가 검토: config/initializers/integration.rb:1에서 Procore를 추가하면 성공적 refresh 후 refresh_token_expired_at이 설정되어 cron job이 사전 갱신을 시도할 수 있다. 단, Procore의 refresh token 유효기간 정책을 확인한 후 적절한 기간(현재 bim360은 14일)을 설정해야 한다.

  2. token 변수 undefined 버그 수정: app/repositories/integration_repository.rb:137에서 @model.refresh_token_response_body = token은 rescue 블록 안에서 line 128의 token 할당이 실패한 경우 undefined이다. 에러 응답 본문이나 에러 메시지를 저장하도록 수정 필요.

장기 개선 (재발 방지)#

  1. 장기 미사용 integration에 대한 proactive 알림: 일정 기간(예: 30일) 이상 access token이 갱신되지 않은 integration에 대해 사용자에게 재인증을 안내하는 알림 메커니즘 검토.

  2. 통합 상태 대시보드: 팀별 integration 상태(active/failed/만료 임박)를 모니터링할 수 있는 admin 대시보드 검토.

Monitoring#

  • 기존 에러 모니터링으로 충분. invalid_grant 에러가 특정 팀/integration에 반복 발생하는지 추적:
text
service:cupixworks-api "Procore refresh_token failed" @environment:production
  • 로그 레벨을 warn으로 변경한 경우:
text
service:cupixworks-api status:warn "Procore refresh_token failed" @environment:production

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 단발성 이벤트이며, 시스템 장애가 아닌 사용자의 OAuth 인증 만료로 인한 예상된 에러. 사용자 재인증으로 즉시 해결 가능. 코드 수정이 필요하다면 로그 레벨 변경(error → warn)이 적절하다.