fix(core): On OAuth access token update only update partial credential (#17135)

Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
Andreas Fitzek
2025-07-09 12:35:22 +02:00
committed by GitHub
parent 0f59eeaf5b
commit c8b3ac6ab0
5 changed files with 139 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
import { CredentialsEntity, type CredentialsRepository } from '@n8n/db';
import { EntityNotFoundError } from '@n8n/typeorm';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import type {
IAuthenticateGeneric,
@@ -9,8 +10,10 @@ import type {
INode,
INodeProperties,
INodeTypes,
INodeCredentialsDetails,
} from 'n8n-workflow';
import { deepCopy, Workflow } from 'n8n-workflow';
import { type InstanceSettings, Cipher } from 'n8n-core';
import { CredentialTypes } from '@/credential-types';
import { CredentialsHelper } from '@/credentials-helper';
@@ -22,6 +25,10 @@ describe('CredentialsHelper', () => {
const mockNodesAndCredentials = mock<LoadNodesAndCredentials>();
const credentialsRepository = mock<CredentialsRepository>();
// Setup cipher for testing
const cipher = new Cipher(mock<InstanceSettings>({ encryptionKey: 'test_key_for_testing' }));
Container.set(Cipher, cipher);
const credentialsHelper = new CredentialsHelper(
new CredentialTypes(mockNodesAndCredentials),
mock(),
@@ -283,4 +290,95 @@ describe('CredentialsHelper', () => {
});
}
});
describe('updateCredentialsOauthTokenData', () => {
test('only updates oauthTokenData field while preserving other credential fields', async () => {
const nodeCredentials: INodeCredentialsDetails = {
id: 'cred-123',
name: 'Test OAuth2 Credential',
};
const existingCredentialData = {
clientId: 'existing-client-id',
clientSecret: 'existing-client-secret',
scope: 'read write',
customField: 'custom-value',
oauthTokenData: {
access_token: 'old-access-token',
refresh_token: 'old-refresh-token',
expires_in: 3600,
},
};
const newOauthTokenData = {
oauthTokenData: {
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 7200,
token_type: 'Bearer',
},
};
const mockCredentialEntity = {
id: 'cred-123',
name: 'Test OAuth2 Credential',
type: 'oAuth2Api',
data: cipher.encrypt(existingCredentialData),
};
credentialsRepository.findOneByOrFail.mockResolvedValue(
mockCredentialEntity as CredentialsEntity,
);
const beforeUpdateTime = new Date();
await credentialsHelper.updateCredentialsOauthTokenData(
nodeCredentials,
'oAuth2Api',
newOauthTokenData,
);
expect(credentialsRepository.update).toHaveBeenCalledWith(
{ id: 'cred-123', type: 'oAuth2Api' },
expect.objectContaining({
id: 'cred-123',
name: 'Test OAuth2 Credential',
type: 'oAuth2Api',
data: expect.any(String),
updatedAt: expect.any(Date),
}),
);
const updateCall = credentialsRepository.update.mock.calls[0];
const updatedCredentialData = updateCall[1];
const updatedAt = updatedCredentialData.updatedAt as Date;
expect(updatedAt).toBeInstanceOf(Date);
expect(updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdateTime.getTime());
const decryptedUpdatedData = cipher.decrypt(updatedCredentialData.data as string);
const parsedUpdatedData = JSON.parse(decryptedUpdatedData);
expect(parsedUpdatedData).toEqual({
clientId: 'existing-client-id',
clientSecret: 'existing-client-secret',
scope: 'read write',
customField: 'custom-value',
oauthTokenData: {
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 7200,
token_type: 'Bearer',
},
});
expect(parsedUpdatedData.clientId).toBe('existing-client-id');
expect(parsedUpdatedData.clientSecret).toBe('existing-client-secret');
expect(parsedUpdatedData.scope).toBe('read write');
expect(parsedUpdatedData.customField).toBe('custom-value');
expect(parsedUpdatedData.oauthTokenData.access_token).toBe('new-access-token');
expect(parsedUpdatedData.oauthTokenData.refresh_token).toBe('new-refresh-token');
expect(parsedUpdatedData.oauthTokenData.expires_in).toBe(7200);
expect(parsedUpdatedData.oauthTokenData.token_type).toBe('Bearer');
});
});
});

View File

@@ -457,6 +457,32 @@ export class CredentialsHelper extends ICredentialsHelper {
await this.credentialsRepository.update(findQuery, newCredentialsData);
}
/**
* Updates credential's oauth token data in the database
*/
async updateCredentialsOauthTokenData(
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void> {
const credentials = await this.getCredentials(nodeCredentials, type);
credentials.updateData({ oauthTokenData: data.oauthTokenData });
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
// Add special database related data
newCredentialsData.updatedAt = new Date();
// Save the credentials in DB
const findQuery = {
id: credentials.id,
type,
};
// @ts-ignore CAT-957
await this.credentialsRepository.update(findQuery, newCredentialsData);
}
async credentialCanUseExternalSecrets(nodeCredential: INodeCredentialsDetails): Promise<boolean> {
if (!nodeCredential.id) {
return false;

View File

@@ -74,4 +74,10 @@ export class CredentialsHelper extends ICredentialsHelper {
_type: string,
_data: ICredentialDataDecryptedObject,
): Promise<void> {}
async updateCredentialsOauthTokenData(
_nodeCredentials: INodeCredentialsDetails,
_type: string,
_data: ICredentialDataDecryptedObject,
): Promise<void> {}
}

View File

@@ -980,7 +980,7 @@ export async function requestOAuth2(
credentials.oauthTokenData = data;
// Save the refreshed token
await additionalData.credentialsHelper.updateCredentials(
await additionalData.credentialsHelper.updateCredentialsOauthTokenData(
nodeCredentials,
credentialsType,
credentials as unknown as ICredentialDataDecryptedObject,
@@ -1061,7 +1061,7 @@ export async function requestOAuth2(
});
}
const nodeCredentials = node.credentials[credentialsType];
await additionalData.credentialsHelper.updateCredentials(
await additionalData.credentialsHelper.updateCredentialsOauthTokenData(
nodeCredentials,
credentialsType,
credentials as unknown as ICredentialDataDecryptedObject,
@@ -1141,7 +1141,7 @@ export async function requestOAuth2(
const nodeCredentials = node.credentials[credentialsType];
// Save the refreshed token
await additionalData.credentialsHelper.updateCredentials(
await additionalData.credentialsHelper.updateCredentialsOauthTokenData(
nodeCredentials,
credentialsType,
credentials as unknown as ICredentialDataDecryptedObject,

View File

@@ -223,6 +223,12 @@ export abstract class ICredentialsHelper {
data: ICredentialDataDecryptedObject,
): Promise<void>;
abstract updateCredentialsOauthTokenData(
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void>;
abstract getCredentialsProperties(type: string): INodeProperties[];
}