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/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index 4cee30dd4cd9f..ba56678e68e14 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -534,7 +534,33 @@ export class CredentialsHelper extends ICredentialsHelper { nodeCredentials: INodeCredentialsDetails, type: string, data: ICredentialDataDecryptedObject, + additionalData: IWorkflowExecuteAdditionalData, ): Promise { + const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type); + + const resolverId = + credentialsEntity.resolverId ?? additionalData.workflowSettings?.credentialResolverId; + + if (credentialsEntity.isResolvable && resolverId) { + const credentials = await this.getCredentials(nodeCredentials, type); + const staticData = credentials.getData(); + + await this.dynamicCredentialsProxy.storeOAuthTokenDataIfNeeded( + { + id: credentialsEntity.id, + name: credentialsEntity.name, + type: credentialsEntity.type, + isResolvable: credentialsEntity.isResolvable, + resolverId, + }, + data.oauthTokenData as IDataObject, + additionalData.executionContext, + staticData, + additionalData.workflowSettings, + ); + return; + } + const credentials = await this.getCredentials(nodeCredentials, type); credentials.updateData({ oauthTokenData: data.oauthTokenData }); diff --git a/packages/cli/src/credentials/dynamic-credentials-proxy.ts b/packages/cli/src/credentials/dynamic-credentials-proxy.ts index c5edc3cc31520..5689e9a894f78 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, Service } from '@n8n/di'; +import { Cipher } from 'n8n-core'; import type { CredentialStoreMetadata, IDynamicCredentialStorageProvider, @@ -6,14 +8,15 @@ import type { import type { ICredentialContext, ICredentialDataDecryptedObject, + IDataObject, IExecutionContext, IWorkflowSettings, } from 'n8n-workflow'; +import { toCredentialContext, UnexpectedError } from 'n8n-workflow'; import type { CredentialResolveMetadata, ICredentialResolutionProvider, } from './credential-resolution-provider.interface'; -import { Service } from '@n8n/di'; @Service() export class DynamicCredentialsProxy @@ -79,4 +82,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, + ); + } } 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, ); }); 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..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 @@ -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( diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index ed9924c52daea..5c48fbcbb5203 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -240,6 +240,7 @@ export abstract class ICredentialsHelper { nodeCredentials: INodeCredentialsDetails, type: string, data: ICredentialDataDecryptedObject, + additionalData: IWorkflowExecuteAdditionalData, ): Promise; abstract getCredentialsProperties(type: string): INodeProperties[];