Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions packages/cli/src/credentials/__tests__/credentials.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2258,4 +2258,57 @@ describe('CredentialsService', () => {
).rejects.toThrow('The field "apiKey" is mandatory for credentials of type "apiCredential"');
});
});

describe('validateOAuthCredentialUrls', () => {
const testProjectId = 'test-project-id';

beforeEach(() => {
credentialsHelper.getCredentialsProperties.mockReturnValue([]);
jest.spyOn(validation, 'validateExternalSecretsPermissions').mockResolvedValue();
});

it('should reject an invalid OAuth2 URL', async () => {
credentialTypes.getParentTypes.mockReturnValue(['oAuth2Api']);
const data = { authUrl: 'javascript:alert(1)' };

await expect(
service.checkCredentialData('myOAuth2Cred', data, ownerUser, testProjectId),
).rejects.toThrow('OAuth url must use HTTP or HTTPS protocol');
});

it('should accept a valid OAuth2 URL', async () => {
credentialTypes.getParentTypes.mockReturnValue(['oAuth2Api']);
const data = { authUrl: 'https://example.com/oauth/authorize' };

await expect(
service.checkCredentialData('myOAuth2Cred', data, ownerUser, testProjectId),
).resolves.toBeUndefined();
});

it('should skip validation for expression-prefixed OAuth2 URLs', async () => {
credentialTypes.getParentTypes.mockReturnValue(['oAuth2Api']);
const data = {
authUrl: '=https://example.com/oauth/authorize',
accessTokenUrl: '={{ $vars.tokenUrl }}',
serverUrl: '={{ $vars.serverUrl }}',
};

await expect(
service.checkCredentialData('myOAuth2Cred', data, ownerUser, testProjectId),
).resolves.toBeUndefined();
});

it('should skip validation for expression-prefixed OAuth1 URLs', async () => {
credentialTypes.getParentTypes.mockReturnValue(['oAuth1Api']);
const data = {
authUrl: '={{ $vars.authUrl }}',
requestTokenUrl: '=https://example.com/request-token',
accessTokenUrl: '={{ $vars.tokenUrl }}',
};

await expect(
service.checkCredentialData('myOAuth1Cred', data, ownerUser, testProjectId),
).resolves.toBeUndefined();
});
});
});
5 changes: 3 additions & 2 deletions packages/cli/src/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
CREDENTIAL_EMPTY_VALUE,
deepCopy,
displayParameter,
isExpression,
isINodePropertyCollection,
NodeHelpers,
} from 'n8n-workflow';
Expand Down Expand Up @@ -1073,7 +1074,7 @@ export class CredentialsService {
const oauthUrlFields = ['authUrl', 'accessTokenUrl', 'serverUrl'] as const;
for (const field of oauthUrlFields) {
const value = data[field];
if (typeof value === 'string' && value.trim() !== '') {
if (typeof value === 'string' && value.trim() !== '' && !isExpression(value)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be evaluating the expression to validate it on the back-end? or are we happy this isn't validated?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can, as the evaluation could require run context data that is not here on configuration time. I've seen this pattern of discarding specific field validation if it contains expressions (e.g https://github.com/n8n-io/n8n/blob/master/packages/@n8n/ai-workflow-builder.ee/src/validation/checks/parameters.ts#L215)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, fair, I imagine this is going to be tricky if we begin implementing allow / deny listing domains instance wide, if we don't evaluate this before saving

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. I guess we would need to re-introduce some kind of validation context, meaning we would need to know if the expression can be validated in globally of it requires runtime data

validateOAuthUrl(value);
}
}
Expand All @@ -1082,7 +1083,7 @@ export class CredentialsService {
const oauthUrlFields = ['authUrl', 'requestTokenUrl', 'accessTokenUrl'] as const;
for (const field of oauthUrlFields) {
const value = data[field];
if (typeof value === 'string' && value.trim() !== '') {
if (typeof value === 'string' && value.trim() !== '' && !isExpression(value)) {
validateOAuthUrl(value);
}
}
Expand Down
Loading