feat(enable_banking): support MFA/decoupled banks and harden session handling (#2174)

Decoupled/MFA banks (e.g. VR Bank in Holstein) were hard-blocked because the
authorize flow aborted whenever auth_methods[0] was DECOUPLED. Enable Banking's
hosted /auth page actually coordinates decoupled SCA and redirects back with a
code, so route these banks through it instead:

- Provider#start_authorization accepts and forwards an auth_method param
- EnableBankingItem#select_auth_method picks the best method
  (REDIRECT > DECOUPLED > EMBEDDED), filtering by psu_type and skipping hidden
  methods
- Shared begin_authorization! re-fetches ASPSP metadata on each authorize and
  reauthorize, so the method is always re-derived (no persistence required)
- Remove the DECOUPLED block in the controller

Also stop the integration from constantly reporting "session expired":

- Only a session-level GET /sessions 401/404 flips the connection to
  requires_update; per-account 401/404 are retried and no longer kill the
  whole connection
- Reconcile session_expires_at from the API's access.valid_until on every sync
- Treat an expired session as a graceful requires_update state instead of
  raising a bare error

No schema changes. Adds covering tests.
This commit is contained in:
Tobias Rahloff
2026-06-04 19:53:52 +02:00
committed by GitHub
parent 6a89efb9c9
commit fe47c918bb
16 changed files with 393 additions and 69 deletions

View File

@@ -131,39 +131,6 @@ class EnableBankingItemsController < ApplicationController
return
end
# Re-fetch ASPSP list from provider to avoid session cookie overflow.
# We do not store full ASPSP metadata in the session to stay within the 4KB limit;
# instead, we re-query the provider here for the final authorization parameters.
aspsp_data = nil
begin
provider_for_lookup = @enable_banking_item.enable_banking_provider
if provider_for_lookup
response = provider_for_lookup.get_aspsps(country: @enable_banking_item.country_code)
raw_aspsps = response[:aspsps] || response["aspsps"] || []
found = raw_aspsps.find { |a| a[:name] == aspsp_name || a["name"] == aspsp_name }
aspsp_data = found&.with_indifferent_access
end
rescue Provider::EnableBanking::EnableBankingError => e
Rails.logger.warn "Enable Banking: could not fetch ASPSP metadata in authorize: #{e.message}"
end
# Block DECOUPLED banks — our OAuth redirect flow doesn't support them
if aspsp_data.present?
# Adjust psu_type if the bank does not support the requested type
supported_types = Array(aspsp_data[:psu_types]).map(&:to_s)
if supported_types.any? && !supported_types.include?(psu_type)
psu_type = supported_types.first
end
first_method = Array(aspsp_data[:auth_methods]).first
approach = first_method&.dig(:approach) || first_method&.dig("approach")
if approach == "DECOUPLED"
redirect_to settings_providers_path, alert: t(".decoupled_not_supported",
default: "This bank uses a separate device authentication method which is not yet supported. Please add this account manually.")
return
end
end
begin
target_item = if params[:new_connection] == "true"
Current.family.enable_banking_items.create!(
@@ -181,12 +148,14 @@ class EnableBankingItemsController < ApplicationController
language = I18n.locale.to_s.split("-").first
redirect_url = target_item.start_authorization(
# begin_authorization! re-fetches ASPSP metadata and auto-selects the best
# auth method (REDIRECT > DECOUPLED > EMBEDDED). Decoupled/MFA banks proceed
# through Enable Banking's hosted SCA page rather than being blocked.
redirect_url = target_item.begin_authorization!(
aspsp_name: aspsp_name,
redirect_url: enable_banking_callback_url,
state: target_item.id,
psu_type: psu_type,
aspsp_data: aspsp_data,
language: language
)
@@ -269,11 +238,11 @@ class EnableBankingItemsController < ApplicationController
begin
language = I18n.locale.to_s.split("-").first
redirect_url = @enable_banking_item.start_authorization(
aspsp_name: @enable_banking_item.aspsp_name,
# Route through the shared path so reauthorization re-selects the same auth
# method (decoupled banks included) instead of falling back to a default.
redirect_url = @enable_banking_item.begin_authorization!(
redirect_url: enable_banking_callback_url,
state: @enable_banking_item.id,
psu_type: @enable_banking_item.psu_type || "personal",
language: language
)