diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index a6081382e5..efddcbb9ea 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -55,8 +55,8 @@ auth: clientSecret: ${AZURE_CLIENT_SECRET} tenantId: ${AZURE_TENANT_ID} domainHint: ${AZURE_TENANT_ID} - scope: - - user.read + additionalScopes: + - Mail.Send ``` The Microsoft provider is a structure with three mandatory configuration keys: diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index 91fffc2361..df63e9ccf4 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -28,7 +28,7 @@ export interface Config { clientSecret: string; domainHint?: string; callbackUrl?: string; - scope?: string[]; + additionalScopes?: string[]; }; }; }; diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts index 8446881a4d..93058b7eec 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts @@ -21,6 +21,7 @@ import { PassportProfile, } from '@backstage/plugin-auth-node'; import { ExtendedMicrosoftStrategy } from './strategy'; +import { scopeHelper } from './scopes'; /** @public */ export const microsoftAuthenticator = createOAuthAuthenticator({ @@ -31,9 +32,10 @@ export const microsoftAuthenticator = createOAuthAuthenticator({ const clientSecret = config.getString('clientSecret'); const tenantId = config.getString('tenantId'); const domainHint = config.getOptionalString('domainHint'); - const scope: string[] = config.getOptionalStringArray('scope') || [ - 'user.read', - ]; + const scope = scopeHelper( + ['user.read'], + config.getOptionalStringArray('additionalScopes'), + ); const helper = PassportOAuthAuthenticatorHelper.from( new ExtendedMicrosoftStrategy( diff --git a/plugins/auth-backend-module-microsoft-provider/src/scopes.test.ts b/plugins/auth-backend-module-microsoft-provider/src/scopes.test.ts new file mode 100644 index 0000000000..412f87a601 --- /dev/null +++ b/plugins/auth-backend-module-microsoft-provider/src/scopes.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { scopeHelper } from './scopes'; + +describe('microsoftScopeHelper', () => { + it('default scope only with undefined additional scopes', () => { + const scopeResult = scopeHelper(['default']); + expect(scopeResult.length).toBe(1); + expect(scopeResult).toEqual(expect.arrayContaining(['default'])); + }); + + it('default scope only with empty additional scopes', () => { + const scopeResult = scopeHelper(['default'], []); + expect(scopeResult.length).toBe(1); + expect(scopeResult).toEqual(expect.arrayContaining(['default'])); + }); + + it('default scope with mutually exclusive additional scopes', () => { + const scopeResult = scopeHelper(['default'], ['secondScope', 'thirdScope']); + expect(scopeResult.length).toBe(3); + expect(scopeResult).toEqual( + expect.arrayContaining(['default', 'secondScope', 'thirdScope']), + ); + }); + + it('default scope with overlapping additional scopes', () => { + const scopeResult = scopeHelper( + ['default', 'secondScope'], + ['default', 'secondScope', 'thirdScope'], + ); + expect(scopeResult.length).toBe(3); + expect(scopeResult).toEqual( + expect.arrayContaining(['default', 'secondScope', 'thirdScope']), + ); + }); +}); diff --git a/plugins/auth-backend-module-microsoft-provider/src/scopes.ts b/plugins/auth-backend-module-microsoft-provider/src/scopes.ts new file mode 100644 index 0000000000..2165180382 --- /dev/null +++ b/plugins/auth-backend-module-microsoft-provider/src/scopes.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Combine two arrays of scopes without duplicates + * @param defaultScopes Default scopes + * @param additionalScopes Optional additional scopes + * @returns List of scopes + * @public + */ +export const scopeHelper = function ( + defaultScopes: string[], + additionalScopes?: string[], +): string[] { + const scope: string[] = defaultScopes.concat(additionalScopes || []); + return scope.filter((value, index) => scope.indexOf(value) === index); +};