From cffba39844fa6704a69dde959e06dab15a141d70 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 22 Jul 2025 21:10:41 +0000 Subject: [PATCH] feat(oauth): add OAuth token support --- changelog.md | 9 +++++++ package.json | 3 ++- readme.md | 21 +++++++++++++++++ test/test.oauth.ts | 44 +++++++++++++++++++++++++++++++++++ ts/bunq.classes.account.ts | 4 +++- ts/bunq.classes.apicontext.ts | 15 ++++++++++++ ts/bunq.classes.session.ts | 23 ++++++++++++++++++ 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 test/test.oauth.ts diff --git a/changelog.md b/changelog.md index d25a794..5588c0b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-07-22 - 3.0.5 - feat(oauth) +Add OAuth token support + +- Added support for OAuth access tokens with isOAuthToken flag +- OAuth tokens skip session creation since they already have an associated session +- Fixed "Authentication token already has a user session" error for OAuth tokens +- Added OAuth documentation to readme with usage examples +- Created test cases for OAuth token flow + ## 2025-07-22 - 3.0.4 - fix(tests,security) Improve test reliability and remove sensitive file diff --git a/package.json b/package.json index 3e1612d..adc254b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apiclient.xyz/bunq", - "version": "3.0.4", + "version": "3.0.5", "private": false, "description": "A full-featured TypeScript/JavaScript client for the bunq API", "type": "module", @@ -17,6 +17,7 @@ "test:session": "(tstest test/test.session.ts --verbose)", "test:errors": "(tstest test/test.errors.ts --verbose)", "test:advanced": "(tstest test/test.advanced.ts --verbose)", + "test:oauth": "(tstest test/test.oauth.ts --verbose)", "build": "(tsbuild --web)" }, "devDependencies": { diff --git a/readme.md b/readme.md index a7c69ec..deffc1e 100644 --- a/readme.md +++ b/readme.md @@ -428,6 +428,27 @@ const payment = await BunqPayment.builder(bunq, account) // The same request ID will return the original payment without creating a duplicate ``` +### OAuth Token Support + +```typescript +// Using OAuth access token instead of API key +const bunq = new BunqAccount({ + apiKey: 'your-oauth-access-token', // OAuth token from bunq OAuth flow + deviceName: 'OAuth App', + environment: 'PRODUCTION', + isOAuthToken: true // Important: Set this flag for OAuth tokens +}); + +await bunq.init(); + +// OAuth tokens already have an associated session from the OAuth flow, +// so the library will skip session creation and use the token directly +const accounts = await bunq.getAccounts(); + +// Note: OAuth tokens have their own expiry mechanism managed by bunq's OAuth server +// The library will not attempt to refresh OAuth tokens +``` + ### Error Handling ```typescript diff --git a/test/test.oauth.ts b/test/test.oauth.ts new file mode 100644 index 0000000..2066959 --- /dev/null +++ b/test/test.oauth.ts @@ -0,0 +1,44 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import * as bunq from '../ts/index.js'; + +tap.test('should handle OAuth token initialization', async () => { + // Note: This test requires a valid OAuth token to run properly + // In a real test environment, you would use a test OAuth token + + // Test OAuth token initialization + const oauthBunq = new bunq.BunqAccount({ + apiKey: 'test-oauth-token', // This would be a real OAuth token + deviceName: 'OAuth Test App', + environment: 'SANDBOX', + isOAuthToken: true + }); + + // Mock test - in reality this would connect to bunq + try { + // The init should skip session creation for OAuth tokens + await oauthBunq.init(); + console.log('OAuth token initialization successful (mock)'); + } catch (error) { + // In sandbox with fake token, this will fail, which is expected + console.log('OAuth token test completed (expected failure with mock token)'); + } +}); + +tap.test('should not attempt session refresh for OAuth tokens', async () => { + const oauthBunq = new bunq.BunqAccount({ + apiKey: 'test-oauth-token', + deviceName: 'OAuth Test App', + environment: 'SANDBOX', + isOAuthToken: true + }); + + // Test that ensureValidSession doesn't try to refresh OAuth tokens + try { + await oauthBunq.apiContext.ensureValidSession(); + console.log('OAuth session management test passed'); + } catch (error) { + console.log('OAuth session test completed'); + } +}); + +tap.start(); \ No newline at end of file diff --git a/ts/bunq.classes.account.ts b/ts/bunq.classes.account.ts index 760e67d..6ae399e 100644 --- a/ts/bunq.classes.account.ts +++ b/ts/bunq.classes.account.ts @@ -9,6 +9,7 @@ export interface IBunqConstructorOptions { apiKey: string; environment: 'SANDBOX' | 'PRODUCTION'; permittedIps?: string[]; + isOAuthToken?: boolean; // Set to true when using OAuth access token instead of API key } /** @@ -35,7 +36,8 @@ export class BunqAccount { apiKey: this.options.apiKey, environment: this.options.environment, deviceDescription: this.options.deviceName, - permittedIps: this.options.permittedIps + permittedIps: this.options.permittedIps, + isOAuthToken: this.options.isOAuthToken }); // Initialize API context (handles installation, device registration, session) diff --git a/ts/bunq.classes.apicontext.ts b/ts/bunq.classes.apicontext.ts index 50431c0..319c008 100644 --- a/ts/bunq.classes.apicontext.ts +++ b/ts/bunq.classes.apicontext.ts @@ -9,6 +9,7 @@ export interface IBunqApiContextOptions { environment: 'SANDBOX' | 'PRODUCTION'; deviceDescription: string; permittedIps?: string[]; + isOAuthToken?: boolean; } export class BunqApiContext { @@ -43,6 +44,15 @@ export class BunqApiContext { * Initialize the API context (installation, device, session) */ public async init(): Promise { + // If using OAuth token, skip session creation + if (this.options.isOAuthToken) { + // OAuth tokens already have an associated session + this.context.sessionToken = this.options.apiKey; + this.session = new BunqSession(this.crypto, this.context); + this.session.setOAuthMode(true); + return; + } + // Try to load existing context const existingContext = await this.loadContext(); @@ -125,6 +135,11 @@ export class BunqApiContext { * Refresh session if needed */ public async ensureValidSession(): Promise { + // OAuth tokens don't need session refresh + if (this.options.isOAuthToken) { + return; + } + await this.session.refreshSession(); await this.saveContext(); } diff --git a/ts/bunq.classes.session.ts b/ts/bunq.classes.session.ts index 5492477..373b419 100644 --- a/ts/bunq.classes.session.ts +++ b/ts/bunq.classes.session.ts @@ -13,6 +13,7 @@ export class BunqSession { private crypto: BunqCrypto; private context: IBunqApiContext; private sessionExpiryTime: plugins.smarttime.TimeStamp; + private isOAuthMode: boolean = false; constructor(crypto: BunqCrypto, context: IBunqApiContext) { this.crypto = crypto; @@ -139,10 +140,27 @@ export class BunqSession { this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(Date.now() + 600000); } + /** + * Set OAuth mode + */ + public setOAuthMode(isOAuth: boolean): void { + this.isOAuthMode = isOAuth; + if (isOAuth) { + // OAuth tokens don't expire in the same way as regular sessions + // Set a far future expiry time + this.sessionExpiryTime = plugins.smarttime.TimeStamp.fromMilliSeconds(Date.now() + 365 * 24 * 60 * 60 * 1000); + } + } + /** * Check if session is still valid */ public isSessionValid(): boolean { + // OAuth tokens are always considered valid (they have their own expiry mechanism) + if (this.isOAuthMode) { + return true; + } + if (!this.sessionExpiryTime) { return false; } @@ -155,6 +173,11 @@ export class BunqSession { * Refresh the session if needed */ public async refreshSession(): Promise { + // OAuth tokens don't need session refresh + if (this.isOAuthMode) { + return; + } + if (!this.isSessionValid()) { await this.createSession(); }