From b1c31614ce6387ad306895e9640f94e0d89ddb8c Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 15 Dec 2025 12:00:23 +0000 Subject: [PATCH 1/6] add dynamic support for storing refreshed credentials --- packages/cli/src/credentials-helper.ts | 43 ++++++++++++++++++- .../utils/request-helper-functions.ts | 6 ++- packages/workflow/src/interfaces.ts | 1 + 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index 4cee30dd4cd9f..9b0b7134cedb8 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -9,11 +9,11 @@ import { GLOBAL_OWNER_ROLE, SharedCredentialsRepository, } from '@n8n/db'; -import { Service } from '@n8n/di'; +import { Container, Service } from '@n8n/di'; import { PROJECT_ADMIN_ROLE_SLUG, PROJECT_OWNER_ROLE_SLUG } from '@n8n/permissions'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { EntityNotFoundError, In } from '@n8n/typeorm'; -import { Credentials, getAdditionalKeys } from 'n8n-core'; +import { Cipher, Credentials, getAdditionalKeys } from 'n8n-core'; import type { ICredentialDataDecryptedObject, ICredentialsExpressionResolveValues, @@ -40,6 +40,7 @@ import { Workflow, UnexpectedError, isExpression, + toCredentialContext, } from 'n8n-workflow'; import { RESPONSE_ERROR_MESSAGES } from './constants'; @@ -534,7 +535,45 @@ export class CredentialsHelper extends ICredentialsHelper { nodeCredentials: INodeCredentialsDetails, type: string, data: ICredentialDataDecryptedObject, + additionalData: IWorkflowExecuteAdditionalData, ): Promise { + const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type); + + if (credentialsEntity.isResolvable && credentialsEntity.resolverId) { + const cipher = Container.get(Cipher); + + let credentialContext: { version: 1; identity: string } | undefined; + + if (additionalData.executionContext?.credentials) { + const decrypted = cipher.decrypt(additionalData.executionContext.credentials); + credentialContext = toCredentialContext(decrypted) as { version: 1; identity: string }; + } + + if (!credentialContext) { + throw new UnexpectedError('No credential context found', { + extra: { nodeCredentials, type }, + }); + } + + const credentials = await this.getCredentials(nodeCredentials, type); + const staticData = credentials.getData(); + + await this.dynamicCredentialsProxy.storeIfNeeded( + { + id: credentialsEntity.id, + name: credentialsEntity.name, + type: credentialsEntity.type, + isResolvable: credentialsEntity.isResolvable, + resolverId: credentialsEntity.resolverId, + }, + { oauthTokenData: data.oauthTokenData }, + credentialContext, + staticData, + additionalData.workflowSettings, + ); + return; + } + const credentials = await this.getCredentials(nodeCredentials, type); credentials.updateData({ oauthTokenData: data.oauthTokenData }); diff --git a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts index 2f12f38964247..a7764db2af572 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts @@ -941,6 +941,7 @@ export async function requestOAuth2( nodeCredentials, credentialsType, credentials as unknown as ICredentialDataDecryptedObject, + additionalData, ); oauthTokenData = data; @@ -1022,6 +1023,7 @@ export async function requestOAuth2( nodeCredentials, credentialsType, credentials as unknown as ICredentialDataDecryptedObject, + additionalData, ); const refreshedRequestOption = newToken.sign(requestOptions as ClientOAuth2RequestObject); @@ -1102,6 +1104,7 @@ export async function requestOAuth2( nodeCredentials, credentialsType, credentials as unknown as ICredentialDataDecryptedObject, + additionalData, ); this.logger.debug( @@ -1268,6 +1271,7 @@ export async function refreshOAuth2Token( nodeCredentials, credentialsType, credentials as unknown as ICredentialDataDecryptedObject, + additionalData, ); this.logger.debug( @@ -1626,7 +1630,7 @@ export const getRequestHelperFunctions = ( const responseContentType = newResponse.headers['content-type']?.toString() ?? ''; if (responseContentType.includes('application/json')) { - newResponse.body = jsonParse(contentBody, { fallbackValue: {} }); + newResponse.body = jsonParse(contentBody as string, { fallbackValue: {} }); } else { newResponse.body = contentBody; } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index c62bb0df7a5de..23f0a03d58a84 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -239,6 +239,7 @@ export abstract class ICredentialsHelper { nodeCredentials: INodeCredentialsDetails, type: string, data: ICredentialDataDecryptedObject, + additionalData: IWorkflowExecuteAdditionalData, ): Promise; abstract getCredentialsProperties(type: string): INodeProperties[]; From d0bca01e75df37f9f8d0b682796804f0bf89124b Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 15 Dec 2025 12:17:09 +0000 Subject: [PATCH 2/6] add support for dynamic credentials to oauth refresh --- packages/cli/src/__tests__/credentials-helper.test.ts | 2 ++ .../node-execution-context/utils/request-helper-functions.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/__tests__/credentials-helper.test.ts b/packages/cli/src/__tests__/credentials-helper.test.ts index 48b7b77ddb41c..07c18afcb4610 100644 --- a/packages/cli/src/__tests__/credentials-helper.test.ts +++ b/packages/cli/src/__tests__/credentials-helper.test.ts @@ -12,6 +12,7 @@ import type { INodeProperties, INodeTypes, INodeCredentialsDetails, + IWorkflowExecuteAdditionalData, } from 'n8n-workflow'; import { deepCopy, Workflow } from 'n8n-workflow'; @@ -340,6 +341,7 @@ describe('CredentialsHelper', () => { nodeCredentials, 'oAuth2Api', newOauthTokenData, + {} as IWorkflowExecuteAdditionalData, ); expect(credentialsRepository.update).toHaveBeenCalledWith( diff --git a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts index a7764db2af572..b0c0b1cdebbd6 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts @@ -1630,7 +1630,7 @@ export const getRequestHelperFunctions = ( const responseContentType = newResponse.headers['content-type']?.toString() ?? ''; if (responseContentType.includes('application/json')) { - newResponse.body = jsonParse(contentBody as string, { fallbackValue: {} }); + newResponse.body = jsonParse(contentBody, { fallbackValue: {} }); } else { newResponse.body = contentBody; } From 0c93d3802512cc4a993bc556a1a62356339c7d09 Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 15 Dec 2025 12:44:14 +0000 Subject: [PATCH 3/6] fix tests --- .../utils/__tests__/request-helper-functions.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts index da226a24f80e8..e5c0aaaf969ec 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts @@ -939,6 +939,7 @@ describe('Request Helper Functions', () => { refresh_token: 'new-refresh-token', }), }), + mockAdditionalData, ); }); @@ -981,6 +982,7 @@ describe('Request Helper Functions', () => { refresh_token: 'new-refresh-token', }), }), + mockAdditionalData, ); }); @@ -1020,6 +1022,7 @@ describe('Request Helper Functions', () => { refresh_token: 'new-refresh-token', }), }), + mockAdditionalData, ); }); From e084a020932a5c0b675daf01760b124647d26c00 Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 15 Dec 2025 13:45:51 +0000 Subject: [PATCH 4/6] workflow resolver fallback --- packages/cli/src/credentials-helper.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index 9b0b7134cedb8..c520d02e2b55b 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -539,7 +539,10 @@ export class CredentialsHelper extends ICredentialsHelper { ): Promise { const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type); - if (credentialsEntity.isResolvable && credentialsEntity.resolverId) { + const resolverId = + credentialsEntity.resolverId ?? additionalData.workflowSettings?.credentialResolverId; + + if (credentialsEntity.isResolvable && resolverId) { const cipher = Container.get(Cipher); let credentialContext: { version: 1; identity: string } | undefined; @@ -564,7 +567,7 @@ export class CredentialsHelper extends ICredentialsHelper { name: credentialsEntity.name, type: credentialsEntity.type, isResolvable: credentialsEntity.isResolvable, - resolverId: credentialsEntity.resolverId, + resolverId, }, { oauthTokenData: data.oauthTokenData }, credentialContext, From 6b5b92687857f65cad85ab0247d450fdd52913a6 Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 15 Dec 2025 14:11:19 +0000 Subject: [PATCH 5/6] move logic in to proxy --- packages/cli/src/credentials-helper.ts | 26 +++-------- .../credentials/dynamic-credentials-proxy.ts | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index c520d02e2b55b..ba56678e68e14 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -9,11 +9,11 @@ import { GLOBAL_OWNER_ROLE, SharedCredentialsRepository, } from '@n8n/db'; -import { Container, Service } from '@n8n/di'; +import { Service } from '@n8n/di'; import { PROJECT_ADMIN_ROLE_SLUG, PROJECT_OWNER_ROLE_SLUG } from '@n8n/permissions'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { EntityNotFoundError, In } from '@n8n/typeorm'; -import { Cipher, Credentials, getAdditionalKeys } from 'n8n-core'; +import { Credentials, getAdditionalKeys } from 'n8n-core'; import type { ICredentialDataDecryptedObject, ICredentialsExpressionResolveValues, @@ -40,7 +40,6 @@ import { Workflow, UnexpectedError, isExpression, - toCredentialContext, } from 'n8n-workflow'; import { RESPONSE_ERROR_MESSAGES } from './constants'; @@ -543,25 +542,10 @@ export class CredentialsHelper extends ICredentialsHelper { credentialsEntity.resolverId ?? additionalData.workflowSettings?.credentialResolverId; if (credentialsEntity.isResolvable && resolverId) { - const cipher = Container.get(Cipher); - - let credentialContext: { version: 1; identity: string } | undefined; - - if (additionalData.executionContext?.credentials) { - const decrypted = cipher.decrypt(additionalData.executionContext.credentials); - credentialContext = toCredentialContext(decrypted) as { version: 1; identity: string }; - } - - if (!credentialContext) { - throw new UnexpectedError('No credential context found', { - extra: { nodeCredentials, type }, - }); - } - const credentials = await this.getCredentials(nodeCredentials, type); const staticData = credentials.getData(); - await this.dynamicCredentialsProxy.storeIfNeeded( + await this.dynamicCredentialsProxy.storeOAuthTokenDataIfNeeded( { id: credentialsEntity.id, name: credentialsEntity.name, @@ -569,8 +553,8 @@ export class CredentialsHelper extends ICredentialsHelper { isResolvable: credentialsEntity.isResolvable, resolverId, }, - { oauthTokenData: data.oauthTokenData }, - credentialContext, + data.oauthTokenData as IDataObject, + additionalData.executionContext, staticData, additionalData.workflowSettings, ); diff --git a/packages/cli/src/credentials/dynamic-credentials-proxy.ts b/packages/cli/src/credentials/dynamic-credentials-proxy.ts index c5edc3cc31520..49a6a3899855b 100644 --- a/packages/cli/src/credentials/dynamic-credentials-proxy.ts +++ b/packages/cli/src/credentials/dynamic-credentials-proxy.ts @@ -1,4 +1,6 @@ import type { Logger } from '@n8n/backend-common'; +import { Container } from '@n8n/di'; +import { Cipher } from 'n8n-core'; import type { CredentialStoreMetadata, IDynamicCredentialStorageProvider, @@ -6,9 +8,11 @@ import type { import type { ICredentialContext, ICredentialDataDecryptedObject, + IDataObject, IExecutionContext, IWorkflowSettings, } from 'n8n-workflow'; +import { toCredentialContext, UnexpectedError } from 'n8n-workflow'; import type { CredentialResolveMetadata, ICredentialResolutionProvider, @@ -79,4 +83,45 @@ export class DynamicCredentialsProxy workflowSettings, ); } + + /** + * Stores OAuth token data for dynamic credentials, handling execution context decryption + */ + async storeOAuthTokenDataIfNeeded( + credentialStoreMetadata: CredentialStoreMetadata, + oauthTokenData: IDataObject, + executionContext: IExecutionContext | undefined, + staticData: ICredentialDataDecryptedObject, + workflowSettings?: IWorkflowSettings, + ): Promise { + if (!credentialStoreMetadata.isResolvable || !credentialStoreMetadata.resolverId) { + return; + } + + const cipher = Container.get(Cipher); + + let credentialContext: { version: 1; identity: string } | undefined; + + if (executionContext?.credentials) { + const decrypted = cipher.decrypt(executionContext.credentials); + credentialContext = toCredentialContext(decrypted) as { version: 1; identity: string }; + } + + if (!credentialContext) { + throw new UnexpectedError('No credential context found', { + extra: { + credentialId: credentialStoreMetadata.id, + credentialName: credentialStoreMetadata.name, + }, + }); + } + + await this.storeIfNeeded( + credentialStoreMetadata, + { oauthTokenData } as ICredentialDataDecryptedObject, + credentialContext, + staticData, + workflowSettings, + ); + } } From 7800225ae0a6dea5581ee7fcb2bf16680aa5e903 Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 15 Dec 2025 14:16:47 +0000 Subject: [PATCH 6/6] dual di import --- packages/cli/src/credentials/dynamic-credentials-proxy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/credentials/dynamic-credentials-proxy.ts b/packages/cli/src/credentials/dynamic-credentials-proxy.ts index 49a6a3899855b..5689e9a894f78 100644 --- a/packages/cli/src/credentials/dynamic-credentials-proxy.ts +++ b/packages/cli/src/credentials/dynamic-credentials-proxy.ts @@ -1,5 +1,5 @@ import type { Logger } from '@n8n/backend-common'; -import { Container } from '@n8n/di'; +import { Container, Service } from '@n8n/di'; import { Cipher } from 'n8n-core'; import type { CredentialStoreMetadata, @@ -17,7 +17,6 @@ import type { CredentialResolveMetadata, ICredentialResolutionProvider, } from './credential-resolution-provider.interface'; -import { Service } from '@n8n/di'; @Service() export class DynamicCredentialsProxy