Skip to content

Commit 8991a09

Browse files
authored
Merge pull request #4 from hongwei1/main
UK Open Banking consent authorisation with SCA, plus OAuth/session security hardening
2 parents 9a31f9f + 042ed41 commit 8991a09

52 files changed

Lines changed: 2200 additions & 1871 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/unit_tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: unit tests
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
12+
- uses: actions/setup-node@v4
13+
with:
14+
node-version: 22
15+
cache: npm
16+
17+
- name: Install dependencies
18+
run: npm ci
19+
20+
- name: Sync SvelteKit
21+
run: npm run prepare --workspaces --if-present
22+
23+
- name: Run unit tests
24+
run: npm run test:unit --workspaces --if-present -- --run

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ build
99
.claude
1010
security-scan.log
1111
security-fix.log
12+
.idea
13+
*.code-workspace

apps/api-manager/src/lib/components/Toast.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { toaster } from '$lib/utils/toastService';
44
</script>
55

6-
<Toast.Group {toaster}>
6+
<Toast.Group {toaster} placement="top-end" class="fixed top-4 right-4 z-[9999] flex flex-col gap-2">
77
{#snippet children(toast)}
88
<Toast {toast}>
99
<Toast.Message>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { OAuth2Client } from 'arctic';
3+
import { OAuth2ClientWithConfig } from './client';
4+
5+
// Build a JWT-shaped token (header.payload.signature) with a base64 payload.
6+
function createMockJWT(payload: Record<string, unknown>): string {
7+
const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
8+
const body = btoa(JSON.stringify(payload));
9+
return `${header}.${body}.mock-signature`;
10+
}
11+
12+
describe('OAuth2ClientWithConfig', () => {
13+
let client: OAuth2ClientWithConfig;
14+
15+
beforeEach(() => {
16+
// arctic's OAuth2Client is globally mocked to return a plain object, which
17+
// would hijack `this` in the subclass constructor and strip its prototype
18+
// methods. Reset it to a no-op constructor so the real subclass instance is
19+
// built.
20+
vi.mocked(OAuth2Client).mockImplementation(function () {} as never);
21+
client = new OAuth2ClientWithConfig('id', 'secret', 'http://localhost/callback');
22+
});
23+
24+
describe('constructor', () => {
25+
it('creates an instance exposing the custom methods', () => {
26+
expect(client.OIDCConfig).toBeUndefined();
27+
expect(typeof client.checkAccessTokenExpiration).toBe('function');
28+
});
29+
});
30+
31+
describe('checkAccessTokenExpiration (fail closed)', () => {
32+
it('returns false for a token whose expiry is in the future', async () => {
33+
const token = createMockJWT({ exp: Math.floor(Date.now() / 1000) + 3600 });
34+
await expect(client.checkAccessTokenExpiration(token)).resolves.toBe(false);
35+
});
36+
37+
it('returns true for an expired token', async () => {
38+
const token = createMockJWT({ exp: Math.floor(Date.now() / 1000) - 3600 });
39+
await expect(client.checkAccessTokenExpiration(token)).resolves.toBe(true);
40+
});
41+
42+
it('treats a token without an exp claim as expired', async () => {
43+
const token = createMockJWT({ sub: 'user-1' });
44+
await expect(client.checkAccessTokenExpiration(token)).resolves.toBe(true);
45+
});
46+
47+
it('treats an undecodable token as expired instead of throwing', async () => {
48+
await expect(client.checkAccessTokenExpiration('not.a.jwt')).resolves.toBe(true);
49+
});
50+
51+
it('treats a malformed JWT payload as expired instead of throwing', async () => {
52+
await expect(
53+
client.checkAccessTokenExpiration('eyJhbGciOiJSUzI1NiJ9.invalid-payload.sig')
54+
).resolves.toBe(true);
55+
});
56+
});
57+
});

apps/api-manager/src/lib/oauth/sessionHelper.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ export class SessionOAuthHelper {
2626
logger.debug(` Access token present: ${!!oauthData?.access_token}`);
2727
logger.debug(` Refresh token present: ${!!oauthData?.refresh_token}`);
2828

29-
if (oauthData?.access_token) {
30-
logger.debug(` Access token length: ${oauthData.access_token.length}`);
31-
logger.debug(
32-
` Access token preview: ${oauthData.access_token.substring(0, 30)}...`,
33-
);
34-
}
35-
3629
if (!oauthData?.provider || !oauthData?.access_token) {
3730
logger.warn("🔐 SESSION OAUTH - Missing provider or access token");
3831
return null;

apps/api-manager/src/routes/(protected)/account-access/account-directory/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
)}
165165
<tr>
166166
<td class="cell-mono cell-id">
167-
<a href="/account-access/accounts/{encodeURIComponent(account.bank_id)}/{encodeURIComponent(account.account_id)}/owner" class="account-link">
167+
<a href="/account-access/accounts/{encodeURIComponent(account.bank_id)}/{encodeURIComponent(account.account_id)}/public" class="account-link">
168168
{account.account_id}
169169
</a>
170170
</td>

apps/api-manager/src/routes/(protected)/account-access/accounts/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
const res = await trackedFetch(`/proxy/obp/v6.0.0/banks/${encodeURIComponent(bankId)}/accounts`);
2020
if (!res.ok) {
2121
const data = await res.json().catch(() => ({}));
22-
throw new Error(data.message ?? `HTTP ${res.status}`);
22+
throw new Error(data.message || data.error || `HTTP ${res.status} Failed to fetch accounts`);
2323
}
2424
const data = await res.json();
2525
accounts = data.accounts || [];

apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/+page.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@
146146
);
147147
if (!res.ok) {
148148
const data = await res.json().catch(() => ({}));
149-
throw new Error(data.message ?? `HTTP ${res.status}`);
149+
const msg = data.message || data.error || `HTTP ${res.status}`;
150+
throw new Error(msg);
150151
}
151152
account = await res.json();
152153
} catch (err) {
@@ -171,8 +172,7 @@
171172
);
172173
if (!res.ok) {
173174
const data = await res.json().catch(() => ({}));
174-
throw new Error(data.message ?? `HTTP ${res.status}`);
175-
}
175+
throw new Error(data.message || data.error || `Failed to fetch users with access for view ${vid}`); }
176176
const data = await res.json();
177177
return { viewId: vid, users: data.users || [] };
178178
})
@@ -391,7 +391,7 @@
391391
);
392392
if (!res.ok) {
393393
const data = await res.json().catch(() => ({}));
394-
throw new Error(data.message ?? `HTTP ${res.status}`);
394+
throw new Error(data.message || data.error || `HTTP ${res.status} Failed to fetch customer account links`);
395395
}
396396
const data = await res.json();
397397
customerAccountLinks = data.links || [];
@@ -434,7 +434,7 @@
434434
435435
if (!res.ok) {
436436
const data = await res.json().catch(() => ({}));
437-
throw new Error(data.message ?? `HTTP ${res.status}`);
437+
throw new Error(data.message || data.error || `HTTP ${res.status} Failed to create account attribute`);
438438
}
439439
440440
const created = await res.json();

apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/counterparties/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
);
3232
if (!res.ok) {
3333
const data = await res.json().catch(() => ({}));
34-
throw new Error(data.message ?? `HTTP ${res.status}`);
34+
throw new Error(data.message || data.error || `HTTP ${res.status} Failed to fetch counterparties`);
3535
}
3636
const data = await res.json();
3737
counterparties = data.counterparties || [];

apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
);
8080
if (!res.ok) {
8181
const data = await res.json().catch(() => ({}));
82-
throw new Error(data.message ?? `HTTP ${res.status}`);
82+
throw new Error(data.message || data.error || `HTTP ${res.status} Failed to fetch counterparty details`);
8383
}
8484
counterparty = await res.json();
8585
} catch (err) {

0 commit comments

Comments
 (0)