feat(oauth): add OAuth session caching to prevent multiple authentication attempts
This commit is contained in:
@@ -2,6 +2,7 @@ import * as plugins from './bunq.plugins.js';
|
||||
import { BunqApiContext } from './bunq.classes.apicontext.js';
|
||||
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
|
||||
import { BunqUser } from './bunq.classes.user.js';
|
||||
import { BunqApiError } from './bunq.classes.httpclient.js';
|
||||
import type { IBunqSessionServerResponse } from './bunq.interfaces.js';
|
||||
|
||||
export interface IBunqConstructorOptions {
|
||||
@@ -16,6 +17,9 @@ export interface IBunqConstructorOptions {
|
||||
* the main bunq account
|
||||
*/
|
||||
export class BunqAccount {
|
||||
// Static cache for OAuth token sessions to prevent multiple authentication attempts
|
||||
private static oauthSessionCache = new Map<string, BunqApiContext>();
|
||||
|
||||
public options: IBunqConstructorOptions;
|
||||
public apiContext: BunqApiContext;
|
||||
public userId: number;
|
||||
@@ -31,17 +35,60 @@ export class BunqAccount {
|
||||
* Initialize the bunq account
|
||||
*/
|
||||
public async init() {
|
||||
// Create API context
|
||||
this.apiContext = new BunqApiContext({
|
||||
apiKey: this.options.apiKey,
|
||||
environment: this.options.environment,
|
||||
deviceDescription: this.options.deviceName,
|
||||
permittedIps: this.options.permittedIps,
|
||||
isOAuthToken: this.options.isOAuthToken
|
||||
});
|
||||
// For OAuth tokens, check if we already have a cached session
|
||||
if (this.options.isOAuthToken) {
|
||||
const cacheKey = `${this.options.apiKey}_${this.options.environment}`;
|
||||
const cachedContext = BunqAccount.oauthSessionCache.get(cacheKey);
|
||||
|
||||
if (cachedContext && cachedContext.hasValidSession()) {
|
||||
// Reuse existing session
|
||||
this.apiContext = cachedContext;
|
||||
console.log('Reusing existing OAuth session from cache');
|
||||
} else {
|
||||
// Create new context and cache it
|
||||
this.apiContext = new BunqApiContext({
|
||||
apiKey: this.options.apiKey,
|
||||
environment: this.options.environment,
|
||||
deviceDescription: this.options.deviceName,
|
||||
permittedIps: this.options.permittedIps,
|
||||
isOAuthToken: this.options.isOAuthToken
|
||||
});
|
||||
|
||||
try {
|
||||
await this.apiContext.init();
|
||||
// Cache the successfully initialized context
|
||||
BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext);
|
||||
} catch (error) {
|
||||
// Handle "Superfluous authentication" or "Authentication token already has a user session" errors
|
||||
if (error instanceof BunqApiError) {
|
||||
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
|
||||
BunqAccount.oauthSessionCache.set(cacheKey, this.apiContext);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular API key flow
|
||||
this.apiContext = new BunqApiContext({
|
||||
apiKey: this.options.apiKey,
|
||||
environment: this.options.environment,
|
||||
deviceDescription: this.options.deviceName,
|
||||
permittedIps: this.options.permittedIps,
|
||||
isOAuthToken: this.options.isOAuthToken
|
||||
});
|
||||
|
||||
// Initialize API context (handles installation, device registration, session)
|
||||
await this.apiContext.init();
|
||||
await this.apiContext.init();
|
||||
}
|
||||
|
||||
// Create user instance
|
||||
this.bunqUser = new BunqUser(this.apiContext);
|
||||
@@ -160,4 +207,28 @@ export class BunqAccount {
|
||||
this.apiContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the OAuth session cache
|
||||
*/
|
||||
public static clearOAuthCache(): void {
|
||||
BunqAccount.oauthSessionCache.clear();
|
||||
console.log('OAuth session cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a specific OAuth token from the cache
|
||||
*/
|
||||
public static clearOAuthCacheForToken(apiKey: string, environment: 'SANDBOX' | 'PRODUCTION'): void {
|
||||
const cacheKey = `${apiKey}_${environment}`;
|
||||
BunqAccount.oauthSessionCache.delete(cacheKey);
|
||||
console.log(`OAuth session cache cleared for token in ${environment} environment`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current size of the OAuth cache
|
||||
*/
|
||||
public static getOAuthCacheSize(): number {
|
||||
return BunqAccount.oauthSessionCache.size;
|
||||
}
|
||||
}
|
||||
|
@@ -162,4 +162,40 @@ export class BunqApiContext {
|
||||
public getBaseUrl(): string {
|
||||
return this.context.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the context has a valid session
|
||||
*/
|
||||
public hasValidSession(): boolean {
|
||||
return this.session && this.session.isSessionValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with existing OAuth session (skip installation/device/session creation)
|
||||
*/
|
||||
public async initWithExistingSession(): Promise<void> {
|
||||
// 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
|
||||
|
||||
this.context.sessionToken = this.options.apiKey;
|
||||
|
||||
// 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 (response && response.Response) {
|
||||
console.log('Successfully reused existing OAuth session');
|
||||
await this.saveContext();
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to reuse OAuth session: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user