Merge pull request #20694 from afscrome/entra-domain-hint

Add `domain_hint` support to Entra ID login
This commit is contained in:
Fredrik Adelöw
2023-10-26 17:07:24 +02:00
committed by GitHub
6 changed files with 96 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-microsoft-provider': patch
---
Added support for specifying a domain hint on the Microsoft authentication provider configuration.
+6 -1
View File
@@ -38,13 +38,18 @@ auth:
clientId: ${AUTH_MICROSOFT_CLIENT_ID}
clientSecret: ${AUTH_MICROSOFT_CLIENT_SECRET}
tenantId: ${AUTH_MICROSOFT_TENANT_ID}
domainHint: ${AZURE_TENANT_ID}
```
The Microsoft provider is a structure with three configuration keys:
The Microsoft provider is a structure with three mandatory configuration keys:
- `clientId`: Application (client) ID, found on App Registration > Overview
- `clientSecret`: Secret, found on App Registration > Certificates & secrets
- `tenantId`: Directory (tenant) ID, found on App Registration > Overview
- `domainHint` (optional): Typically the same as `tenantId`.
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)
## Outbound Network Access
@@ -15,7 +15,10 @@ export const authModuleMicrosoftProvider: () => BackendFeature;
// @public (undocumented)
export const microsoftAuthenticator: OAuthAuthenticator<
PassportOAuthAuthenticatorHelper,
{
helper: PassportOAuthAuthenticatorHelper;
domainHint: string | undefined;
},
PassportProfile
>;
@@ -26,6 +26,7 @@ export interface Config {
* @visibility secret
*/
clientSecret: string;
domainHint?: string;
callbackUrl?: string;
};
};
@@ -30,8 +30,9 @@ export const microsoftAuthenticator = createOAuthAuthenticator({
const clientId = config.getString('clientId');
const clientSecret = config.getString('clientSecret');
const tenantId = config.getString('tenantId');
const domainHint = config.getOptionalString('domainHint');
return PassportOAuthAuthenticatorHelper.from(
const helper = PassportOAuthAuthenticatorHelper.from(
new MicrosoftStrategy(
{
clientID: clientId,
@@ -55,19 +56,30 @@ export const microsoftAuthenticator = createOAuthAuthenticator({
},
),
);
return {
helper,
domainHint,
};
},
async start(input, helper) {
return helper.start(input, {
async start(input, ctx) {
const options: Record<string, string> = {
accessType: 'offline',
});
};
if (ctx.domainHint !== undefined) {
options.domain_hint = ctx.domainHint;
}
return ctx.helper.start(input, options);
},
async authenticate(input, helper) {
return helper.authenticate(input);
async authenticate(input, ctx) {
return ctx.helper.authenticate(input);
},
async refresh(input, helper) {
return helper.refresh(input);
async refresh(input, ctx) {
return ctx.helper.refresh(input);
},
});
@@ -21,7 +21,7 @@ import request from 'supertest';
import { authModuleMicrosoftProvider } from './module';
describe('authModuleMicrosoftProvider', () => {
it('should start', async () => {
it('should start without domain hint', async () => {
const { server } = await startTestBackend({
features: [
authPlugin,
@@ -77,4 +77,63 @@ describe('authModuleMicrosoftProvider', () => {
nonce: decodeURIComponent(nonceCookie.value),
});
});
it('should start with domain hint', 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',
domainHint: 'somedomain',
},
},
},
},
},
}),
],
});
const agent = request.agent(server);
const res = await agent.get('/api/auth/microsoft/start?env=development');
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: 'user.read',
client_id: 'another-client-id',
redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`,
state: expect.any(String),
domain_hint: 'somedomain',
});
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
env: 'development',
nonce: decodeURIComponent(nonceCookie.value),
});
});
});