feat: Show scopes for managed OAuth credentials with N8N_MANAGED_OAUTH_SHOW_SCOPES (backport to release-candidate/2.35.x) (#36397)

Co-authored-by: yehorkardash <yehor.kardash@n8n.io>
Co-authored-by: Jon <jonathan.bennetts@gmail.com>
This commit is contained in:
n8n-assistant[bot]
2026-08-17 08:49:47 +00:00
committed by GitHub
parent 0dccd14a44
commit d8e3ee6c7e
7 changed files with 100 additions and 4 deletions
@@ -30,6 +30,14 @@ class CredentialsOverwrite {
*/
@Env('N8N_SKIP_CREDENTIAL_OVERWRITE')
skipTypes: CommaSeparatedStringArray<string> = [];
/**
* Comma-separated list of managed OAuth credential types whose scope fields
* remain user-configurable.
* @example `N8N_MANAGED_OAUTH_SHOW_SCOPES=googleOAuth2Api`
*/
@Env('N8N_MANAGED_OAUTH_SHOW_SCOPES')
showScopes: CommaSeparatedStringArray<string> = [];
}
@Config
+13
View File
@@ -135,6 +135,7 @@ describe('GlobalConfig', () => {
endpoint: '',
endpointAuthToken: '',
persistence: false,
showScopes: [],
skipTypes: [],
},
},
@@ -742,6 +743,18 @@ describe('GlobalConfig', () => {
expect(config.agents.sandboxSnapshot).toBe('n8n/agent-knowledge:1.2.3');
});
it('should parse N8N_MANAGED_OAUTH_SHOW_SCOPES from env variables', () => {
process.env = {
N8N_MANAGED_OAUTH_SHOW_SCOPES: 'googleOAuth2Api,microsoftOAuth2Api',
};
const config = Container.get(GlobalConfig);
expect(config.credentials.overwrite.showScopes).toEqual([
'googleOAuth2Api',
'microsoftOAuth2Api',
]);
});
it('should use values from env variables when defined', () => {
process.env = {
DB_POSTGRESDB_HOST: 'some-host',
@@ -838,10 +838,54 @@ describe('FrontendService', () => {
describe('overwriteCredentialsProperties', () => {
afterEach(() => {
// Restore globalConfig.credentials to the default so other tests are unaffected
(globalConfig as any).credentials = { overwrite: { skipTypes: [] } };
(globalConfig as any).credentials = { overwrite: { showScopes: [], skipTypes: [] } };
loadNodesAndCredentials.types = { credentials: [], nodes: [] };
});
it('should expose managed OAuth scopes only for configured credential types', () => {
const baseCredential = {
name: 'googleOAuth2Api',
displayName: 'Google OAuth2 API',
properties: [],
} as ICredentialType;
const childCredential = {
name: 'googleSheetsOAuth2Api',
displayName: 'Google Sheets OAuth2 API',
properties: [],
} as ICredentialType;
loadNodesAndCredentials.types = {
credentials: [baseCredential, childCredential],
nodes: [],
};
(globalConfig as any).credentials = {
overwrite: { showScopes: ['googleOAuth2Api'], skipTypes: [] },
};
const { service } = createMockService();
(service as any).overwriteCredentialsProperties();
expect(baseCredential.__showManagedOAuthScopes).toBe(true);
expect(childCredential.__showManagedOAuthScopes).toBeUndefined();
});
it('should clear stale managed OAuth scope visibility metadata', () => {
const credential = {
name: 'googleOAuth2Api',
displayName: 'Google OAuth2 API',
properties: [],
__showManagedOAuthScopes: true,
} as ICredentialType;
loadNodesAndCredentials.types = { credentials: [credential], nodes: [] };
(globalConfig as any).credentials = { overwrite: { showScopes: [], skipTypes: [] } };
const { service } = createMockService();
(service as any).overwriteCredentialsProperties();
expect(credential.__showManagedOAuthScopes).toBeUndefined();
});
it('should set __skipManagedCreation for types in the skip list', () => {
const skipCredential = {
name: 'googleSheetsOAuth2Api',
@@ -764,11 +764,12 @@ export class FrontendService {
private overwriteCredentialsProperties() {
const { credentials } = this.loadNodesAndCredentials.types;
const credentialsOverwrites = this.credentialsOverwrites.getAll();
const { skipTypes } = this.globalConfig.credentials.overwrite;
const { showScopes = [], skipTypes } = this.globalConfig.credentials.overwrite;
for (const credential of credentials) {
// Clear any existing overwritten properties to prevent stale data
delete credential.__overwrittenProperties;
delete credential.__skipManagedCreation;
delete credential.__showManagedOAuthScopes;
const overwrittenProperties = [];
this.credentialTypes
@@ -793,6 +794,10 @@ export class FrontendService {
credential.__skipManagedCreation = true;
}
if (showScopes.includes(credential.name)) {
credential.__showManagedOAuthScopes = true;
}
// Inject the per-instance JWKS URI as the default of any `jwksUri`
// property on `oAuth2Api` itself or any credential that extends it
// and explicitly re-declares the field. Inheritance is blocked by
@@ -783,6 +783,30 @@ describe('CredentialEdit', () => {
expect(queryByText('Custom Scopes Notice')).not.toBeInTheDocument();
});
test('shows scope fields when enabled for the managed OAuth credential type', async () => {
const credentialsStore = setupManagedCapableStores({
grantType: 'authorizationCode',
customScopes: true,
});
credentialsStore.state.credentialTypes[discordOAuth2ApiManagedCapable.name] = {
...discordOAuth2ApiManagedCapable,
__showManagedOAuthScopes: true,
};
const { queryByText } = renderComponent({
props: {
activeId: 'cred-2',
modalName: CREDENTIAL_EDIT_MODAL_KEY,
mode: 'edit',
},
});
await retry(() => expect(credentialsStore.getCredentialData).toHaveBeenCalled());
expect(queryByText('Custom Scopes')).toBeInTheDocument();
expect(queryByText('Enabled Scopes')).toBeInTheDocument();
});
test('shows scope fields when managed OAuth is available but user has provided their own clientId/clientSecret', async () => {
const credentialsStore = setupManagedCapableStores({
grantType: 'authorizationCode',
@@ -357,10 +357,11 @@ export function useCredentialForm(options: UseCredentialFormOptions) {
function displayCredentialParameter(parameter: INodeProperties): boolean {
if (parameter.type === 'hidden') return false;
const isManagedCredential = isEditingManagedCredential.value || isManagedOAuthMode.value;
if (
MANAGED_CREDENTIAL_HIDDEN_PROPERTIES.has(parameter.name) &&
(isEditingManagedCredential.value || isManagedOAuthMode.value)
isManagedCredential &&
!credentialType.value?.__showManagedOAuthScopes
) {
return false;
}
+1
View File
@@ -393,6 +393,7 @@ export interface ICredentialType {
documentationUrl?: string;
__overwrittenProperties?: string[];
__skipManagedCreation?: boolean;
__showManagedOAuthScopes?: boolean;
authenticate?: IAuthenticate;
preAuthentication?: (
this: IHttpRequestHelper,