auth-backend-module-microsoft-provider: update scope handling to define required scopes in backend
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-microsoft-provider': patch
|
||||
---
|
||||
|
||||
Updated the Microsoft authenticator to accurately define required scopes, but to also omit the required and additional scopes when requesting resource scopes.
|
||||
@@ -67,8 +67,6 @@ auth:
|
||||
clientSecret: ${AZURE_CLIENT_SECRET}
|
||||
tenantId: ${AZURE_TENANT_ID}
|
||||
domainHint: ${AZURE_TENANT_ID}
|
||||
additionalScopes:
|
||||
- Mail.Send
|
||||
signIn:
|
||||
resolvers:
|
||||
# typically you would pick one of these
|
||||
@@ -86,7 +84,7 @@ The Microsoft provider is a structure with three mandatory configuration keys:
|
||||
Leave blank if your app registration is multi tenant.
|
||||
When specified, this reduces login friction for users with accounts in multiple tenants by automatically filtering away accounts from other tenants.
|
||||
For more details, see [Home Realm Discovery](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy)
|
||||
- `additionalScopes` (optional): List of scopes for the App Registration. The default and mandatory value is ['user.read'].
|
||||
- `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones.
|
||||
|
||||
### Resolvers
|
||||
|
||||
|
||||
@@ -21,21 +21,30 @@ import {
|
||||
PassportProfile,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { ExtendedMicrosoftStrategy } from './strategy';
|
||||
import { union } from 'lodash';
|
||||
|
||||
/** @public */
|
||||
export const microsoftAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: ['email', 'openid', 'offline_access', 'user.read'],
|
||||
transform({ requested, granted, required, additional }) {
|
||||
// Resources scopes are of the form `<resource>/<scope>`, and are handled
|
||||
// separately from the normal scopes in the client. When request a
|
||||
// resource scope we should only include forward the request scope along
|
||||
// with offline_access.
|
||||
const hasResourceScope = Array.from(requested).some(s => s.includes('/'));
|
||||
if (hasResourceScope) {
|
||||
return [...requested, 'offline_access'];
|
||||
}
|
||||
return [...requested, ...granted, ...required, ...additional];
|
||||
},
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const tenantId = config.getString('tenantId');
|
||||
const domainHint = config.getOptionalString('domainHint');
|
||||
const scope = union(
|
||||
['user.read'],
|
||||
config.getOptionalStringArray('additionalScopes'),
|
||||
);
|
||||
|
||||
const helper = PassportOAuthAuthenticatorHelper.from(
|
||||
new ExtendedMicrosoftStrategy(
|
||||
@@ -44,7 +53,6 @@ export const microsoftAuthenticator = createOAuthAuthenticator({
|
||||
clientSecret: clientSecret,
|
||||
callbackURL: callbackUrl,
|
||||
tenant: tenantId,
|
||||
scope: scope,
|
||||
},
|
||||
(
|
||||
accessToken: string,
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('authModuleMicrosoftProvider', () => {
|
||||
expect(startUrl.pathname).toBe('/my-tenant-id/oauth2/v2.0/authorize');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
scope: 'User.Read.All',
|
||||
scope: 'email openid offline_access user.read User.Read.All',
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`,
|
||||
state: expect.any(String),
|
||||
@@ -97,6 +97,7 @@ describe('authModuleMicrosoftProvider', () => {
|
||||
clientSecret: 'another-client-secret',
|
||||
tenantId: 'another-tenant-id',
|
||||
domainHint: 'somedomain',
|
||||
additionalScopes: ['some-extra-scope'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -125,7 +126,7 @@ describe('authModuleMicrosoftProvider', () => {
|
||||
expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
scope: 'user.read',
|
||||
scope: 'email openid offline_access user.read some-extra-scope',
|
||||
client_id: 'another-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`,
|
||||
state: expect.any(String),
|
||||
@@ -137,4 +138,64 @@ describe('authModuleMicrosoftProvider', () => {
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include required scopes for resources', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
authPlugin,
|
||||
authModuleMicrosoftProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
microsoft: {
|
||||
development: {
|
||||
clientId: 'another-client-id',
|
||||
clientSecret: 'another-client-secret',
|
||||
tenantId: 'another-tenant-id',
|
||||
additionalScopes: ['some-extra-scope'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const agent = request.agent(server);
|
||||
|
||||
const res = await agent.get(
|
||||
'/api/auth/microsoft/start?env=development&scope=some-resource/some-scope',
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
|
||||
const nonceCookie = agent.jar.getCookie('microsoft-nonce', {
|
||||
domain: 'localhost',
|
||||
path: '/api/auth/microsoft/handler',
|
||||
script: false,
|
||||
secure: false,
|
||||
});
|
||||
expect(nonceCookie).toBeDefined();
|
||||
|
||||
const startUrl = new URL(res.get('location'));
|
||||
expect(startUrl.origin).toBe('https://login.microsoftonline.com');
|
||||
expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
scope: 'some-resource/some-scope offline_access',
|
||||
client_id: 'another-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`,
|
||||
state: expect.any(String),
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
env: 'development',
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user