we-promise/sure · error · EnableBankingError
unauthorized
unauthorized
Error message
Invalid credentials or expired JWT
What it means
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.
Source
Thrown at app/models/provider/enable_banking.rb:287
def auth_headers
{
"Authorization" => "Bearer #{generate_jwt}",
"Accept" => "application/json"
}
end
def handle_response(response)
case response.code
when 200, 201
parse_response_body(response)
when 204
{}
when 400
response_data = parse_error_response_body(response)
raise EnableBankingError.new("Bad request to Enable Banking API: #{response.body}", :bad_request, response_data: response_data)
when 401
raise EnableBankingError.new("Invalid credentials or expired JWT", :unauthorized)
when 403
raise EnableBankingError.new("Access forbidden - check your application permissions", :access_forbidden)
when 404
raise EnableBankingError.new("Resource not found", :not_found)
when 408
raise EnableBankingError.new("Request timeout from Enable Banking API", :timeout)
when 422
response_data = parse_response_body(response)
raise EnableBankingError.new("Validation error from Enable Banking API: #{response.body}", :validation_error, response_data: response_data)
when 429
raise EnableBankingError.new("Rate limit exceeded. Please try again later.", :rate_limited)
else
response_data = parse_error_response_body(response)
raise EnableBankingError.new("Failed to fetch data: #{response.code} #{response.message} - #{response.body}", :fetch_failed, response_data: response_data)
end
end
def parse_error_response_body(response)View on GitHub (pinned to e69894adb9)
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
Defensive patterns
Strategy: validation
Validate before calling
def enable_banking_credentials_ok?(application_id, key_pem) key = OpenSSL::PKey.read(key_pem) return false unless key.is_a?(OpenSSL::PKey::RSA) # canary: cheapest authenticated call proves kid (application_id) matches the key probe = Provider::EnableBanking.new(application_id: application_id, client_certificate: key_pem) probe.get_aspsps(country: "FI") && true rescue OpenSSL::PKey::PKeyError, Provider::EnableBanking::EnableBankingError => e e.error_type != :unauthorized end
Type guard
def eb_unauthorized?(error) error.is_a?(Provider::EnableBanking::EnableBankingError) && error.error_type == :unauthorized end
Try / catch
begin client.get_aspsps(country: "DE") rescue Provider::EnableBanking::EnableBankingError => e raise unless e.error_type == :unauthorized # JWT is freshly signed per request: this means wrong app_id/key pair or clock skew — fix config, don't retry connection.update!(status: "credential_error") raise end
Prevention
- 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
When it happens
Trigger: 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.
Common situations: Key rotation without updating the stored secret, copying credentials between staging/production applications, VMs without NTP drifting clock, typos in the application UUID.
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/d634b7fe462bba7d.
Report an issue: GitHub.