diff --git a/changelog.md b/changelog.md index f109db9..4aa59ab 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-07-22 - 3.1.1 - fix(oauth) +Fix OAuth token authentication flow for existing installations + +- Fixed initWithExistingInstallation to properly create new sessions with existing installation/device +- OAuth tokens now correctly skip installation/device steps when they already exist +- Session creation still uses OAuth token as the secret parameter +- Properly handles "Superfluous authentication" errors by reusing existing installation +- Renamed initWithExistingSession to initWithExistingInstallation for clarity + ## 2025-07-22 - 3.1.0 - feat(oauth) Add OAuth session caching to prevent multiple authentication attempts diff --git a/package.json b/package.json index 92195bf..2d934b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apiclient.xyz/bunq", - "version": "3.1.0", + "version": "3.1.1", "private": false, "description": "A full-featured TypeScript/JavaScript client for the bunq API", "type": "module", diff --git a/ts/bunq.classes.account.ts b/ts/bunq.classes.account.ts index ad40305..ad7c698 100644 --- a/ts/bunq.classes.account.ts +++ b/ts/bunq.classes.account.ts @@ -64,10 +64,10 @@ export class BunqAccount { const errorMessages = error.errors.map(e => e.error_description).join(' '); if (errorMessages.includes('Superfluous authentication') || errorMessages.includes('Authentication token already has a user session')) { - console.log('OAuth token already has an active session, attempting to reuse...'); - // Try to use the token directly without creating new session - await this.apiContext.initWithExistingSession(); - // Cache the context with existing session + console.log('OAuth token already has installation/device, attempting to create new session...'); + // Try to create a new session with existing installation/device + await this.apiContext.initWithExistingInstallation(); + // Cache the context with new session BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext); } else { throw error; diff --git a/ts/bunq.classes.apicontext.ts b/ts/bunq.classes.apicontext.ts index 205a8eb..66eb78d 100644 --- a/ts/bunq.classes.apicontext.ts +++ b/ts/bunq.classes.apicontext.ts @@ -171,31 +171,49 @@ export class BunqApiContext { } /** - * Initialize with existing OAuth session (skip installation/device/session creation) + * Initialize with existing installation and device (for OAuth tokens that already completed these steps) */ - public async initWithExistingSession(): Promise { - // For OAuth tokens that already have a session, we just need to: - // 1. Use the OAuth token as the session token - // 2. Set OAuth mode for proper expiry handling + public async initWithExistingInstallation(): Promise { + // For OAuth tokens that already have installation/device but need a new session + // We need to: + // 1. Try to load existing installation/device info + // 2. Create a new session using the OAuth token as the secret - this.context.sessionToken = this.options.apiKey; + const existingContext = await this.loadContext(); - // Create session instance with existing token - this.session = new BunqSession(this.crypto, this.context); - this.session.setOAuthMode(true); - - // Try to get user info to validate the session - try { - // This will test if the session is valid - const testClient = this.session.getHttpClient(); - const response = await testClient.get('/v1/user'); + if (existingContext && existingContext.clientPrivateKey && existingContext.clientPublicKey) { + // Restore crypto keys from previous installation + this.crypto.setKeys( + existingContext.clientPrivateKey, + existingContext.clientPublicKey + ); - if (response && response.Response) { - console.log('Successfully reused existing OAuth session'); + // Update context with existing installation data + this.context = { ...this.context, ...existingContext }; + + // Create new session instance + this.session = new BunqSession(this.crypto, this.context); + + // Try to create a new session with the OAuth token + try { + await this.session.init( + this.options.deviceDescription, + this.options.permittedIps || [], + true // skipInstallationAndDevice = true + ); + + if (this.options.isOAuthToken) { + this.session.setOAuthMode(true); + } + await this.saveContext(); + console.log('Successfully created new session with existing installation'); + } catch (error) { + throw new Error(`Failed to create session with OAuth token: ${error.message}`); } - } catch (error) { - throw new Error(`Failed to reuse OAuth session: ${error.message}`); + } else { + // No existing installation, fall back to full init + throw new Error('No existing installation found, full initialization required'); } } } \ No newline at end of file diff --git a/ts/bunq.classes.session.ts b/ts/bunq.classes.session.ts index d069b48..e35cc95 100644 --- a/ts/bunq.classes.session.ts +++ b/ts/bunq.classes.session.ts @@ -24,14 +24,16 @@ export class BunqSession { /** * Initialize a new bunq API session */ - public async init(deviceDescription: string, permittedIps: string[] = []): Promise { - // Step 1: Installation - await this.createInstallation(); + public async init(deviceDescription: string, permittedIps: string[] = [], skipInstallationAndDevice: boolean = false): Promise { + if (!skipInstallationAndDevice) { + // Step 1: Installation + await this.createInstallation(); + + // Step 2: Device registration + await this.registerDevice(deviceDescription, permittedIps); + } - // Step 2: Device registration - await this.registerDevice(deviceDescription, permittedIps); - - // Step 3: Session creation + // Step 3: Session creation (always required) await this.createSession(); }