{"record":{"id":"d634b7fe462bba7d","repo":"we-promise/sure","slug":"unauthorized-d634b7","errorCode":"unauthorized","errorMessage":"Invalid credentials or expired JWT","messagePattern":"Invalid credentials or expired JWT","errorType":"exception","errorClass":"EnableBankingError","httpStatus":401,"severity":"error","filePath":"app/models/provider/enable_banking.rb","lineNumber":287,"sourceCode":"\n    def auth_headers\n      {\n        \"Authorization\" => \"Bearer #{generate_jwt}\",\n        \"Accept\" => \"application/json\"\n      }\n    end\n\n    def handle_response(response)\n      case response.code\n      when 200, 201\n        parse_response_body(response)\n      when 204\n        {}\n      when 400\n        response_data = parse_error_response_body(response)\n        raise EnableBankingError.new(\"Bad request to Enable Banking API: #{response.body}\", :bad_request, response_data: response_data)\n      when 401\n        raise EnableBankingError.new(\"Invalid credentials or expired JWT\", :unauthorized)\n      when 403\n        raise EnableBankingError.new(\"Access forbidden - check your application permissions\", :access_forbidden)\n      when 404\n        raise EnableBankingError.new(\"Resource not found\", :not_found)\n      when 408\n        raise EnableBankingError.new(\"Request timeout from Enable Banking API\", :timeout)\n      when 422\n        response_data = parse_response_body(response)\n        raise EnableBankingError.new(\"Validation error from Enable Banking API: #{response.body}\", :validation_error, response_data: response_data)\n      when 429\n        raise EnableBankingError.new(\"Rate limit exceeded. Please try again later.\", :rate_limited)\n      else\n        response_data = parse_error_response_body(response)\n        raise EnableBankingError.new(\"Failed to fetch data: #{response.code} #{response.message} - #{response.body}\", :fetch_failed, response_data: response_data)\n      end\n    end\n\n    def parse_error_response_body(response)","sourceCodeStart":269,"sourceCodeEnd":305,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/enable_banking.rb#L269-L305","documentation":"Raised by Provider::EnableBanking#handle_response on HTTP 401: the RS256 JWT the client generates (kid = application_id, signed with the configured private key, exp = now + 3600) was rejected as invalid or expired. Because a fresh JWT is signed per request, 'expired' in practice means server-visible clock skew or a mismatched application_id/private_key pair, not an old token being reused.","triggerScenarios":"application_id from one Enable Banking application paired with another application's private key; the private key was rotated in the portal and the stored copy revoked; system clock on the app server drifting minutes ahead so exp appears in the past (or iat in the future) to Enable Banking.","commonSituations":"Key rotation without updating the stored secret, copying credentials between staging/production applications, VMs without NTP drifting clock, typos in the application UUID.","solutions":["Confirm application_id and private key come from the SAME Enable Banking application (kid must match the key the portal knows)","Re-download the current private key PEM from the portal and update the stored secret, since rotated keys may be revoked immediately","Check clock sync (timedatectl status / ntp) — drift beyond the JWT validity window (1 hour) produces exactly this error","Make a canary get_aspsps(country: 'FI') call at boot to fail fast on credential problems before user-facing flows"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"def enable_banking_credentials_ok?(application_id, key_pem)\n  key = OpenSSL::PKey.read(key_pem)\n  return false unless key.is_a?(OpenSSL::PKey::RSA)\n  # canary: cheapest authenticated call proves kid (application_id) matches the key\n  probe = Provider::EnableBanking.new(application_id: application_id, client_certificate: key_pem)\n  probe.get_aspsps(country: \"FI\") && true\nrescue OpenSSL::PKey::PKeyError, Provider::EnableBanking::EnableBankingError => e\n  e.error_type != :unauthorized\nend","typeGuard":"def eb_unauthorized?(error)\n  error.is_a?(Provider::EnableBanking::EnableBankingError) && error.error_type == :unauthorized\nend","tryCatchPattern":"begin\n  client.get_aspsps(country: \"DE\")\nrescue Provider::EnableBanking::EnableBankingError => e\n  raise unless e.error_type == :unauthorized\n  # JWT is freshly signed per request: this means wrong app_id/key pair or clock skew — fix config, don't retry\n  connection.update!(status: \"credential_error\")\n  raise\nend","preventionTips":["Pair application_id and private key from the same portal application — kid must match the key","Run the get_aspsps canary at boot to fail fast on credential mismatch","Keep NTP enabled on app servers; JWT window is 1 hour and skew beyond it always 401s","After key rotation in the portal, update the stored secret immediately — old keys get revoked"],"tags":["enable-banking","http-401","jwt","rs256","authentication","clock-skew"],"backgroundTag":"jwt-token-expired","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}