we-promise/sure · error · AuthenticationError
access_forbidden
access_forbidden
Error message
Access forbidden - check your permissions
What it means
The API answered HTTP 403: the credential is valid (it passed auth) but is not entitled to the resource. Mapped to AuthenticationError(:access_forbidden), though it is an authorization problem, not an authentication one - the token works, the target does not.
Source
Thrown at app/models/provider/indexa_capital.rb:206
jwt
end
def handle_response(response)
case response.code
when 200, 201
begin
JSON.parse(response.body, symbolize_names: true)
rescue JSON::ParserError => e
raise Error.new("Invalid JSON in response: #{e.message}", :bad_response)
end
when 400
Rails.logger.error "IndexaCapital API: Bad request - #{response.body}"
raise Error.new("Bad request: #{response.body}", :bad_request)
when 401
raise AuthenticationError.new("Invalid credentials", :unauthorized)
when 403
raise AuthenticationError.new("Access forbidden - check your permissions", :access_forbidden)
when 404
raise Error.new("Resource not found", :not_found)
when 429
raise Error.new("Rate limit exceeded. Please try again later.", :rate_limited)
when 500..599
raise Error.new("IndexaCapital server error (#{response.code}). Please try again later.", :server_error)
else
Rails.logger.error "IndexaCapital API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
raise Error.new("Unexpected error: #{response.code} - #{response.body}", :unknown)
end
end
# Extract accounts array from /users/me response
# API returns: { accounts: [{ account_number: "ABC12345", type: "mutual", status: "active", ... }] }
def extract_accounts(user_data)
accounts = user_data[:accounts] || []
accounts.map do |acct|
{View on GitHub (pinned to e69894adb9)
Solutions
- Only ever call account endpoints with account_number values freshly returned by list_accounts for the same credential
- Drop or disable the offending account from the sync set and re-list to confirm it disappeared
- Check the Indexa dashboard for the token's permitted accounts/scopes
- If every call 403s with a token that used to work, the token's permissions changed - regenerate it
Example fix
# before
provider.get_portfolio(account_number: params[:account_number]) # user-supplied
# after
owned = provider.list_accounts.map { |a| a[:account_number] }
raise ArgumentError, "account not owned by this credential" unless owned.include?(params[:account_number])
provider.get_portfolio(account_number: params[:account_number]) Defensive patterns
Strategy: validation
Validate before calling
owned = provider.list_accounts.map { |a| a[:account_number] }.to_set
raise ArgumentError, "account not owned by this credential" unless owned.include?(account_number)
provider.get_portfolio(account_number: account_number) Type guard
def indexa_forbidden?(error) error.is_a?(Provider::IndexaCapital::AuthenticationError) && error.error_type == :access_forbidden end
Try / catch
begin provider.get_portfolio(account_number: num) rescue Provider::IndexaCapital::AuthenticationError => e raise unless e.error_type == :access_forbidden account.update!(sync_disabled: true, disable_reason: "not_authorized_by_provider") end
Prevention
- Always cross-check the account number against a fresh list_accounts for the same credential
- Never accept account numbers from user input into provider calls
- Remember :access_forbidden means the token WORKS - don't touch credentials when you see it
- When an ownership change happens at Indexa, expect this on the affected account only
When it happens
Trigger: Querying /accounts/{n}/... with an account_number that belongs to a different Indexa user; a token whose plan/permissions exclude the endpoint; regional or account-state restrictions (e.g. closed or transferred pension plan).
Common situations: Typo'd or transposed account numbers that are well-formed but foreign, tokens scoped to a subset of accounts, account ownership changed (divorce/inheritance transfers) while the number stayed in your sync list.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/1b8092979528fdfa.
Report an issue: GitHub.