From 93dddf618141285ebf3442b30cb58d8ff3393bce Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 22 Jul 2025 21:56:10 +0000 Subject: [PATCH] fix(oauth): correct OAuth implementation to match bunq documentation --- changelog.md | 18 ++++++++++++++++++ package.json | 2 +- readme.md | 12 +++++++----- test/test.oauth.ts | 26 ++++++++++++-------------- ts/bunq.classes.apicontext.ts | 19 +++++-------------- ts/bunq.classes.httpclient.ts | 32 ++++++++------------------------ ts/bunq.classes.session.ts | 12 ------------ 7 files changed, 51 insertions(+), 70 deletions(-) diff --git a/changelog.md b/changelog.md index bb4c1ac..13dd0fe 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,23 @@ # Changelog +## 2025-07-22 - 3.0.8 - fix(oauth) +Correct OAuth implementation to match bunq documentation + +- Removed OAuth mode from HTTP client - OAuth tokens use normal request signing +- OAuth tokens now work exactly like regular API keys (per bunq docs) +- Fixed test comments to reflect correct OAuth behavior +- Simplified OAuth handling by removing unnecessary special cases +- OAuth tokens properly go through full auth flow with request signing + +## 2025-07-22 - 3.0.7 - fix(oauth) +Fix OAuth token authentication flow + +- OAuth tokens now go through full initialization (installation → device → session) +- Fixed "Insufficient authentication" errors by treating OAuth tokens as API keys +- OAuth tokens are used as the 'secret' parameter, not as session tokens +- Follows bunq documentation: "Just use the OAuth Token as a normal bunq API key" +- Removed incorrect session skip logic for OAuth tokens + ## 2025-07-22 - 3.0.6 - fix(oauth) Fix OAuth token private key error diff --git a/package.json b/package.json index b24eb80..57a182b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apiclient.xyz/bunq", - "version": "3.0.6", + "version": "3.0.8", "private": false, "description": "A full-featured TypeScript/JavaScript client for the bunq API", "type": "module", diff --git a/readme.md b/readme.md index deffc1e..fbcc2a9 100644 --- a/readme.md +++ b/readme.md @@ -436,17 +436,19 @@ 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 + isOAuthToken: true // Optional: Set for OAuth-specific handling }); 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 +// OAuth tokens work just like regular API keys: +// 1. They go through installation → device → session creation +// 2. The OAuth token is used as the 'secret' during authentication +// 3. A session token is created and used for all API calls 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 +// According to bunq documentation: +// "Just use the OAuth Token (access_token) as a normal bunq API key" ``` ### Error Handling diff --git a/test/test.oauth.ts b/test/test.oauth.ts index 02808d5..969a7a2 100644 --- a/test/test.oauth.ts +++ b/test/test.oauth.ts @@ -15,7 +15,8 @@ tap.test('should handle OAuth token initialization', async () => { // Mock test - in reality this would connect to bunq try { - // The init should skip session creation for OAuth tokens + // OAuth tokens should go through full initialization flow + // (installation → device → session) await oauthBunq.init(); console.log('OAuth token initialization successful (mock)'); } catch (error) { @@ -24,7 +25,7 @@ tap.test('should handle OAuth token initialization', async () => { } }); -tap.test('should not attempt session refresh for OAuth tokens', async () => { +tap.test('should handle OAuth token session management', async () => { const oauthBunq = new bunq.BunqAccount({ apiKey: 'test-oauth-token', deviceName: 'OAuth Test App', @@ -32,7 +33,8 @@ tap.test('should not attempt session refresh for OAuth tokens', async () => { isOAuthToken: true }); - // Test that ensureValidSession doesn't try to refresh OAuth tokens + // OAuth tokens now behave the same as regular API keys + // They go through normal session management try { await oauthBunq.apiContext.ensureValidSession(); console.log('OAuth session management test passed'); @@ -41,7 +43,7 @@ tap.test('should not attempt session refresh for OAuth tokens', async () => { } }); -tap.test('should handle OAuth tokens without private key errors', async () => { +tap.test('should handle OAuth tokens through full initialization', async () => { const oauthBunq = new bunq.BunqAccount({ apiKey: 'test-oauth-token', deviceName: 'OAuth Test App', @@ -50,21 +52,17 @@ tap.test('should handle OAuth tokens without private key errors', async () => { }); try { - // Initialize (should skip session creation) + // OAuth tokens go through full initialization flow + // The OAuth token is used as the API key/secret await oauthBunq.init(); - // Try to make a request (should skip signing) - // This would have thrown "Private key not generated yet" before the fix + // The HTTP client works normally with OAuth tokens (including request signing) const httpClient = oauthBunq.apiContext.getHttpClient(); - // Test that HTTP client is in OAuth mode and won't try to sign - console.log('OAuth HTTP client test passed - no private key errors'); + console.log('OAuth initialization test passed - full flow completed'); } catch (error) { - // Expected to fail with network/auth error, not private key error - if (error.message && error.message.includes('Private key not generated')) { - throw new Error('OAuth mode should not require private keys'); - } - console.log('OAuth private key test completed (expected network failure)'); + // Expected to fail with invalid token error, not initialization skip + console.log('OAuth initialization test completed (expected auth failure with mock token)'); } }); diff --git a/ts/bunq.classes.apicontext.ts b/ts/bunq.classes.apicontext.ts index 319c008..b75841a 100644 --- a/ts/bunq.classes.apicontext.ts +++ b/ts/bunq.classes.apicontext.ts @@ -44,15 +44,6 @@ 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(); @@ -78,6 +69,11 @@ export class BunqApiContext { this.options.deviceDescription, this.options.permittedIps || [] ); + + // Set OAuth mode if applicable (for session expiry handling) + if (this.options.isOAuthToken) { + this.session.setOAuthMode(true); + } // Save context await this.saveContext(); @@ -135,11 +131,6 @@ 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.httpclient.ts b/ts/bunq.classes.httpclient.ts index a910186..8326163 100644 --- a/ts/bunq.classes.httpclient.ts +++ b/ts/bunq.classes.httpclient.ts @@ -10,20 +10,12 @@ export class BunqHttpClient { private crypto: BunqCrypto; private context: IBunqApiContext; private requestCounter: number = 0; - private isOAuthMode: boolean = false; constructor(crypto: BunqCrypto, context: IBunqApiContext) { this.crypto = crypto; this.context = context; } - /** - * Set OAuth mode - */ - public setOAuthMode(isOAuth: boolean): void { - this.isOAuthMode = isOAuth; - } - /** * Update the API context (used after getting session token) */ @@ -44,20 +36,13 @@ export class BunqHttpClient { const body = options.body ? JSON.stringify(options.body) : undefined; // Add signature if required - // Skip signing for OAuth tokens or if explicitly disabled - if (options.useSigning !== false && !this.isOAuthMode) { - try { - const privateKey = this.crypto.getPrivateKey(); - headers['X-Bunq-Client-Signature'] = this.crypto.createSignatureHeader( - options.method, - options.endpoint, - headers, - body || '' - ); - } catch (error) { - // If no private key is available (e.g., OAuth mode), skip signing - // This is expected for OAuth tokens - } + if (options.useSigning !== false && this.crypto.getPrivateKey()) { + headers['X-Bunq-Client-Signature'] = this.crypto.createSignatureHeader( + options.method, + options.endpoint, + headers, + body || '' + ); } // Make the request @@ -81,8 +66,7 @@ export class BunqHttpClient { const response = await plugins.smartrequest.request(url, requestOptions); // Verify response signature if we have server public key - // Skip verification for OAuth tokens as they don't have installation keys - if (this.context.serverPublicKey && !this.isOAuthMode) { + if (this.context.serverPublicKey) { // Convert headers to string-only format const stringHeaders: { [key: string]: string } = {}; for (const [key, value] of Object.entries(response.headers)) { diff --git a/ts/bunq.classes.session.ts b/ts/bunq.classes.session.ts index 0e0b84f..d069b48 100644 --- a/ts/bunq.classes.session.ts +++ b/ts/bunq.classes.session.ts @@ -149,8 +149,6 @@ export class BunqSession { // 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); - // Also set OAuth mode on HTTP client - this.httpClient.setOAuthMode(true); } } @@ -158,11 +156,6 @@ export class BunqSession { * 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; } @@ -175,11 +168,6 @@ 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(); }