Add domain_hint support to Entra ID login
When a user is logged in to multiple microsoft accounts, there can be be a little bit of friction in the Entra login process as users will be asked to select the account to login with. Scenarios in which a user may have multiple microsoft accounts 1. Someone logged in to your work Entra ID account, and a personal microsoft account 2. A consultant who has an Entra ID account at both their employer, as well as the company they're contracted out to. 3. A user has a regular account, as well as one or more high priviliged accounts. When a domain hint is provided, Entra will filter out all the accounts which don't belong to the tenant specified on the `domain_hint`. In many cases, this will filter to a single account, avoiding the need to select an account at all (e.g. scenario 1 & 2). This won't always happen (e.g. scenario 3). Additionally in the case a tenant has been configured to federate authentication elsewhere (e.g. to an on premise AD FS), setting the domain hint means entra can send the user straight to the federated authentication soruce, removing further steps If backstage is allowign authentication from multiple tenants, this field should be left blank. For more details, see https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy 99% of the time, this value should be the same as the tenantId, so we could get rid of hte domain hint, and set it to the same value as the tenant id automatically. We'd need to provide a config option (e.g. `isMultiTenant: true`) to opt out of this. For those edge cases, this would be a breaking change. I decided to go with specifying the `domain_hint` seperatly for now just in case my assumptions are wrong and there are more cases wher ehte `domain_hint` will get in the way. We can always make this the default behaviour later on. Signed-off-by: Alex Crome <afscrome@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-microsoft-provider': minor
|
||||
---
|
||||
|
||||
Added support for specifying a `domain_hint` on Microsoft authentication provider configuration.
|
||||
This should typically be set to the same value as your `tenantId`.
|
||||
If you allow users from multiple tenants to authenticate, then leave this blank.
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
providers:
|
||||
microsoft:
|
||||
development:
|
||||
#...
|
||||
domainHint: ${AZURE_TENANT_ID}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface Config {
|
||||
*/
|
||||
tenantId: string;
|
||||
clientSecret: string;
|
||||
domainHint?: string;
|
||||
callbackUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
PassportProfile,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
let domainHint: string | undefined = undefined;
|
||||
|
||||
/** @public */
|
||||
export const microsoftAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
@@ -30,6 +32,7 @@ export const microsoftAuthenticator = createOAuthAuthenticator({
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const tenantId = config.getString('tenantId');
|
||||
domainHint = config.getOptionalString('domainHint');
|
||||
|
||||
return PassportOAuthAuthenticatorHelper.from(
|
||||
new MicrosoftStrategy(
|
||||
@@ -58,9 +61,15 @@ export const microsoftAuthenticator = createOAuthAuthenticator({
|
||||
},
|
||||
|
||||
async start(input, helper) {
|
||||
return helper.start(input, {
|
||||
const options: Record<string, string> = {
|
||||
accessType: 'offline',
|
||||
});
|
||||
};
|
||||
|
||||
if (domainHint !== undefined) {
|
||||
options.domain_hint = domainHint;
|
||||
}
|
||||
|
||||
return helper.start(input, options);
|
||||
},
|
||||
|
||||
async authenticate(input, helper) {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user