ES /docs

BIM360 Authentication failed: - error: 400 Bad Request

RCA: BIM360 Authentication failed: 400 Bad Request

Overview#

What Happened#

2026-07-29 02:25 KST 에 cupixworks-api (us-west-2 production) 에서 POST /api/v1/facilities/3dfas/integrations 요청 중 Autodesk BIM360 OAuth token 교환이 400 Bad Request 로 실패했다. Bim360Operation.get_tokenCupix::Errors::Parameter (ARG10000) 를 발생시켰고 컨트롤러가 400 응답을 반환했다. 동일한 클라이언트가 37초 뒤 (02:25:02 KST) 재시도로 인테그레이션 생성/토큰 갱신을 정상 완료했다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter
exception.message BIM360 Authentication failed: - error: 400 Bad Request
top_frame app/operations/bim360_operation.rb:32
endpoint POST /api/v1/facilities/3dfas/integrations
controller Api::V1::IntegrationsController#create
deploy production-us-west-2-20260728T0624Z0-812cb7d9-cupixworks
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
BIM360 Integration (facility 3dfas) 1 단일 인테그레이션 생성 요청 400 실패. 사용자가 37초 뒤 재시도하여 성공했다.

Timeline#

  1. 2026-07-29 02:24:33 KST — 동일 facility 의 기존 BIM360 integration(7410) 토큰이 정상 refresh ([Integration] Successfully refreshed token for bim360 integration(7410)).
  2. 2026-07-29 02:25:00 KSTPOST /api/v1/facilities/3dfas/integrations 요청이 Bim360Operation.get_token 에서 400 Bad Request 로 실패 (request_id 590e8486-c39f-4e8e-84cb-51f6bba0c583).
  3. 2026-07-29 02:25:02 KST — 같은 facility 에서 POST /api/v1/facilities/3dfas/integrations/bim360/access_token 이 200 으로 성공.
  4. 2026-07-29 02:25:10 KSTPUT /api/v1/facilities/3dfas/integrations/bim360 (update) 200 성공.
  5. 2026-07-29 02:25:25 / 02:25:32 KST — 관련 review/facility access_token 요청 연속 200 성공.

Error Log#

Datadog Logs

text
BIM360 Authentication failed:  - error: 400 Bad Request

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-29 02:25 KST
  • 최근 발생: 2026-07-29 02:25 KST
  • 사용자 영향: 단일 요청. 동일 세션에서 즉시 재시도로 복구되었고 후속 관련 API 호출은 모두 200. 지난 14일간 동일 fingerprint 로그 9건 확인 — 유사한 low-volume, self-healing 패턴이 산발적으로 재발 중.

Root Cause Summary#

Autodesk Forge OAuth2 authorization_code 교환 요청이 Autodesk 측에서 400 Bad Request 로 거절되었다. 400 은 통상 (a) authorization code 만료·재사용, (b) redirect_uri 불일치, (c) 클라이언트 자격증명 오류 중 하나를 의미한다. 이번 case 는 같은 사용자가 37초 뒤 재시도에 성공했으므로 (b)/(c) 같은 설정 이슈가 아니라 authorization code 의 short-TTL 만료 또는 중복 사용 (사용자의 double-submit 또는 페이지 재로드) 이 가장 가능성이 높다. 다만 Bim360Operation.get_token 이 Autodesk 응답 본문에서 존재하지 않는 필드(developerMessage) 를 참조하기 때문에 로그 메시지가 BIM360 Authentication failed: - error: 400 Bad Request 로 원인 코드가 비어 렌더되어, 결정적 증거를 확보하지 못했다 — uncertain, needs verification.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/integrations_controller.rb:19 (#create)
  • Factory: IntegrationFactoryIntegration 모델의 set_parameters (concern) 호출
  • Provider dispatch: app/concerns/parameter/integration.rb:17-35provider == 'bim360'Bim360Operation
  • Token exchange call: app/concerns/parameter/integration.rb:54
  • Failure point: app/operations/bim360_operation.rb:29-37RestClient::Exception rescue 블록
app/controllers/api/v1/integrations_controller.rb:19-23ruby
def create
  @model = factory_instance.create!(params)

  super
end
app/concerns/parameter/integration.rb:53-71ruby
elsif params[:code].present?
  token = operation_class.get_token(params[:code], params[:region])

  if TOKEN_REFRESH_REQUIRED_PROVIDERS.include?(self.model.provider)
    @model.refresh_token_expired_at = 14.days.since
  else
    @model.refresh_token_expired_at = nil
  end

  if token['access_token'].nil? || token['refresh_token'].nil? || token['token_type'].nil? || token['expires_in'].nil?
    raise Cupix::Errors::BadGateway.new(code: 'BG10001', reason: "OAuth's token format is invalid")
  end

  @model.access_token = token['access_token']
  @model.refresh_token = token['refresh_token']
  @model.token_type = token['token_type']
  @model.expired_at = DateTime.now + token['expires_in'].seconds
  @model.state = operation_class.authenticated_state(@model.integratable_type)
end
app/operations/bim360_operation.rb:16-48ruby
def self.get_token(code, region)
  data = {
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: $OAUTH[:autodesk_forge][:bim360][:redirect_uri]
  }
  header = {
    authorization: "Basic #{$OAUTH[:autodesk_forge][:bim360][:token]}"
  }

  begin
    url = "#{$OAUTH[:autodesk_forge][:site]}#{$OAUTH[:autodesk_forge][:token_url]}"
    response = Cupix::HttpClient.post(url, data, header)
  rescue RestClient::Exception => e
    response = JSON.parse(e.response)

    Cupix::Logger.error("BIM360 Authentication failed: #{response['developerMessage']} - error: #{e.message}", class: self.name, function: __method__)
    raise Cupix::Errors::Parameter.new(
      code: 'ARG10000',
      reason: "BIM360 Authentication failed: #{response['developerMessage']}",
      message: e.message
    )
  rescue StandardError => e
    Cupix::Logger.error("BIM360 Authentication failed: #{e.message}", class: self.name, function: __method__)
    raise Cupix::Errors::BadGateway.new(
      code: 'BG10001',
      reason: "BIM360 Authentication failed: #{e.message}",
      message: e.message
    )
  end

  JSON.parse(response)
end

기대 동작: Autodesk Forge token endpoint 가 400 을 반환하면 응답 본문의 표준 OAuth2 필드(error, error_description) 를 로그/에러 reason 에 남겨 원인 파악이 가능해야 한다.

실제 동작: 코드는 response['developerMessage'] 만 참조한다. Autodesk Forge OAuth2 v2 token endpoint 는 RFC 6749 준수 응답을 반환하고 developerMessage 필드는 Autodesk 의 다른 REST API (예: Data Management) 에서만 사용된다. 결과적으로 response['developerMessage']nil 이 되어 로그 메시지가 BIM360 Authentication failed: - error: 400 Bad Request 로 두 콜론 사이가 비어버린다. 400 의 실제 원인 코드가 관측에서 소실된다.

같은 파일의 sibling operation 인 procore_operation.rb:40 은 표준 OAuth2 필드를 사용한다:

app/operations/procore_operation.rb:28-46ruby
rescue StandardError => e
  begin
    response = JSON.parse(e.response)
  rescue StandardError
    Cupix::Logger.error("Procore get_token failed with non-JSON response: #{e.message}", class: self.name, function: __method__)
    raise Cupix::Errors::BadGateway.new(
      code: 'BG10001',
      reason: "Procore Authentication failed: #{e.message}",
      message: e.message
    )
  end

  Cupix::Logger.error("Procore Authentication failed: #{response['error']}, message: #{response['error_description']} - error: #{e.message}", class: self.name, function: __method__)
  raise Cupix::Errors::Parameter.new(
    code: 'ARG10000',
    reason: "Procore Authentication failed: #{response['error']}",
    message: response['error_description']
  )
end

Procore 는 error / error_description 을 로깅해 400 원인을 남긴다. BIM360 도 같은 필드를 남기도록 하면 이번 같은 산발적 400 의 근본 원인을 데이터로 확인할 수 있다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "BIM360 Authentication failed"
text
service:cupixworks-api @request_id:590e8486-c39f-4e8e-84cb-51f6bba0c583

Trace 재구성 (request_id 로 묶은 2개 로그):

json
{
  "timestamp": "2026-07-29 02:25:00 KST",
  "status": "error",
  "message": "BIM360 Authentication failed:  - error: 400 Bad Request",
  "class": "Bim360Operation",
  "function": "get_token"
}
json
{
  "timestamp": "2026-07-29 02:25:00 KST",
  "status": "info",
  "message": "[400] POST /api/v1/facilities/3dfas/integrations (Api::V1::IntegrationsController#create)",
  "error": {
    "reason": "BIM360 Authentication failed: ",
    "code": "ARG10000",
    "message": "400 Bad Request",
    "class": "Cupix::Errors::Parameter"
  }
}

error.reasonBIM360 Authentication failed: 로 끝나 developerMessage 가 empty string (nil.to_s) 임을 확인 — code path 분석과 일치.

동일 세션의 즉시 복구 증거:

text
2026-07-29 02:25:02  [200] POST /api/v1/facilities/3dfas/integrations/bim360/access_token
2026-07-29 02:25:10  [200] PUT  /api/v1/facilities/3dfas/integrations/bim360
2026-07-29 02:25:32  [200] POST /api/v1/facilities/3dfas/integrations/bim360/access_token

지난 14일간 동일 fingerprint 발생 이력:

text
2026-07-16 17:49:55  error
2026-07-16 02:44:57  error  (Integration 4626 refresh_token flow)
2026-07-21 14:13:46  error
2026-07-21 14:14:12  error
2026-07-21 15:50:11  error
2026-07-23 10:34:22  error
2026-07-24 14:55:59  error
2026-07-28 17:25:00  error  (this cluster)

빈도 8건/14일 = 저빈도, 재시도로 회복되는 산발성 패턴.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Autodesk 측 authorization code 만료·재사용 (사용자 double-submit, 페이지 재로드, 링크 클릭 지연). 400 은 표준 OAuth2 invalid_grant 대응이 흔함. 동일 사용자가 37초 후 재시도로 성공 ([200] .../access_token at 02:25:02 KST). 지난 14일 8건 모두 산발/single-hit 패턴으로 spike 없음. Autodesk 설정 이슈면 지속 실패해야 함. 응답 본문의 error 필드가 로그에 남지 않아 결정적 증거 없음 — uncertain, needs verification. Likely (uncertain — needs verification)
H2 Autodesk Forge 측 일시적 장애 / 5xx. 상태코드가 400 (client error) 이며, RestClient::Exception rescue 블록에서 처리됨. 5xx 였다면 timeout/BadGateway 로 다른 exception 경로. Rejected
H3 잘못된 redirect_uri 또는 클라이언트 자격증명 ($OAUTH[:autodesk_forge][:bim360][:token]). 400 Bad Request 는 자격증명 오류의 표준 응답 중 하나. 같은 config 로 동일 시각 앞뒤에 refresh_token (integration 7410, 15, 7451) 및 access_token 요청이 모두 200 성공. 자격증명 문제라면 전면 실패해야 함. Rejected
H4 로그 메시지의 double-space 자체가 별개의 bug — Autodesk 응답 필드 이름 오사용 (developerMessage 는 Autodesk Data Management API 용, OAuth2 token endpoint 는 error/error_description 사용). procore_operation.rb:40 은 표준 OAuth2 필드를 로깅. BIM360 로그의 error.reason: "BIM360 Authentication failed: " 는 필드가 nil 임을 시사. Autodesk Forge OAuth2 문서(https://forge.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST/) 는 표준 OAuth2 error 스키마 명시. Confirmed (별개 관측성 결함)

Fix Recommendation#

즉시 조치 (Critical)#

  • app/operations/bim360_operation.rb:32,35response['developerMessage'] 를 표준 OAuth2 필드로 교체해 400 원인이 로그와 응답에 남도록 한다. Procore 구현(procore_operation.rb:40-45) 을 참조 패턴으로 삼는다. 접근:
    • 로그 메시지에 response['error'] (예: invalid_grant, invalid_client, invalid_request) 와 response['error_description'] 을 함께 남긴다.
    • Cupix::Errors::Parameter#reason 에도 response['error'] 를 반영해 클라이언트가 400 원인을 인지할 수 있게 한다.
    • 같은 파일의 refresh_token (bim360_operation.rb:66-70) 도 동일 필드 이름을 참조하므로 함께 수정.

단기 개선 (1주 이내)#

  • Procore 처럼 JSON.parse(e.response) 자체가 실패하는 경우를 대비한 fallback rescue 추가 (procore_operation.rb:29-38 패턴). 현재 코드는 Autodesk 가 비-JSON 400 을 반환하면 JSON::ParserError 로 재폭발한다.
  • authorization_code 재사용/만료 여부를 명시 로깅하기 위해, request context (facility_key, provider, user_id, code prefix 앞 8자 정도) 를 error log 에 함께 남긴다. code 전체를 남기지 않도록 주의.
  • 400 응답 시 warn 또는 info 레벨 이벤트로 원인 분포(error 필드 별 count) 를 집계할 수 있는 tag 를 추가.

장기 개선 (재발 방지)#

  • 3rd-party OAuth error mapping 을 shared helper 로 추출 (bim360, procore, plangrid, revizto 가 유사한 rescue 패턴 반복). helper 는 provider 별 error field 이름을 알고 표준화된 로그 포맷을 생성.
  • 클라이언트 (프런트엔드) 에 authorization code single-submit 방어 로직이 있는지 확인 (double click, back button, network retry). RCA 관점에서만 언급 — 자동 fix 범위 외.

Monitoring#

  • Datadog 대시보드 timeseries: BIM360 400 발생률.
text
service:cupixworks-api status:error @class:Bim360Operation @function:get_token
  • 표준 OAuth2 필드 반영 후 원인 코드별 breakdown 이 가능해지면 (fix 적용 후 사용):
text
service:cupixworks-api status:error @class:Bim360Operation "invalid_grant"
  • Integration 생성 성공률 tracking:
text
service:cupixworks-api "POST /api/v1/facilities" "integrations" -status:error
  • Alert threshold 후보: 10분 rolling window 에서 @class:Bim360Operation @function:get_token error 가 5건 초과 시 알림 (지속 실패 = config drift 신호).

Risk Assessment#

  • Risk level: low — 사용자 영향은 개별 요청 단발성 실패이며 즉시 재시도로 회복. 단, 관측성 결함으로 인해 반복 원인 파악이 어렵다.
  • 예상 복잡도: trivial — 로그/reason 필드 이름 2곳 교체 + procore 스타일 fallback rescue 추가. spec 는 spec/operations/ 관련 파일에 mock 응답 필드 이름만 조정.