Skip to content

Commit 053ff86

Browse files
committed
feat(portal): let the PSU pick accounts before authorising a UK consent
The UK consent approval page only showed Confirm/Deny with no account selection, and the SCA page's POST .../authorise call omitted account_ids entirely. OBP-API's authorise endpoint now requires account_ids and rejects any account the PSU doesn't hold, so this call was failing with 400. Fetch the PSU's own accounts at the consent's bank (GET /my/accounts, filtered by bank_id) and let them check which ones the requested permissions apply to. Carry the selection through the SCA redirect as a comma-joined account_ids query param, then a hidden form field, and include it in the final POST .../authorise body.
1 parent a80a60a commit 053ff86

4 files changed

Lines changed: 121 additions & 11 deletions

File tree

apps/portal/src/routes/(protected)/uk-consent-request-sca/+page.server.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,31 @@ export async function load(event: RequestEvent) {
1818
const bankId = event.url.searchParams.get('bank_id');
1919
const challengeId = event.url.searchParams.get('challenge_id');
2020
const requestedOidcReturnUrl = event.url.searchParams.get('oidc_return_url');
21+
// The accounts the PSU selected on /uk-consent-request, carried through as a comma-joined
22+
// query param -- OBP-API's authorise endpoint requires account_ids in the final POST body.
23+
const accountIds = event.url.searchParams.get('account_ids') || '';
2124

2225
if (!consentId || !bankId || !challengeId) {
2326
return {
2427
loadError: 'Missing required parameter (CONSENT_ID, bank_id or challenge_id).',
2528
consentId: consentId || '',
2629
bankId: bankId || '',
2730
challengeId: challengeId || '',
28-
oidcReturnUrl: ''
31+
oidcReturnUrl: '',
32+
accountIds: ''
2933
};
3034
}
3135

3236
const token = event.locals.session.data.oauth?.access_token;
3337
if (!token) {
34-
return { loadError: 'Unauthorized: No access token found in session.', consentId, bankId, challengeId, oidcReturnUrl: '' };
38+
return {
39+
loadError: 'Unauthorized: No access token found in session.',
40+
consentId,
41+
bankId,
42+
challengeId,
43+
oidcReturnUrl: '',
44+
accountIds: ''
45+
};
3546
}
3647

3748
const oidcReturnUrl =
@@ -42,7 +53,7 @@ export async function load(event: RequestEvent) {
4253
logger.warn(`Rejected untrusted oidc_return_url: ${requestedOidcReturnUrl}`);
4354
}
4455

45-
return { consentId, bankId, challengeId, oidcReturnUrl };
56+
return { consentId, bankId, challengeId, oidcReturnUrl, accountIds };
4657
}
4758

4859
export const actions = {
@@ -53,10 +64,17 @@ export const actions = {
5364
const bankId = formData.get('bankId') as string;
5465
const challengeId = formData.get('challengeId') as string;
5566
const oidcReturnUrlRaw = formData.get('oidcReturnUrl') as string;
67+
const accountIds = ((formData.get('accountIds') as string) || '')
68+
.split(',')
69+
.map((id) => id.trim())
70+
.filter(Boolean);
5671

5772
if (!otp) {
5873
return { message: 'Please enter the OTP code.' };
5974
}
75+
if (accountIds.length === 0) {
76+
return { message: 'No accounts were selected for this consent. Please start over.' };
77+
}
6078

6179
const token = locals.session.data.oauth?.access_token;
6280
if (!token) {
@@ -76,7 +94,7 @@ export const actions = {
7694
// the PSU only if the challenge answer is correct; a wrong answer leaves it unauthorised.
7795
await obp_requests.post(
7896
`/obp/v5.1.0/banks/${bankId}/consents/${consentId}/authorise`,
79-
{ challenge_id: challengeId, answer: otp },
97+
{ challenge_id: challengeId, answer: otp, account_ids: accountIds },
8098
token
8199
);
82100
} catch (e) {

apps/portal/src/routes/(protected)/uk-consent-request-sca/+page.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<input type="hidden" name="bankId" value={data.bankId} />
4141
<input type="hidden" name="challengeId" value={data.challengeId} />
4242
<input type="hidden" name="oidcReturnUrl" value={data.oidcReturnUrl} />
43+
<input type="hidden" name="accountIds" value={data.accountIds} />
4344

4445
<button type="submit" class="btn preset-filled-primary-500 w-full">
4546
Verify &amp; Authorise

apps/portal/src/routes/(protected)/uk-consent-request/+page.server.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,33 @@ export async function load(event: RequestEvent) {
3131
const requestedOidcReturnUrl = event.url.searchParams.get('oidc_return_url');
3232

3333
if (!consentId) {
34-
return { loadError: 'Missing required parameter: CONSENT_ID.', consentId: '', bankId: bankId || '', apiStandard, oidcReturnUrl: '' };
34+
return {
35+
loadError: 'Missing required parameter: CONSENT_ID.',
36+
consentId: '',
37+
bankId: bankId || '',
38+
apiStandard,
39+
oidcReturnUrl: ''
40+
};
3541
}
3642
if (!bankId) {
37-
return { loadError: 'Missing required parameter: bank_id.', consentId, bankId: '', apiStandard, oidcReturnUrl: '' };
43+
return {
44+
loadError: 'Missing required parameter: bank_id.',
45+
consentId,
46+
bankId: '',
47+
apiStandard,
48+
oidcReturnUrl: ''
49+
};
3850
}
3951

4052
const token = event.locals.session.data.oauth?.access_token;
4153
if (!token) {
42-
return { loadError: 'Unauthorized: No access token found in session.', consentId, bankId, apiStandard, oidcReturnUrl: '' };
54+
return {
55+
loadError: 'Unauthorized: No access token found in session.',
56+
consentId,
57+
bankId,
58+
apiStandard,
59+
oidcReturnUrl: ''
60+
};
4361
}
4462

4563
// oidc_return_url must point back to a configured OIDC provider host — otherwise it becomes
@@ -69,7 +87,32 @@ export async function load(event: RequestEvent) {
6987
logger.warn('Could not fetch UK consent details:', e);
7088
}
7189

72-
return { consentId, bankId, apiStandard, oidcReturnUrl, status, permissions, expirationDateTime };
90+
// The PSU must pick which of their own accounts this consent's permissions apply to --
91+
// OBP-API only binds/grants access for accounts the authorising user actually holds
92+
// (POST .../authorise requires account_ids, and rejects any account the PSU doesn't hold).
93+
let userAccounts: { accountId: string; label: string }[] = [];
94+
try {
95+
const accountsResponse = await obp_requests.get('/obp/v6.0.0/my/accounts', token);
96+
userAccounts = (accountsResponse.accounts || [])
97+
.filter((account: any) => account.bank_id === bankId)
98+
.map((account: any) => ({
99+
accountId: account.id,
100+
label: account.label || account.id
101+
}));
102+
} catch (e) {
103+
logger.warn('Could not fetch user accounts:', e);
104+
}
105+
106+
return {
107+
consentId,
108+
bankId,
109+
apiStandard,
110+
oidcReturnUrl,
111+
status,
112+
permissions,
113+
expirationDateTime,
114+
userAccounts
115+
};
73116
}
74117

75118
export const actions = {
@@ -78,6 +121,7 @@ export const actions = {
78121
const consentId = formData.get('consentId') as string;
79122
const bankId = formData.get('bankId') as string;
80123
const oidcReturnUrlRaw = formData.get('oidcReturnUrl') as string;
124+
const selectedAccountIds = formData.getAll('selectedAccountIds') as string[];
81125

82126
const token = locals.session.data.oauth?.access_token;
83127
if (!token) {
@@ -94,6 +138,10 @@ export const actions = {
94138
return { message: 'No valid return URL was provided to complete the flow.' };
95139
}
96140

141+
if (selectedAccountIds.length === 0) {
142+
return { message: 'Please select at least one account to grant access to.' };
143+
}
144+
97145
let challengeId = '';
98146
try {
99147
// Start SCA: OBP-API issues a one-time challenge (OTP) to the PSU. The consent is only
@@ -117,7 +165,8 @@ export const actions = {
117165
CONSENT_ID: consentId,
118166
bank_id: bankId,
119167
challenge_id: challengeId,
120-
oidc_return_url: oidcReturnUrl
168+
oidc_return_url: oidcReturnUrl,
169+
account_ids: selectedAccountIds.join(',')
121170
});
122171
redirect(303, `/uk-consent-request-sca?${params.toString()}`);
123172
},

apps/portal/src/routes/(protected)/uk-consent-request/+page.svelte

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,55 @@
7474
{/if}
7575
</div>
7676

77+
<!-- Account Selection -->
78+
<div class="mb-6 rounded-lg bg-white p-6 shadow-md dark:bg-gray-800">
79+
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
80+
Select Accounts
81+
</h2>
82+
{#if data.userAccounts?.length}
83+
<p class="mb-4 text-sm text-gray-700 dark:text-gray-300">
84+
Choose which of your accounts the requested permissions apply to:
85+
</p>
86+
<div class="space-y-3" data-testid="uk-consent-account-list">
87+
{#each data.userAccounts as account}
88+
<label
89+
class="flex cursor-pointer items-center gap-3 rounded-md border p-3 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
90+
>
91+
<input
92+
type="checkbox"
93+
name="selectedAccountIds"
94+
form="confirmForm"
95+
value={account.accountId}
96+
class="checkbox"
97+
data-testid="uk-consent-account-checkbox"
98+
/>
99+
<div>
100+
<span class="font-medium text-gray-900 dark:text-gray-100">{account.label}</span>
101+
<span class="ml-2 font-mono text-xs text-gray-500 dark:text-gray-400"
102+
>{account.accountId}</span
103+
>
104+
</div>
105+
</label>
106+
{/each}
107+
</div>
108+
{:else}
109+
<p class="text-error-500 text-sm" data-testid="uk-consent-no-accounts">
110+
You have no accounts at this bank, so this consent cannot be authorised.
111+
</p>
112+
{/if}
113+
</div>
114+
77115
<!-- Actions -->
78116
<div class="flex gap-4">
79-
<form method="post" action="?/confirm" class="flex-1">
117+
<form method="post" action="?/confirm" id="confirmForm" class="flex-1">
80118
<input type="hidden" name="consentId" value={data.consentId} />
81119
<input type="hidden" name="bankId" value={data.bankId} />
82120
<input type="hidden" name="oidcReturnUrl" value={data.oidcReturnUrl} />
83-
<button type="submit" class="btn preset-filled-primary-500 w-full">
121+
<button
122+
type="submit"
123+
class="btn preset-filled-primary-500 w-full"
124+
disabled={!data.userAccounts?.length}
125+
>
84126
Confirm Consent
85127
</button>
86128
</form>

0 commit comments

Comments
 (0)