From daa02d6e642e6ff8e0cccb9518755cf623158670 Mon Sep 17 00:00:00 2001 From: Jordan Slott Date: Fri, 27 Sep 2024 16:16:30 -0400 Subject: [PATCH 1/3] Fixes #26503 Add skipUserProfile flag to Microsoft authenticator Signed-off-by: Jordan Slott --- .changeset/heavy-ties-tell.md | 5 +++ docs/auth/microsoft/provider.md | 1 + .../config.d.ts | 1 + .../src/authenticator.test.ts | 26 ++++++++++- .../src/authenticator.ts | 43 +++++++++---------- .../src/strategy.ts | 8 +++- 6 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 .changeset/heavy-ties-tell.md diff --git a/.changeset/heavy-ties-tell.md b/.changeset/heavy-ties-tell.md new file mode 100644 index 0000000000..7e7afdb6e7 --- /dev/null +++ b/.changeset/heavy-ties-tell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +--- + +Add skipUserProfile config flag to Microsoft authenticator diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index eb4fc833fd..5285aea3cd 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -86,6 +86,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: 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, to be requested in addition to the required ones. +- `skipUserProfile` (optional): If true, skips loading the user profile even if the `User.Read` scope is present. This is a performance optmization during login and can be used with resolvers that only needs the email address in `spec.profile.email` obtained when the `email` OAuth2 scope is present. ### Resolvers diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index b1ed2d5766..f8b5f0355d 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -29,6 +29,7 @@ export interface Config { domainHint?: string; callbackUrl?: string; additionalScopes?: string | string[]; + skipUserProfile?: boolean; signIn?: { resolvers: Array< | { resolver: 'emailMatchingUserEntityAnnotation' } diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts index 74a7ab9974..0f39aed526 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts @@ -262,7 +262,7 @@ describe('microsoftAuthenticator', () => { expect(profile.photos).toStrictEqual([{ value: photo }]); }); - it('returns access token for non-microsoft graph scope', async () => { + it('returns access token for non-microsoft graph scope', async () => { const foreignScope = 'aks-audience/user.read'; const refreshResponse = await microsoftAuthenticator.refresh( createRefreshRequest(foreignScope), @@ -274,5 +274,29 @@ describe('microsoftAuthenticator', () => { microsoftApi.generateAccessToken(foreignScope), ); }); + + it('returns access token when skipping user profile load', async () => { + // Replace implementation to set skipUserProfile config + implementation = microsoftAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + additionalScopes: ['User.Read.All'], + skipUserProfile: true, + }), + }); + + const refreshResponse = await microsoftAuthenticator.refresh( + createRefreshRequest(scope), + implementation, + ); + + expect(refreshResponse.fullProfile).toBeUndefined(); + expect(refreshResponse.session.accessToken).toBe( + microsoftApi.generateAccessToken(scope), + ); + }); }); }); diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts index fe7dc8f7e1..5767abaaf7 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts @@ -45,31 +45,30 @@ export const microsoftAuthenticator = createOAuthAuthenticator({ const clientSecret = config.getString('clientSecret'); const tenantId = config.getString('tenantId'); const domainHint = config.getOptionalString('domainHint'); + const skipUserProfile = + config.getOptionalBoolean('skipUserProfile') ?? false; - const helper = PassportOAuthAuthenticatorHelper.from( - new ExtendedMicrosoftStrategy( - { - clientID: clientId, - clientSecret: clientSecret, - callbackURL: callbackUrl, - tenant: tenantId, - }, - ( - accessToken: string, - refreshToken: string, - params: any, - fullProfile: PassportProfile, - done: PassportOAuthDoneCallback, - ) => { - done( - undefined, - { fullProfile, params, accessToken }, - { refreshToken }, - ); - }, - ), + const strategy = new ExtendedMicrosoftStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + tenant: tenantId, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, ); + strategy.setSkipUserProfile(skipUserProfile); + const helper = PassportOAuthAuthenticatorHelper.from(strategy); + return { helper, domainHint, diff --git a/plugins/auth-backend-module-microsoft-provider/src/strategy.ts b/plugins/auth-backend-module-microsoft-provider/src/strategy.ts index d5a1aa31fe..910bd85d1d 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/strategy.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/strategy.ts @@ -20,6 +20,12 @@ import fetch from 'node-fetch'; import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; export class ExtendedMicrosoftStrategy extends MicrosoftStrategy { + private shouldSkipUserProfile = false; + + public setSkipUserProfile(shouldSkipUserProfile: boolean): void { + this.shouldSkipUserProfile = shouldSkipUserProfile; + } + userProfile( accessToken: string, done: (err?: unknown, profile?: PassportProfile) => void, @@ -66,7 +72,7 @@ export class ExtendedMicrosoftStrategy extends MicrosoftStrategy { private skipUserProfile(accessToken: string): boolean { try { - return !this.hasGraphReadScope(accessToken); + return this.shouldSkipUserProfile || !this.hasGraphReadScope(accessToken); } catch { // If there is any error with checking the scope // we fall back to not skipping the user profile From 77e741da065ba44f2de168cec9bdf2c369c08839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 2 Oct 2024 12:28:47 +0200 Subject: [PATCH 2/3] Update docs/auth/microsoft/provider.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/auth/microsoft/provider.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index 5285aea3cd..f20e17ea6c 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -86,7 +86,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: 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, to be requested in addition to the required ones. -- `skipUserProfile` (optional): If true, skips loading the user profile even if the `User.Read` scope is present. This is a performance optmization during login and can be used with resolvers that only needs the email address in `spec.profile.email` obtained when the `email` OAuth2 scope is present. +- `skipUserProfile` (optional): If true, skips loading the user profile even if the `User.Read` scope is present. This is a performance optimization during login and can be used with resolvers that only needs the email address in `spec.profile.email` obtained when the `email` OAuth2 scope is present. ### Resolvers From 6b43d475fdd9e6f064aca72d609ae8062585d22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 2 Oct 2024 12:28:52 +0200 Subject: [PATCH 3/3] Update .changeset/heavy-ties-tell.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/heavy-ties-tell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/heavy-ties-tell.md b/.changeset/heavy-ties-tell.md index 7e7afdb6e7..072b0c7860 100644 --- a/.changeset/heavy-ties-tell.md +++ b/.changeset/heavy-ties-tell.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend-module-microsoft-provider': patch --- -Add skipUserProfile config flag to Microsoft authenticator +Add `skipUserProfile` config flag to Microsoft authenticator