mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +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 { CredentialsEntity, type CredentialsRepository } from '@n8n/db';
|
||||||
import { EntityNotFoundError } from '@n8n/typeorm';
|
import { EntityNotFoundError } from '@n8n/typeorm';
|
||||||
|
import { Container } from '@n8n/di';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type {
|
import type {
|
||||||
IAuthenticateGeneric,
|
IAuthenticateGeneric,
|
||||||
@@ -9,8 +10,10 @@ import type {
|
|||||||
INode,
|
INode,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
|
INodeCredentialsDetails,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { deepCopy, Workflow } from 'n8n-workflow';
|
import { deepCopy, Workflow } from 'n8n-workflow';
|
||||||
|
import { type InstanceSettings, Cipher } from 'n8n-core';
|
||||||
|
|
||||||
import { CredentialTypes } from '@/credential-types';
|
import { CredentialTypes } from '@/credential-types';
|
||||||
import { CredentialsHelper } from '@/credentials-helper';
|
import { CredentialsHelper } from '@/credentials-helper';
|
||||||
@@ -22,6 +25,10 @@ describe('CredentialsHelper', () => {
|
|||||||
const mockNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
const mockNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
||||||
const credentialsRepository = mock<CredentialsRepository>();
|
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(
|
const credentialsHelper = new CredentialsHelper(
|
||||||
new CredentialTypes(mockNodesAndCredentials),
|
new CredentialTypes(mockNodesAndCredentials),
|
||||||
mock(),
|
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);
|
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> {
|
async credentialCanUseExternalSecrets(nodeCredential: INodeCredentialsDetails): Promise<boolean> {
|
||||||
if (!nodeCredential.id) {
|
if (!nodeCredential.id) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -74,4 +74,10 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||||||
_type: string,
|
_type: string,
|
||||||
_data: ICredentialDataDecryptedObject,
|
_data: ICredentialDataDecryptedObject,
|
||||||
): Promise<void> {}
|
): Promise<void> {}
|
||||||
|
|
||||||
|
async updateCredentialsOauthTokenData(
|
||||||
|
_nodeCredentials: INodeCredentialsDetails,
|
||||||
|
_type: string,
|
||||||
|
_data: ICredentialDataDecryptedObject,
|
||||||
|
): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -980,7 +980,7 @@ export async function requestOAuth2(
|
|||||||
credentials.oauthTokenData = data;
|
credentials.oauthTokenData = data;
|
||||||
|
|
||||||
// Save the refreshed token
|
// Save the refreshed token
|
||||||
await additionalData.credentialsHelper.updateCredentials(
|
await additionalData.credentialsHelper.updateCredentialsOauthTokenData(
|
||||||
nodeCredentials,
|
nodeCredentials,
|
||||||
credentialsType,
|
credentialsType,
|
||||||
credentials as unknown as ICredentialDataDecryptedObject,
|
credentials as unknown as ICredentialDataDecryptedObject,
|
||||||
@@ -1061,7 +1061,7 @@ export async function requestOAuth2(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const nodeCredentials = node.credentials[credentialsType];
|
const nodeCredentials = node.credentials[credentialsType];
|
||||||
await additionalData.credentialsHelper.updateCredentials(
|
await additionalData.credentialsHelper.updateCredentialsOauthTokenData(
|
||||||
nodeCredentials,
|
nodeCredentials,
|
||||||
credentialsType,
|
credentialsType,
|
||||||
credentials as unknown as ICredentialDataDecryptedObject,
|
credentials as unknown as ICredentialDataDecryptedObject,
|
||||||
@@ -1141,7 +1141,7 @@ export async function requestOAuth2(
|
|||||||
const nodeCredentials = node.credentials[credentialsType];
|
const nodeCredentials = node.credentials[credentialsType];
|
||||||
|
|
||||||
// Save the refreshed token
|
// Save the refreshed token
|
||||||
await additionalData.credentialsHelper.updateCredentials(
|
await additionalData.credentialsHelper.updateCredentialsOauthTokenData(
|
||||||
nodeCredentials,
|
nodeCredentials,
|
||||||
credentialsType,
|
credentialsType,
|
||||||
credentials as unknown as ICredentialDataDecryptedObject,
|
credentials as unknown as ICredentialDataDecryptedObject,
|
||||||
|
|||||||
@@ -223,6 +223,12 @@ export abstract class ICredentialsHelper {
|
|||||||
data: ICredentialDataDecryptedObject,
|
data: ICredentialDataDecryptedObject,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
abstract updateCredentialsOauthTokenData(
|
||||||
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
|
type: string,
|
||||||
|
data: ICredentialDataDecryptedObject,
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
abstract getCredentialsProperties(type: string): INodeProperties[];
|
abstract getCredentialsProperties(type: string): INodeProperties[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user