fix(enable-banking): only pin hidden, psu-applicable decoupled auth methods (Lunar dead end) (#1529)
* fix(enable-banking): only pin hidden, psu-applicable decoupled auth methods PR #854 pinned the first DECOUPLED auth method for every bank to fix Handelsbanken corporate consents (its Mobile BankID is a hidden_method that Enable Banking only uses when requested explicitly). The blanket pin also hit banks whose decoupled method is visible and whose default flow already worked: Lunar users were asked for personnummer on the hosted page, told to approve in the app, and no approval ever arrived. Now a method is pinned only when pinning is necessary (hidden_method is true, so the method is unreachable by default) and applicable (psu_types missing/empty or containing the consent's psu_type). Otherwise undefined is returned and the ASPSP default runs, matching the stated intent of #854. The connect log now records the chosen method's approach, hidden_method and psu_types so per-bank behavior can be verified in prod after deploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(enable-banking): bind pinned auth method to the outgoing /auth request Review follow-up on the auth-method pinning branch, two findings: 1. No test bound the selection result to the outgoing request: mutating the connect route to stop forwarding the pinned method's name into startAuthorization (the exact Handelsbanken-corporate regression this code exists to prevent) passed all 157 tests. Add route-level wiring tests asserting startAuthorization receives 'BANKID' in the auth_method argument position (index 5) on both the fresh-connect and reconnect call sites, plus the inverse: unpinned resolves to an undefined auth_method. 2. The auth_method_psu_types log field printed '(aspsp default)' when a method WAS pinned but carried no psu_types (the documented real Handelsbanken shape), contradicting auth_method='BANKID' on the same line. A pinned method without psu_types now logs '(all)'; '(aspsp default)' is reserved for the unpinned case. Both fixes are mutation-verified: reverting either makes the new tests fail (wiring mutation fails 2 tests, sentinel revert fails 1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
2fb3667e8d
commit
3a7688c163
@@ -374,28 +374,53 @@ export async function getASPSPs(country: string = 'SE', psuType?: 'personal' | '
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the auth_method we should request for a given bank, or undefined to
|
||||
* let Enable Banking use the ASPSP's visible default.
|
||||
* Pick the DECOUPLED auth method worth pinning explicitly, or undefined to let
|
||||
* Enable Banking run the ASPSP's default flow.
|
||||
*
|
||||
* Why: several Swedish ASPSPs (notably Handelsbanken) expose Mobile BankID only
|
||||
* as a DECOUPLED method flagged hidden_method=true. When we send no auth_method,
|
||||
* Enable Banking falls back to the visible REDIRECT method, which for
|
||||
* Handelsbanken *corporate* PSUs does not support Mobile BankID, so the consent
|
||||
* fails right after the user approves in the BankID app ("fel efter BankID").
|
||||
* Pinning the decoupled (Mobile BankID) method makes the flow work for both
|
||||
* business and personal PSUs. We return undefined when the bank exposes no
|
||||
* decoupled method or the lookup fails, so banks that already work are untouched.
|
||||
* A method is only pinned when pinning is both NECESSARY and APPLICABLE:
|
||||
*
|
||||
* - hidden_method === true: a hidden method is never used unless requested
|
||||
* explicitly via auth_method, so pinning is the only way to reach it. That
|
||||
* is the Handelsbanken case: its Mobile BankID (DECOUPLED) is hidden, and
|
||||
* without pinning it corporate PSUs fail right after approving in the
|
||||
* BankID app ("fel efter BankID"). A VISIBLE decoupled method is already
|
||||
* part of the bank's own default flow; force-pinning it overrides a working
|
||||
* default. That regression (from PR #854) broke Lunar-class banks: the user
|
||||
* typed their personnummer on Enable Banking's page, was told to approve in
|
||||
* the bank's app, and no approval request ever arrived.
|
||||
* - psu_types, when present and non-empty, must include the PSU type we are
|
||||
* authorizing as: a method scoped to 'personal' must never be pinned for a
|
||||
* 'business' consent (and vice versa). A missing or empty psu_types means
|
||||
* the method applies to all PSU types.
|
||||
*/
|
||||
export async function getPreferredAuthMethod(
|
||||
export function selectPreferredAuthMethod(
|
||||
authMethods: AuthMethod[] | undefined,
|
||||
psuType: 'personal' | 'business'
|
||||
): AuthMethod | undefined {
|
||||
return authMethods?.find(
|
||||
(m) =>
|
||||
m.approach === 'DECOUPLED' &&
|
||||
m.hidden_method === true &&
|
||||
(!m.psu_types || m.psu_types.length === 0 || m.psu_types.includes(psuType))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the auth method to pin for a given bank, with full metadata so the
|
||||
* caller can log approach/hidden_method/psu_types, or undefined to let Enable
|
||||
* Banking use the ASPSP's default flow. Selection rules live in
|
||||
* selectPreferredAuthMethod. Returns undefined on lookup failure so banks
|
||||
* that already work are untouched.
|
||||
*/
|
||||
export async function getPreferredAuthMethodDetails(
|
||||
aspspName: string,
|
||||
country: string,
|
||||
psuType: 'personal' | 'business'
|
||||
): Promise<string | undefined> {
|
||||
): Promise<AuthMethod | undefined> {
|
||||
try {
|
||||
const aspsps = await getASPSPs(country, psuType)
|
||||
const aspsp = aspsps.find((a) => a.name === aspspName)
|
||||
const decoupled = aspsp?.auth_methods?.find((m) => m.approach === 'DECOUPLED')
|
||||
return decoupled?.name
|
||||
return selectPreferredAuthMethod(aspsp?.auth_methods, psuType)
|
||||
} catch (error) {
|
||||
console.error('[enable-banking] getPreferredAuthMethod failed; using ASPSP default', {
|
||||
aspspName,
|
||||
@@ -407,6 +432,20 @@ export async function getPreferredAuthMethod(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name-only convenience wrapper around getPreferredAuthMethodDetails: the
|
||||
* value to send as auth_method on POST /auth, or undefined for the ASPSP
|
||||
* default.
|
||||
*/
|
||||
export async function getPreferredAuthMethod(
|
||||
aspspName: string,
|
||||
country: string,
|
||||
psuType: 'personal' | 'business'
|
||||
): Promise<string | undefined> {
|
||||
const method = await getPreferredAuthMethodDetails(aspspName, country, psuType)
|
||||
return method?.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of supported banks (legacy format for backward compatibility)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user