mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(core): On OAuth access token update only update partial credential (#17135)
Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user