From 581ce7f4d4d998974fbffb405b0c432d18fda034 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Mar 2022 15:46:59 +0100 Subject: [PATCH 01/49] auth-backend: remove all default sign-in resolvers Signed-off-by: Patrik Oldsberg --- .../src/providers/auth0/provider.ts | 14 +---- .../src/providers/github/provider.ts | 35 +----------- .../src/providers/gitlab/provider.ts | 40 +------------ .../src/providers/google/provider.ts | 57 +------------------ .../src/providers/microsoft/provider.ts | 43 +------------- .../src/providers/oauth2/provider.ts | 44 +------------- .../src/providers/oidc/provider.ts | 42 +------------- .../src/providers/okta/provider.ts | 45 +-------------- .../src/providers/onelogin/provider.ts | 16 +----- .../src/providers/saml/provider.ts | 34 +---------- 10 files changed, 10 insertions(+), 360 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index fbd57c0b76..cfe4110b4a 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -180,18 +180,6 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async info => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile does not contain an email'); - } - - const id = profile.email.split('@')[0]; - - return { id, token: '' }; -}; - /** @public */ export type Auth0ProviderOptions = { /** @@ -244,7 +232,7 @@ export const createAuth0Provider = ( profile: makeProfileInfo(fullProfile, params.id_token), }); - const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; + const signInResolver = options?.signIn?.resolver; const provider = new Auth0AuthProvider({ clientId, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index c875f856b6..8462fa1707 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -244,29 +244,6 @@ export class GithubAuthProvider implements OAuthHandlers { } } -export const githubDefaultSignInResolver: SignInResolver< - GithubOAuthResult -> = async (info, ctx) => { - const { fullProfile } = info.result; - - const userId = fullProfile.username || fullProfile.id; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - export type GithubProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -346,16 +323,6 @@ export const createGithubProvider = ( profile: makeProfileInfo(fullProfile), }); - const signInResolverFn = - options?.signIn?.resolver ?? githubDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - const stateEncoder: StateEncoder = options?.stateEncoder ?? (async (req: OAuthStartRequest): Promise<{ encodedState: string }> => { @@ -369,7 +336,7 @@ export const createGithubProvider = ( tokenUrl, userProfileUrl, authorizationUrl, - signInResolver, + signInResolver: options?.signIn?.resolver, authHandler, tokenIssuer, catalogIdentityClient, diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 59f9a5a4cb..460139562c 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -62,34 +62,6 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { logger: Logger; }; -export const gitlabDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile, result } = info; - - let id = result.fullProfile.id; - - if (profile.email) { - id = profile.email.split('@')[0]; - } - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id, token }; -}; - export const gitlabDefaultAuthHandler: AuthHandler = async ({ fullProfile, params, @@ -261,23 +233,13 @@ export const createGitlabProvider = ( const authHandler: AuthHandler = options?.authHandler ?? gitlabDefaultAuthHandler; - const signInResolverFn = - options?.signIn?.resolver ?? gitlabDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - const provider = new GitlabAuthProvider({ clientId, clientSecret, callbackUrl, baseUrl, authHandler, - signInResolver, + signInResolver: options?.signIn?.resolver, catalogIdentityClient, logger, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index e5e5698ec4..14328c9508 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; @@ -205,47 +201,6 @@ export const googleEmailSignInResolver: SignInResolver = async ( return { id: entity.metadata.name, entity, token }; }; -const googleDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - let userId: string; - try { - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - userId = entity.metadata.name; - } catch (error) { - ctx.logger.warn( - `Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`, - ); - userId = profile.email.split('@')[0]; - } - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - export type GoogleProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -295,21 +250,11 @@ export const createGoogleProvider = ( profile: makeProfileInfo(fullProfile, params.id_token), }); - const signInResolverFn = - options?.signIn?.resolver ?? googleDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - const provider = new GoogleAuthProvider({ clientId, clientSecret, callbackUrl, - signInResolver, + signInResolver: options?.signIn?.resolver, authHandler, tokenIssuer, catalogIdentityClient, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 98f94811bd..4888ca22b4 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; @@ -224,33 +220,6 @@ export const microsoftEmailSignInResolver: SignInResolver = async ( return { id: entity.metadata.name, entity, token }; }; -export const microsoftDefaultSignInResolver: SignInResolver< - OAuthResult -> = async (info, ctx) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile contained no email'); - } - - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - export type MicrosoftProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -304,16 +273,6 @@ export const createMicrosoftProvider = ( profile: makeProfileInfo(fullProfile, params.id_token), }); - const signInResolverFn = - options?.signIn?.resolver ?? microsoftDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - const provider = new MicrosoftAuthProvider({ clientId, clientSecret, @@ -321,7 +280,7 @@ export const createMicrosoftProvider = ( authorizationUrl, tokenUrl, authHandler, - signInResolver, + signInResolver: options?.signIn?.resolver, catalogIdentityClient, logger, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index d09337f5ad..c91ec688c3 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; @@ -202,34 +198,6 @@ export class OAuth2AuthProvider implements OAuthHandlers { } } -export const oAuth2DefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile contained no email'); - } - - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - export type OAuth2ProviderOptions = { authHandler?: AuthHandler; @@ -275,23 +243,13 @@ export const createOAuth2Provider = ( profile: makeProfileInfo(fullProfile, params.id_token), }); - const signInResolverFn = - options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - const provider = new OAuth2AuthProvider({ clientId, clientSecret, tokenIssuer, catalogIdentityClient, callbackUrl, - signInResolver, + signInResolver: options?.signIn?.resolver, authHandler, authorizationUrl, tokenUrl, diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index b4f34cd127..f79f726824 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { Client, @@ -215,34 +211,6 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const oidcDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile contained no email'); - } - - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - /** * OIDC provider callback options. An auth handler and a sign in resolver * can be passed while creating a OIDC provider. @@ -302,14 +270,6 @@ export const createOidcProvider = ( picture: userinfo.picture, }, }); - const signInResolverFn = - options?.signIn?.resolver ?? oidcDefaultSignInResolver; - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); const provider = new OidcAuthProvider({ clientId, @@ -319,7 +279,7 @@ export const createOidcProvider = ( metadataUrl, scope, prompt, - signInResolver, + signInResolver: options?.signIn?.resolver, authHandler, logger, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 26f586c578..60c2666200 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { OAuthAdapter, @@ -227,35 +223,6 @@ export const oktaEmailSignInResolver: SignInResolver = async ( return { id: entity.metadata.name, entity, token }; }; -export const oktaDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Okta profile contained no email'); - } - - // TODO(Rugvip): Hardcoded to the local part of the email for now - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - export type OktaProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -313,23 +280,13 @@ export const createOktaProvider = ( profile: makeProfileInfo(fullProfile, params.id_token), }); - const signInResolverFn = - _options?.signIn?.resolver ?? oktaDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - const provider = new OktaAuthProvider({ audience, clientId, clientSecret, callbackUrl, authHandler, - signInResolver, + signInResolver: _options?.signIn?.resolver, tokenIssuer, catalogIdentityClient, logger, diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index e14e8548a0..8e354aeb60 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -179,18 +179,6 @@ export class OneLoginProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async info => { - const { profile } = info; - - if (!profile.email) { - throw new Error('OIDC profile contained no email'); - } - - const id = profile.email.split('@')[0]; - - return { id, token: '' }; -}; - /** @public */ export type OneLoginProviderOptions = { /** @@ -243,15 +231,13 @@ export const createOneLoginProvider = ( profile: makeProfileInfo(fullProfile, params.id_token), }); - const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; - const provider = new OneLoginProvider({ clientId, clientSecret, callbackUrl, issuer, authHandler, - signInResolver, + signInResolver: options?.signIn?.resolver, tokenIssuer, catalogIdentityClient, logger, diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 4458d423ef..fd0e5606f4 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -148,28 +148,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } -const samlDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const id = info.result.fullProfile.nameID; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id, token }; -}; - type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; /** @public */ @@ -218,16 +196,6 @@ export const createSamlProvider = ( }, }); - const signInResolverFn = - options?.signIn?.resolver ?? samlDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, - }); - return new SamlAuthProvider({ callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, entryPoint: config.getString('entryPoint'), @@ -248,7 +216,7 @@ export const createSamlProvider = ( tokenIssuer, appUrl: globalConfig.appUrl, authHandler, - signInResolver, + signInResolver: options?.signIn?.resolver, logger, catalogIdentityClient, }); From e13f387ed05013a3ae7a04a883b126bcace77a73 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Mar 2022 15:47:47 +0100 Subject: [PATCH 02/49] auth-backend: make resolver required in all sign-in options Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/github/provider.ts | 2 +- plugins/auth-backend/src/providers/gitlab/provider.ts | 2 +- plugins/auth-backend/src/providers/google/provider.ts | 2 +- plugins/auth-backend/src/providers/microsoft/provider.ts | 2 +- plugins/auth-backend/src/providers/oauth2/provider.ts | 2 +- plugins/auth-backend/src/providers/oidc/provider.ts | 2 +- plugins/auth-backend/src/providers/okta/provider.ts | 2 +- plugins/auth-backend/src/providers/saml/provider.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 8462fa1707..fb61a3ae97 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -258,7 +258,7 @@ export type GithubProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; /** diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 460139562c..b16757470b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -199,7 +199,7 @@ export type GitlabProviderOptions = { * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. */ signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 14328c9508..7e5a998785 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -215,7 +215,7 @@ export type GoogleProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 4888ca22b4..4bc16fdbef 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -234,7 +234,7 @@ export type MicrosoftProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index c91ec688c3..efe7dbe82b 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -202,7 +202,7 @@ export type OAuth2ProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index f79f726824..f90adb2922 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -227,7 +227,7 @@ export type OidcProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 60c2666200..36f1e00ee2 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -237,7 +237,7 @@ export type OktaProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index fd0e5606f4..c7b0896b49 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -165,7 +165,7 @@ export type SamlProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; From ebcd28073aba9dbb6854ed0a5d72381911c1f8a1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Mar 2022 16:22:41 +0100 Subject: [PATCH 03/49] auth-backend: reintroduce behavior of some default sign-in resolvers Signed-off-by: Patrik Oldsberg --- .../src/providers/github/index.ts | 5 ++- .../src/providers/github/provider.test.ts | 41 +++++++++++++++---- .../src/providers/github/provider.ts | 29 +++++++++++++ .../src/providers/gitlab/provider.test.ts | 17 +++++--- .../src/providers/gitlab/provider.ts | 29 +++++++++++++ .../auth-backend/src/providers/saml/index.ts | 5 ++- .../src/providers/saml/provider.ts | 24 +++++++++++ 7 files changed, 136 insertions(+), 14 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index a855b13b83..3b0c605f0e 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { createGithubProvider } from './provider'; +export { + createGithubProvider, + githubUsernameEntityNameSignInResolver, +} from './provider'; export type { GithubOAuthResult, GithubProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 206df52083..37dd4115ed 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -21,7 +21,7 @@ import { CatalogIdentityClient } from '../../lib/catalog'; import { GithubAuthProvider, GithubOAuthResult, - githubDefaultSignInResolver, + githubUsernameEntityNameSignInResolver, } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; @@ -46,14 +46,18 @@ describe('GithubAuthProvider', () => { }; const catalogIdentityClient = { findUser: jest.fn(), - }; + resolveCatalogMembership: async ({ + entityRefs, + }: { + entityRefs: string[]; + }) => entityRefs, + } as unknown as CatalogIdentityClient; const provider = new GithubAuthProvider({ logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, + catalogIdentityClient: catalogIdentityClient, tokenIssuer: tokenIssuer as unknown as TokenIssuer, - signInResolver: githubDefaultSignInResolver, + signInResolver: githubUsernameEntityNameSignInResolver, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -254,6 +258,7 @@ describe('GithubAuthProvider', () => { result: { fullProfile: { id: 'ipd12039', + username: 'daveboyle', provider: 'github', displayName: 'Dave Boyle', }, @@ -271,8 +276,8 @@ describe('GithubAuthProvider', () => { expect(response).toEqual({ response: { backstageIdentity: { - id: 'ipd12039', - token: 'token-for-user:default/ipd12039', + id: 'daveboyle', + token: 'token-for-user:default/daveboyle', }, providerInfo: { accessToken: 'a.b.c', @@ -287,6 +292,28 @@ describe('GithubAuthProvider', () => { }); }); + it('should fail if username is not available', async () => { + mockFrameHandler.mockResolvedValueOnce({ + result: { + fullProfile: { + id: 'ipd12039', + provider: 'github', + displayName: 'Dave Boyle', + }, + accessToken: 'a.b.c', + params: { + scope: 'read:user', + expires_in: '123', + }, + }, + privateInfo: { refreshToken: 'refresh-me' }, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + 'GitHub user profile does not contain a username', + ); + }); + it('should forward a new refresh token on refresh', async () => { const mockRefreshToken = jest.spyOn( helpers, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index fb61a3ae97..e57722d611 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -244,6 +244,35 @@ export class GithubAuthProvider implements OAuthHandlers { } } +export const githubUsernameEntityNameSignInResolver: SignInResolver< + GithubOAuthResult +> = async (info, ctx) => { + const { fullProfile } = info.result; + + const userId = fullProfile.username; + if (!userId) { + throw new Error(`GitHub user profile does not contain a username`); + } + + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const ownershipEntityRefs = + await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [entityRef], + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { + sub: entityRef, + ent: ownershipEntityRefs, + }, + }); + + return { id: userId, token }; +}; + export type GithubProviderOptions = { /** * The profile transformation function used to verify and convert the auth response diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index f90de3c75b..f51ba58365 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { GitlabAuthProvider, gitlabDefaultSignInResolver } from './provider'; +import { + GitlabAuthProvider, + gitlabUsernameEntityNameSignInResolver, +} from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { PassportProfile } from '../../lib/passport/types'; import { OAuthResult } from '../../lib/oauth'; @@ -34,15 +37,19 @@ describe('GitlabAuthProvider', () => { }; const catalogIdentityClient = { findUser: jest.fn(), - }; + resolveCatalogMembership: async ({ + entityRefs, + }: { + entityRefs: string[]; + }) => entityRefs, + } as unknown as CatalogIdentityClient; const provider = new GitlabAuthProvider({ clientId: 'mock', clientSecret: 'mock', callbackUrl: 'mock', baseUrl: 'mock', - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, + catalogIdentityClient, tokenIssuer: tokenIssuer as unknown as TokenIssuer, authHandler: async ({ fullProfile }) => ({ profile: { @@ -51,7 +58,7 @@ describe('GitlabAuthProvider', () => { picture: 'http://gitlab.com/lols', }, }), - signInResolver: gitlabDefaultSignInResolver, + signInResolver: gitlabUsernameEntityNameSignInResolver, logger: getVoidLogger(), }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index b16757470b..91a9b5d8f6 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -62,6 +62,35 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { logger: Logger; }; +export const gitlabUsernameEntityNameSignInResolver: SignInResolver< + OAuthResult +> = async (info, ctx) => { + const { result } = info; + + const id = result.fullProfile.username; + if (!id) { + throw new Error(`GitLab user profile does not contain a username`); + } + + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); + const ownershipEntityRefs = + await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [entityRef], + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { + sub: entityRef, + ent: ownershipEntityRefs, + }, + }); + + return { id, token }; +}; + export const gitlabDefaultAuthHandler: AuthHandler = async ({ fullProfile, params, diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts index 0aea660941..8d36e17d29 100644 --- a/plugins/auth-backend/src/providers/saml/index.ts +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { createSamlProvider } from './provider'; +export { + createSamlProvider, + samlNameIdEntityNameSignInResolver, +} from './provider'; export type { SamlProviderOptions, SamlAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index c7b0896b49..c3b4fc59e8 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -148,6 +148,30 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } +export const samlNameIdEntityNameSignInResolver: SignInResolver< + SamlAuthResult +> = async (info, ctx) => { + const id = info.result.fullProfile.nameID; + + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); + const ownershipEntityRefs = + await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [entityRef], + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { + sub: entityRef, + ent: ownershipEntityRefs, + }, + }); + + return { id, token }; +}; + type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; /** @public */ From d1b3a1e8056682abfadfc55aa51007e5bd00513a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Mar 2022 16:23:14 +0100 Subject: [PATCH 04/49] auth-backend: added common email local-part sign-in resolver Signed-off-by: Patrik Oldsberg --- .../auth-backend/src/providers/resolvers.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 plugins/auth-backend/src/providers/resolvers.ts diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts new file mode 100644 index 0000000000..ee998c1c90 --- /dev/null +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2022 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 { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { SignInResolver } from './types'; + +export const commonEmailLocalPartEntityNameSignInResolver: SignInResolver< + unknown +> = async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Login failed, user profile does not contain an email'); + } + const [userId] = profile.email.split('@'); + + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const ownershipEntityRefs = + await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [entityRef], + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { + sub: entityRef, + ent: ownershipEntityRefs, + }, + }); + + return { id: userId, token }; +}; From a020f4a720bafb81437a182d03fa73974a54b9d4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Mar 2022 21:59:32 +0100 Subject: [PATCH 05/49] auth-backend: new pattern for exporting auth providers and resolvers, applied to google provider Signed-off-by: Patrik Oldsberg --- .../createAuthProviderIntegration.ts | 42 +++++ .../src/providers/google/provider.ts | 173 ++++++++++-------- plugins/auth-backend/src/providers/index.ts | 2 + .../auth-backend/src/providers/providers.ts | 21 +++ .../auth-backend/src/providers/resolvers.ts | 7 +- 5 files changed, 169 insertions(+), 76 deletions(-) create mode 100644 plugins/auth-backend/src/providers/createAuthProviderIntegration.ts create mode 100644 plugins/auth-backend/src/providers/providers.ts diff --git a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts new file mode 100644 index 0000000000..8b58e89062 --- /dev/null +++ b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 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 { AuthProviderFactory, SignInResolver } from './types'; + +/** + * Creates a standardized representation of an integration with a third-party + * auth provider. + * + * The returned object facilitates the creation of provider instances, and + * supplies built-in sign-in resolvers for the specific provider. + */ +export function createAuthProviderIntegration< + TCreateOptions extends unknown[], + TResolvers extends { + [name in string]: (...args: any[]) => SignInResolver; + }, +>(config: { + create: (...args: TCreateOptions) => AuthProviderFactory; + resolvers: TResolvers; +}): Readonly<{ + create: (...args: TCreateOptions) => AuthProviderFactory; + resolvers: Readonly; +}> { + return Object.freeze({ + ...config, + resolvers: Object.freeze(config.resolvers), + }); +} diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 7e5a998785..68c505cc43 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -38,13 +38,10 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { - AuthProviderFactory, - AuthHandler, - RedirectInfo, - SignInResolver, -} from '../types'; +import { AuthHandler, RedirectInfo, SignInResolver } from '../types'; import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { commonByEmailLocalPartResolver } from '../resolvers'; type PrivateInfo = { refreshToken: string; @@ -179,28 +176,9 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -export const googleEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GoogleProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -219,53 +197,102 @@ export type GoogleProviderOptions = { }; }; -export const createGoogleProvider = ( - options?: GoogleProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export const google = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + tokenManager, + catalogApi, + logger, + }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenManager, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + tokenIssuer, + catalogIdentityClient, + logger, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + callbackUrl, + }); }); + }, + resolvers: { + byEmailLocalPart: () => commonByEmailLocalPartResolver, + lookupEmailAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + if (!profile.email) { + throw new Error('Google profile contained no email'); + } - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - logger, - }); + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'google.com/email': profile.email, + }, + }); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; + }, + }, +}); + +/** + * @deprecated Use `providers.google.create` instead. + */ +export const createGoogleProvider = google.create; + +/** + * @deprecated Use `google.resolvers.lookupEmailAnnotation` instead. + */ +export const googleEmailSignInResolver = google.resolvers.lookupEmailAnnotation; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 78814ae7e6..9650632622 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -30,6 +30,8 @@ export * from './onelogin'; export * from './saml'; export * from './gcp-iap'; +export { providers } from './providers'; + export { factories as defaultAuthProviderFactories } from './factories'; // Export the minimal interface required for implementing a diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts new file mode 100644 index 0000000000..ffe833ff47 --- /dev/null +++ b/plugins/auth-backend/src/providers/providers.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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 { google } from './google/provider'; + +export const providers = Object.freeze({ + google, +}); diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts index ee998c1c90..dd9a12f90a 100644 --- a/plugins/auth-backend/src/providers/resolvers.ts +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -20,9 +20,10 @@ import { } from '@backstage/catalog-model'; import { SignInResolver } from './types'; -export const commonEmailLocalPartEntityNameSignInResolver: SignInResolver< - unknown -> = async (info, ctx) => { +export const commonByEmailLocalPartResolver: SignInResolver = async ( + info, + ctx, +) => { const { profile } = info; if (!profile.email) { From 30f82a32078060e59d643726fd2830700615ff5a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Mar 2022 00:48:32 +0100 Subject: [PATCH 06/49] auth-backend: add provider factory resolverContext and deprecate old fields Signed-off-by: Patrik Oldsberg --- .../src/providers/google/provider.test.ts | 10 +-- .../src/providers/google/provider.ts | 66 ++++++------------- .../providers/oauth2-proxy/provider.test.ts | 15 ++--- .../src/providers/oidc/provider.test.ts | 6 +- plugins/auth-backend/src/providers/types.ts | 23 ++++++- plugins/auth-backend/src/service/router.ts | 11 ++++ 6 files changed, 65 insertions(+), 66 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index a69a7b0485..c31833d561 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -39,10 +39,12 @@ describe('createGoogleProvider', () => { }; const provider = new GoogleAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: { + logger: getVoidLogger(), + catalogIdentityClient: + catalogIdentityClient as unknown as CatalogIdentityClient, + tokenIssuer: tokenIssuer as unknown as TokenIssuer, + }, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 68c505cc43..10187c4d97 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -17,8 +17,7 @@ import express from 'express'; import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; +import { getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -38,8 +37,12 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { AuthHandler, RedirectInfo, SignInResolver } from '../types'; -import { Logger } from 'winston'; +import { + AuthHandler, + AuthResolverContext, + RedirectInfo, + SignInResolver, +} from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { commonByEmailLocalPartResolver } from '../resolvers'; @@ -50,26 +53,20 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class GoogleAuthProvider implements OAuthHandlers { - private readonly _strategy: GoogleStrategy; + private readonly strategy: GoogleStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { - this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this._strategy = new GoogleStrategy( + this.signInResolver = options.signInResolver; + this.resolverContext = options.resolverContext; + this.strategy = new GoogleStrategy( { clientID: options.clientId, clientSecret: options.clientSecret, @@ -102,7 +99,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { + return await executeRedirectStrategy(req, this.strategy, { accessType: 'offline', prompt: 'consent', scope: req.scope, @@ -114,7 +111,7 @@ export class GoogleAuthProvider implements OAuthHandlers { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo - >(req, this._strategy); + >(req, this.strategy); return { response: await this.handleResult(result), @@ -125,12 +122,12 @@ export class GoogleAuthProvider implements OAuthHandlers { async refresh(req: OAuthRefreshRequest) { const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy( - this._strategy, + this.strategy, req.refreshToken, req.scope, ); const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, + this.strategy, accessToken, ); @@ -145,12 +142,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -168,7 +160,7 @@ export class GoogleAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -215,15 +207,7 @@ export const google = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -232,11 +216,6 @@ export const google = createAuthProviderIntegration({ customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({ @@ -249,15 +228,12 @@ export const google = createAuthProviderIntegration({ callbackUrl, signInResolver: options?.signIn?.resolver, authHandler, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts index 61f08bc535..efede36212 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts @@ -24,11 +24,7 @@ jest.mock('@backstage/catalog-client'); import express from 'express'; import { JWT } from 'jose'; import { Logger } from 'winston'; -import { - AuthHandler, - SignInResolver, - AuthProviderFactoryOptions, -} from '../types'; +import { AuthHandler, SignInResolver } from '../types'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity/types'; @@ -191,14 +187,13 @@ describe('Oauth2ProxyAuthProvider', () => { authHandler, signIn: { resolver: signInResolver }, } as Oauth2ProxyProviderOptions; - const factoryOptions = { + + const factory = createOauth2ProxyProvider(providerOptions); + const handler = factory({ logger, catalogApi: {}, tokenIssuer: {}, - } as unknown as AuthProviderFactoryOptions; - - const factory = createOauth2ProxyProvider(providerOptions); - const handler = factory(factoryOptions); + } as any); await handler.refresh!(mockRequest, mockResponse); expect(mockRequest.header).toBeCalledWith(OAUTH2_PROXY_JWT_HEADER); diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index d9902b600e..13fe1ae46b 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -23,7 +23,6 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { OAuthAdapter } from '../../lib/oauth'; -import { AuthProviderFactoryOptions } from '../types'; import { createOidcProvider, OidcAuthProvider, Options } from './provider'; import { getVoidLogger } from '@backstage/backend-common'; @@ -178,14 +177,13 @@ describe('OidcAuthProvider', () => { metadataUrl: 'https://oidc.test/.well-known/openid-configuration', }, } as any); - const options = { + const provider = createOidcProvider()({ globalConfig: { appUrl: 'https://oidc.test', baseUrl: 'https://oidc.test', }, config, - } as AuthProviderFactoryOptions; - const provider = createOidcProvider()(options) as OAuthAdapter; + } as any) as OAuthAdapter; expect(provider.start).toBeDefined(); // Cast provider as any here to be able to inspect private members await (provider as any).handlers.get('testEnv').handlers.implementation; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 5bd52f0c94..ce77a650e3 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -143,6 +143,9 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } +/** + * @deprecated This type is deprecated and will be removed in a future release. + */ export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -154,9 +157,23 @@ export type AuthProviderFactoryOptions = { catalogApi: CatalogApi; }; -export type AuthProviderFactory = ( - options: AuthProviderFactoryOptions, -) => AuthProviderRouteHandlers; +export type AuthProviderFactory = (options: { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + resolverContext: AuthResolverContext; + + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + logger: Logger; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + tokenManager: TokenManager; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + tokenIssuer: TokenIssuer; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + discovery: PluginEndpointDiscovery; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + catalogApi: CatalogApi; +}) => AuthProviderRouteHandlers; export type AuthResponse = { providerInfo: ProviderInfo; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index ec1a52ffc9..42afe56bc0 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -34,6 +34,7 @@ import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import passport from 'passport'; import { Minimatch } from 'minimatch'; +import { CatalogIdentityClient } from '../lib/catalog'; type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -103,6 +104,11 @@ export async function createRouter( const isOriginAllowed = createOriginFilter(config); + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenManager, + }); + for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { @@ -122,6 +128,11 @@ export async function createRouter( tokenIssuer, discovery, catalogApi, + resolverContext: { + logger, + tokenIssuer, + catalogIdentityClient, + }, }); const r = Router(); From 56562c49d6d30bd98ef271d69566786d3fc7d2a3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Mar 2022 09:52:45 +0100 Subject: [PATCH 07/49] backend: add example of setting up sign-in Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/auth.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index da8eedcabc..3b4b2b9450 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createRouter } from '@backstage/plugin-auth-backend'; +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -27,5 +31,13 @@ export default async function createPlugin( database: env.database, discovery: env.discovery, tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, + google: providers.google.create({ + signIn: { + resolver: providers.google.resolvers.byEmailLocalPart(), + }, + }), + }, }); } From 52039f05637a1c80f93b12847acd08287a53e89d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Mar 2022 09:58:12 +0100 Subject: [PATCH 08/49] auth-backend: initial api-report update Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 87 +++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 4227f1db0a..fb58196b2f 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -74,13 +74,21 @@ export type AuthHandlerResult = { // Warning: (ae-missing-release-tag) "AuthProviderFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type AuthProviderFactory = ( - options: AuthProviderFactoryOptions, -) => AuthProviderRouteHandlers; +export type AuthProviderFactory = (options: { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + resolverContext: AuthResolverContext; + logger: Logger; + tokenManager: TokenManager; + tokenIssuer: TokenIssuer; + discovery: PluginEndpointDiscovery; + catalogApi: CatalogApi; +}) => AuthProviderRouteHandlers; // Warning: (ae-missing-release-tag) "AuthProviderFactoryOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -248,9 +256,18 @@ export const createGitlabProvider: ( // Warning: (ae-missing-release-tag) "createGoogleProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const createGoogleProvider: ( - options?: GoogleProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "createMicrosoftProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -369,33 +386,38 @@ export type GithubOAuthResult = { export type GithubProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; stateEncoder?: StateEncoder; }; +// Warning: (ae-missing-release-tag) "githubUsernameEntityNameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const githubUsernameEntityNameSignInResolver: SignInResolver; + // Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type GitlabProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; // Warning: (ae-missing-release-tag) "googleEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const googleEmailSignInResolver: SignInResolver; +// @public @deprecated (undocumented) +export const googleEmailSignInResolver: () => SignInResolver; // Warning: (ae-missing-release-tag) "GoogleProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type GoogleProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -410,7 +432,7 @@ export const microsoftEmailSignInResolver: SignInResolver; export type MicrosoftProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -420,7 +442,7 @@ export type MicrosoftProviderOptions = { export type OAuth2ProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -578,7 +600,7 @@ export type OidcAuthResult = { export type OidcProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -593,7 +615,7 @@ export const oktaEmailSignInResolver: SignInResolver; export type OktaProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -626,6 +648,30 @@ export type ProfileInfo = { picture?: string; }; +// Warning: (ae-missing-release-tag) "providers" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const providers: Readonly<{ + google: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + byEmailLocalPart: () => SignInResolver; + lookupEmailAnnotation(): SignInResolver; + }>; + }>; +}>; + // Warning: (ae-missing-release-tag) "readState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -656,11 +702,16 @@ export type SamlAuthResult = { fullProfile: any; }; +// Warning: (ae-missing-release-tag) "samlNameIdEntityNameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const samlNameIdEntityNameSignInResolver: SignInResolver; + // @public (undocumented) export type SamlProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -709,5 +760,5 @@ export type WebMessageResponse = // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts // src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:118:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:131:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts ``` From f181c8157d47896886458343a1380017195df5e9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Mar 2022 14:16:15 +0100 Subject: [PATCH 09/49] auth-backend: add helper methods in AuthResolverContext + deprecations Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 42 ++++- plugins/auth-backend/src/index.ts | 2 + .../auth-backend/src/lib/catalog/helpers.ts | 3 + .../resolvers/CatalogAuthResolverContext.ts | 176 ++++++++++++++++++ .../auth-backend/src/lib/resolvers/index.ts | 20 ++ .../src/providers/google/provider.test.ts | 19 +- plugins/auth-backend/src/providers/index.ts | 1 + plugins/auth-backend/src/providers/types.ts | 66 ++++++- plugins/auth-backend/src/service/router.ts | 14 +- 9 files changed, 309 insertions(+), 34 deletions(-) create mode 100644 plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts create mode 100644 plugins/auth-backend/src/lib/resolvers/index.ts diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index fb58196b2f..19fa69a6aa 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -9,7 +9,9 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -110,11 +112,40 @@ export interface AuthProviderRouteHandlers { start(req: express.Request, res: express.Response): Promise; } +// Warning: (ae-missing-release-tag) "AuthResolverCatalogUserQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: Exclude; + }; + // @public export type AuthResolverContext = { + logger: Logger; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + issueToken(params: TokenParams): Promise<{ + token: string; + }>; + findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{ + entity: Entity; + }>; + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; }; // Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -360,10 +391,12 @@ export type GcpIapTokenInfo = { [key: string]: JsonValue; }; -// Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts +// @public +export function getDefaultOwnershipEntityRefs(entity: Entity): string[]; + // Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export function getEntityClaims(entity: UserEntity): TokenParams['claims']; // Warning: (ae-missing-release-tag) "GithubOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -760,5 +793,6 @@ export type WebMessageResponse = // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts // src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:131:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:50:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:180:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index d0cade087e..be622e59e9 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -32,3 +32,5 @@ export * from './lib/flow'; export * from './lib/oauth'; export * from './lib/catalog'; + +export { getDefaultOwnershipEntityRefs } from './lib/resolvers'; diff --git a/plugins/auth-backend/src/lib/catalog/helpers.ts b/plugins/auth-backend/src/lib/catalog/helpers.ts index db9b38e2a6..3ff1b5da05 100644 --- a/plugins/auth-backend/src/lib/catalog/helpers.ts +++ b/plugins/auth-backend/src/lib/catalog/helpers.ts @@ -21,6 +21,9 @@ import { } from '@backstage/catalog-model'; import { TokenParams } from '../../identity'; +/** + * @deprecated use {@link getDefaultOwnershipEntityRefs} instead + */ export function getEntityClaims(entity: UserEntity): TokenParams['claims'] { const userRef = stringifyEntityRef(entity); diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts new file mode 100644 index 0000000000..1e2ec1685e --- /dev/null +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2022 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 { TokenManager } from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + DEFAULT_NAMESPACE, + Entity, + parseEntityRef, + RELATION_MEMBER_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; +import { Logger } from 'winston'; +import { TokenIssuer } from '../..'; +import { TokenParams } from '../../identity'; +import { AuthResolverContext } from '../../providers'; +import { AuthResolverCatalogUserQuery } from '../../providers/types'; +import { CatalogIdentityClient } from '../catalog'; + +/** + * Uses the default ownership resolution logic to return an array + * of entity refs that the provided entity claims ownership through. + * + * A reference to the entity itself will also be included in the returned array. + * + * @public + */ +export function getDefaultOwnershipEntityRefs(entity: Entity) { + const membershipRefs = + entity.relations + ?.filter(r => r.type === RELATION_MEMBER_OF) + .map(r => r.targetRef) ?? []; + + return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs])); +} + +/** + * @internal + */ +export class CatalogAuthResolverContext implements AuthResolverContext { + static create(options: { + logger: Logger; + catalogApi: CatalogApi; + tokenIssuer: TokenIssuer; + tokenManager: TokenManager; + }): CatalogAuthResolverContext { + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi: options.catalogApi, + tokenManager: options.tokenManager, + }); + return new CatalogAuthResolverContext( + options.logger, + options.tokenIssuer, + catalogIdentityClient, + options.catalogApi, + options.tokenManager, + ); + } + + private constructor( + public readonly logger: Logger, + public readonly tokenIssuer: TokenIssuer, + public readonly catalogIdentityClient: CatalogIdentityClient, + private readonly catalogApi: CatalogApi, + private readonly tokenManager: TokenManager, + ) {} + + async issueToken(params: TokenParams) { + const token = await this.tokenIssuer.issueToken(params); + return { token }; + } + + async findCatalogUser(query: AuthResolverCatalogUserQuery) { + let result: Entity[] | Entity | undefined = undefined; + const { token } = await this.tokenManager.getToken(); + + if ('entityRef' in query) { + const entityRef = parseEntityRef(query.entityRef, { + defaultKind: 'user', + defaultNamespace: DEFAULT_NAMESPACE, + }); + result = await this.catalogApi.getEntityByRef(entityRef, { token }); + } else if ('annotations' in query) { + const filter: Record = { + kind: 'user', + }; + for (const [key, value] of Object.entries(query.annotations)) { + filter[`metadata.annotations.${key}`] = value; + } + const res = await this.catalogApi.getEntities({ filter }, { token }); + result = res.items; + } else if ('filter' in query) { + const res = await this.catalogApi.getEntities( + { filter: query.filter }, + { token }, + ); + result = res.items; + } else { + throw new InputError('Invalid user lookup query'); + } + + if (Array.isArray(result)) { + if (result.length > 1) { + throw new ConflictError('User lookup resulted in multiple matches'); + } + result = result[0]; + } + if (!result) { + throw new NotFoundError('User not found'); + } + + return { entity: result }; + } + + async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) { + const { entity } = await this.findCatalogUser(query); + const ownershipRefs = getDefaultOwnershipEntityRefs(entity); + + const token = await this.tokenIssuer.issueToken({ + claims: { + sub: stringifyEntityRef(entity), + ent: ownershipRefs, + }, + }); + return { token }; + } + /* + async expandCatalogOwnership(query: { entityRefs: string[] }) { + const { entityRefs } = query; + + const compoundRefs = entityRefs.map(ref => + parseEntityRef(ref.toLocaleLowerCase('en-US'), { + defaultKind: 'user', + defaultNamespace: DEFAULT_NAMESPACE, + }), + ); + const stringRefs = compoundRefs.map(e => stringifyEntityRef(e)); + + const { token } = await this.tokenManager.getToken(); + const { items: entities } = await this.catalogApi.getEntities( + { + filter: compoundRefs.map(ref => ({ + kind: ref.kind, + 'metadata.namespace': ref.namespace, + 'metadata.name': ref.name, + })), + }, + { token }, + ); + + if (compoundRefs.length !== entities.length) { + const found = entities.map(e => stringifyEntityRef(e)); + const missing = stringRefs.filter(ref => !found.includes(ref)); + throw new NotFoundError(`Entities not found for refs ${missing.join()}`); + } + + const memberOf = entities.flatMap(e => getDefaultOwnershipEntityRefs(e)); + + return Array.from(new Set(memberOf)); + } +*/ +} diff --git a/plugins/auth-backend/src/lib/resolvers/index.ts b/plugins/auth-backend/src/lib/resolvers/index.ts new file mode 100644 index 0000000000..c1ca59cb25 --- /dev/null +++ b/plugins/auth-backend/src/lib/resolvers/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 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. + */ + +export { + CatalogAuthResolverContext, + getDefaultOwnershipEntityRefs, +} from './CatalogAuthResolverContext'; diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index c31833d561..2e904c62ad 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -17,9 +17,7 @@ import { GoogleAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -30,21 +28,8 @@ const mockFrameHandler = jest.spyOn( describe('createGoogleProvider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new GoogleAuthProvider({ - resolverContext: { - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, - }, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9650632622..409ac4c4b4 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -41,6 +41,7 @@ export type { AuthProviderFactoryOptions, AuthProviderFactory, AuthHandler, + AuthResolverCatalogUserQuery, AuthResolverContext, AuthHandlerResult, SignInResolver, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index ce77a650e3..d3f6a72aaf 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogApi, GetEntitiesRequest } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { BackstageIdentityResponse, @@ -26,9 +26,40 @@ import { } from '@backstage/plugin-auth-node'; import express from 'express'; import { Logger } from 'winston'; -import { TokenIssuer } from '../identity/types'; +import { TokenIssuer, TokenParams } from '../identity/types'; import { OAuthStartRequest } from '../lib/oauth/types'; import { CatalogIdentityClient } from '../lib/catalog'; +import { Entity } from '@backstage/catalog-model'; + +/** + * A query for a single user in the catalog. + * + * If `entityRef` is used, the default kind is `'User'`. + * + * If `annotations` are used, all annotations must be present and + * match the provided value exactly. Only entities of kind `'User'` will be considered. + * + * If `filter` are used they are passed on as they are to the `CatalogApi`. + * + * Regardless of the query method, the query must match exactly one entity + * in the catalog, or an error will be thrown. + */ +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: Exclude; + }; /** * The context that is used for auth processing. @@ -36,9 +67,36 @@ import { CatalogIdentityClient } from '../lib/catalog'; * @public */ export type AuthResolverContext = { - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; + /** @deprecated Will be removed from the context, access it via a closure instead if needed */ logger: Logger; + /** @deprecated Use the `issueToken` method instead */ + tokenIssuer: TokenIssuer; + /** @deprecated Use the `findCatalogUser` and `signInWithCatalogUser` methods instead, and the `getDefaultOwnershipEntityRefs` helper */ + catalogIdentityClient: CatalogIdentityClient; + + /** + * Issues a Backstage token using the provided parameters. + */ + issueToken(params: TokenParams): Promise<{ token: string }>; + + /** + * Finds a single user in the catalog using the provided query. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + findCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise<{ entity: Entity }>; + + /** + * Finds a single user in the catalog using the provided query, and then + * issues an identity for that user using default ownership resolution. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; }; /** diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 42afe56bc0..80b8a24022 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -34,7 +34,7 @@ import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import passport from 'passport'; import { Minimatch } from 'minimatch'; -import { CatalogIdentityClient } from '../lib/catalog'; +import { CatalogAuthResolverContext } from '../lib/resolvers'; type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -104,11 +104,6 @@ export async function createRouter( const isOriginAllowed = createOriginFilter(config); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { @@ -128,11 +123,12 @@ export async function createRouter( tokenIssuer, discovery, catalogApi, - resolverContext: { + resolverContext: CatalogAuthResolverContext.create({ logger, + catalogApi, tokenIssuer, - catalogIdentityClient, - }, + tokenManager, + }), }); const r = Router(); From 9990055aa3a513ebee65acff04208f23b7194e3d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Mar 2022 14:17:11 +0100 Subject: [PATCH 10/49] backend: add example of custom auth resolver Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/auth.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 3b4b2b9450..39a14139e6 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -35,7 +35,18 @@ export default async function createPlugin( ...defaultAuthProviderFactories, google: providers.google.create({ signIn: { - resolver: providers.google.resolvers.byEmailLocalPart(), + resolver({ profile }, ctx) { + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + return ctx.signInWithCatalogUser({ + entityRef: { + name: profile.email.split('@')[0], + }, + }); + }, }, }), }, From 3c16349463093ab5834e3412e6d71dac67f0a127 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 12:58:03 +0200 Subject: [PATCH 11/49] auth-backend: add missing type exports Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index be622e59e9..d51cb3f1eb 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export * from './service/router'; -export type { TokenIssuer } from './identity'; +export type { TokenIssuer, TokenParams } from './identity'; export * from './providers'; // flow package provides 2 functions diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 409ac4c4b4..c3ecfe8913 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -37,6 +37,7 @@ export { factories as defaultAuthProviderFactories } from './factories'; // Export the minimal interface required for implementing a // custom Authorization Handler export type { + AuthProviderConfig, AuthProviderRouteHandlers, AuthProviderFactoryOptions, AuthProviderFactory, From 29e6a4af63acb87efc81fba47bcfe5eb7a44e575 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:04:23 +0200 Subject: [PATCH 12/49] auth-backend: inline all provider options Signed-off-by: Patrik Oldsberg --- .../src/providers/atlassian/provider.ts | 20 +++++++-- .../src/providers/auth0/provider.ts | 24 +++++++++-- .../src/providers/aws-alb/provider.ts | 23 +++++++++-- .../src/providers/bitbucket/provider.ts | 23 +++++++++-- .../src/providers/gcp-iap/provider.ts | 22 ++++++++-- .../src/providers/gcp-iap/types.ts | 4 +- .../src/providers/github/provider.ts | 41 +++++++++++++++++-- .../src/providers/gitlab/provider.ts | 26 ++++++++++-- .../src/providers/microsoft/provider.ts | 23 +++++++++-- .../src/providers/oauth2-proxy/provider.ts | 23 ++++++++--- .../src/providers/oauth2/provider.ts | 13 ++++-- .../src/providers/oidc/provider.ts | 21 ++++------ .../src/providers/okta/provider.ts | 23 +++++++++-- .../src/providers/onelogin/provider.ts | 24 +++++++++-- .../src/providers/saml/provider.ts | 24 +++++++++-- 15 files changed, 273 insertions(+), 61 deletions(-) diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 26eea420fc..91e18c7e10 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -174,6 +174,9 @@ export class AtlassianAuthProvider implements OAuthHandlers { } } +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type AtlassianProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -189,9 +192,20 @@ export type AtlassianProviderOptions = { }; }; -export const createAtlassianProvider = ( - options?: AtlassianProviderOptions, -): AuthProviderFactory => { +export const createAtlassianProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index cfe4110b4a..8536c97420 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -180,7 +180,9 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -/** @public */ +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type Auth0ProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -200,9 +202,23 @@ export type Auth0ProviderOptions = { }; /** @public */ -export const createAuth0Provider = ( - options?: Auth0ProviderOptions, -): AuthProviderFactory => { +export const createAuth0Provider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 12f7c7f4b4..20b6f6f2ef 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -220,6 +220,9 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } } +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type AwsAlbProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -238,9 +241,23 @@ export type AwsAlbProviderOptions = { }; }; -export const createAwsAlbProvider = ( - options?: AwsAlbProviderOptions, -): AuthProviderFactory => { +export const createAwsAlbProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 1cc0e60bd8..30ea877843 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -247,6 +247,9 @@ export const bitbucketUserIdSignInResolver: SignInResolver< return { id: entity.metadata.name, entity, token }; }; +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type BitbucketProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -265,9 +268,23 @@ export type BitbucketProviderOptions = { }; }; -export const createBitbucketProvider = ( - options?: BitbucketProviderOptions, -): AuthProviderFactory => { +export const createBitbucketProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 7816f68b76..388d1902c4 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -99,9 +99,25 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { * * @public */ -export function createGcpIapProvider( - options: GcpIapProviderOptions, -): AuthProviderFactory { +export function createGcpIapProvider(options: { + /** + * The profile transformation function used to verify and convert the auth + * response into the profile that will be presented to the user. The default + * implementation just provides the authenticated email that the IAP + * presented. + */ + authHandler?: AuthHandler; + + /** + * Configures sign-in for this provider. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory { return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { const audience = config.getString('audience'); diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 9ef1935442..3ef8c049b9 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -71,9 +71,7 @@ export type GcpIapProviderInfo = { export type GcpIapResponse = AuthResponse; /** - * Options for {@link createGcpIapProvider}. - * - * @public + * @deprecated This type has been inlined into the create method and will be removed. */ export type GcpIapProviderOptions = { /** diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index e57722d611..44db24772b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -273,6 +273,9 @@ export const githubUsernameEntityNameSignInResolver: SignInResolver< return { id: userId, token }; }; +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GithubProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -309,9 +312,41 @@ export type GithubProviderOptions = { stateEncoder?: StateEncoder; }; -export const createGithubProvider = ( - options?: GithubProviderOptions, -): AuthProviderFactory => { +export const createGithubProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + + /** + * The state encoder used to encode the 'state' parameter on the OAuth request. + * + * It should return a string that takes the state params (from the request), url encodes the params + * and finally base64 encodes them. + * + * Providing your own stateEncoder will allow you to add addition parameters to the state field. + * + * It is typed as follows: + * `export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>;` + * + * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail + * (These two values will be set by the req.state by default) + * + * For more information, please see the helper module in ../../oauth/helpers #readState + */ + stateEncoder?: StateEncoder; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 91a9b5d8f6..a26313fe95 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -211,6 +211,9 @@ export class GitlabAuthProvider implements OAuthHandlers { } } +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GitlabProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -232,9 +235,26 @@ export type GitlabProviderOptions = { }; }; -export const createGitlabProvider = ( - options?: GitlabProviderOptions, -): AuthProviderFactory => { +export const createGitlabProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + /** + * Maps an auth result to a Backstage identity for the user. + * + * Set to `'email'` to use the default email-based sign in resolver, which will search + * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. + */ + signIn?: { + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 4bc16fdbef..954f7742c4 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -220,6 +220,9 @@ export const microsoftEmailSignInResolver: SignInResolver = async ( return { id: entity.metadata.name, entity, token }; }; +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type MicrosoftProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -238,9 +241,23 @@ export type MicrosoftProviderOptions = { }; }; -export const createMicrosoftProvider = ( - options?: MicrosoftProviderOptions, -): AuthProviderFactory => { +export const createMicrosoftProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 8c4dcc3249..f89f759270 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -51,9 +51,7 @@ export type OAuth2ProxyResult = { }; /** - * Options for the oauth2-proxy provider factory - * - * @public + * @deprecated This type has been inlined into the create method and will be removed. */ export type Oauth2ProxyProviderOptions = { /** @@ -179,9 +177,22 @@ export class Oauth2ProxyAuthProvider * @public */ export const createOauth2ProxyProvider = - ( - options: Oauth2ProxyProviderOptions, - ): AuthProviderFactory => + (options: { + /** + * Configure an auth handler to generate a profile for the user. + */ + authHandler: AuthHandler>; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver>; + }; + }): AuthProviderFactory => ({ catalogApi, logger, tokenIssuer, tokenManager }) => { const signInResolver = options.signIn.resolver; const authHandler = options.authHandler; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index efe7dbe82b..5c5df2a748 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -198,6 +198,9 @@ export class OAuth2AuthProvider implements OAuthHandlers { } } +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type OAuth2ProviderOptions = { authHandler?: AuthHandler; @@ -206,9 +209,13 @@ export type OAuth2ProviderOptions = { }; }; -export const createOAuth2Provider = ( - options?: OAuth2ProviderOptions, -): AuthProviderFactory => { +export const createOAuth2Provider = (options?: { + authHandler?: AuthHandler; + + signIn?: { + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index f90adb2922..82f6df8faf 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -212,16 +212,7 @@ export class OidcAuthProvider implements OAuthHandlers { } /** - * OIDC provider callback options. An auth handler and a sign in resolver - * can be passed while creating a OIDC provider. - * - * authHandler : called after sign in was successful, a new object must be returned which includes a profile - * signInResolver: called after sign in was successful, expects to return a new {@link @backstage/plugin-auth-node#BackstageSignInResult} - * - * Both options are optional. There is fallback for authHandler where the default handler expect an e-mail explicitly - * otherwise it throws an error - * - * @public + * @deprecated This type has been inlined into the create method and will be removed. */ export type OidcProviderOptions = { authHandler?: AuthHandler; @@ -231,9 +222,13 @@ export type OidcProviderOptions = { }; }; -export const createOidcProvider = ( - options?: OidcProviderOptions, -): AuthProviderFactory => { +export const createOidcProvider = (options?: { + authHandler?: AuthHandler; + + signIn?: { + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 36f1e00ee2..74fbe8159c 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -223,6 +223,9 @@ export const oktaEmailSignInResolver: SignInResolver = async ( return { id: entity.metadata.name, entity, token }; }; +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type OktaProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -241,9 +244,23 @@ export type OktaProviderOptions = { }; }; -export const createOktaProvider = ( - _options?: OktaProviderOptions, -): AuthProviderFactory => { +export const createOktaProvider = (_options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 8e354aeb60..526d432b82 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -179,7 +179,9 @@ export class OneLoginProvider implements OAuthHandlers { } } -/** @public */ +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type OneLoginProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -199,9 +201,23 @@ export type OneLoginProviderOptions = { }; /** @public */ -export const createOneLoginProvider = ( - options?: OneLoginProviderOptions, -): AuthProviderFactory => { +export const createOneLoginProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index c3b4fc59e8..bcd77fb7d3 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -174,7 +174,9 @@ export const samlNameIdEntityNameSignInResolver: SignInResolver< type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; -/** @public */ +/** + * @deprecated This type has been inlined into the create method and will be removed. + */ export type SamlProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -194,9 +196,23 @@ export type SamlProviderOptions = { }; /** @public */ -export const createSamlProvider = ( - options?: SamlProviderOptions, -): AuthProviderFactory => { +export const createSamlProvider = (options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}): AuthProviderFactory => { return ({ providerId, globalConfig, From 9c8a2e2116f139c876bfe15269ef462f8c4e617a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:07:25 +0200 Subject: [PATCH 13/49] auth-backend: migrate github provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/github/provider.test.ts | 48 ++++--------- .../src/providers/github/provider.ts | 67 +++---------------- 2 files changed, 22 insertions(+), 93 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 37dd4115ed..e425cef0ff 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -15,9 +15,6 @@ */ import { Profile as PassportProfile } from 'passport'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { GithubAuthProvider, GithubOAuthResult, @@ -26,6 +23,7 @@ import { import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; import { OAuthStartRequest, encodeState } from '../../lib/oauth'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -38,25 +36,12 @@ const mockFrameHandler = jest.spyOn( >; describe('GithubAuthProvider', () => { - const tokenIssuer: TokenIssuer = { - listPublicKeys: jest.fn(), - async issueToken(params) { - return `token-for-${params.claims.sub}`; - }, - }; - const catalogIdentityClient = { - findUser: jest.fn(), - resolveCatalogMembership: async ({ - entityRefs, - }: { - entityRefs: string[]; - }) => entityRefs, - } as unknown as CatalogIdentityClient; - const provider = new GithubAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: catalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: { + signInWithCatalogUser: jest.fn(({ entityRef }) => ({ + token: `token-for-user:${entityRef.name}`, + })), + } as unknown as AuthResolverContext, signInResolver: githubUsernameEntityNameSignInResolver, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), @@ -96,8 +81,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { - id: 'jimmymarkum', - token: 'token-for-user:default/jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -142,8 +126,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { - id: 'jimmymarkum', - token: 'token-for-user:default/jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -186,8 +169,7 @@ describe('GithubAuthProvider', () => { }; const expected = { backstageIdentity: { - id: 'jimmymarkum', - token: 'token-for-user:default/jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -230,8 +212,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { - id: 'daveboyle', - token: 'token-for-user:default/daveboyle', + token: 'token-for-user:daveboyle', }, providerInfo: { accessToken: @@ -276,8 +257,7 @@ describe('GithubAuthProvider', () => { expect(response).toEqual({ response: { backstageIdentity: { - id: 'daveboyle', - token: 'token-for-user:default/daveboyle', + token: 'token-for-user:daveboyle', }, providerInfo: { accessToken: 'a.b.c', @@ -352,8 +332,7 @@ describe('GithubAuthProvider', () => { expect(result).toEqual({ response: { backstageIdentity: { - id: 'mockuser', - token: 'token-for-user:default/mockuser', + token: 'token-for-user:mockuser', }, profile: { displayName: 'Mocked User', @@ -404,8 +383,7 @@ describe('GithubAuthProvider', () => { expect(result).toEqual({ response: { backstageIdentity: { - id: 'mockuser', - token: 'token-for-user:default/mockuser', + token: 'token-for-user:mockuser', }, profile: { displayName: 'Mocked User', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 44db24772b..770715d0ae 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -14,12 +14,7 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; -import { Logger } from 'winston'; import { Profile as PassportProfile } from 'passport'; import { Strategy as GithubStrategy } from 'passport-github2'; import { @@ -36,6 +31,7 @@ import { AuthHandler, SignInResolver, StateEncoder, + AuthResolverContext, } from '../types'; import { OAuthAdapter, @@ -46,8 +42,6 @@ import { encodeState, OAuthRefreshRequest, } from '../../lib/oauth'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; const ACCESS_TOKEN_PREFIX = 'access-token.'; @@ -76,27 +70,21 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; stateEncoder: StateEncoder; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; private readonly stateEncoder: StateEncoder; constructor(options: GithubAuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; this.stateEncoder = options.stateEncoder; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new GithubStrategy( { clientID: options.clientId, @@ -198,12 +186,7 @@ export class GithubAuthProvider implements OAuthHandlers { } private async handleResult(result: GithubOAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const expiresInStr = result.params.expires_in; let expiresInSeconds = @@ -217,7 +200,7 @@ export class GithubAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); // GitHub sessions last longer than Backstage sessions, so if we're using @@ -254,23 +237,7 @@ export const githubUsernameEntityNameSignInResolver: SignInResolver< throw new Error(`GitHub user profile does not contain a username`); } - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - const ownershipEntityRefs = - await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [entityRef], - }); - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: ownershipEntityRefs, - }, - }); - - return { id: userId, token }; + return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); }; /** @@ -347,15 +314,7 @@ export const createGithubProvider = (options?: { */ stateEncoder?: StateEncoder; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -376,11 +335,6 @@ export const createGithubProvider = (options?: { customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile }) => ({ @@ -402,16 +356,13 @@ export const createGithubProvider = (options?: { authorizationUrl, signInResolver: options?.signIn?.resolver, authHandler, - tokenIssuer, - catalogIdentityClient, stateEncoder, - logger, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { persistScopes: true, providerId, - tokenIssuer, callbackUrl, }); }); From 46ab31f7433655b2f160a8d2d591c37ae41f483c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:09:56 +0200 Subject: [PATCH 14/49] auth-backend: migrate atlassian provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/atlassian/provider.test.ts | 17 +------ .../src/providers/atlassian/provider.ts | 45 ++++--------------- 2 files changed, 10 insertions(+), 52 deletions(-) diff --git a/plugins/auth-backend/src/providers/atlassian/provider.test.ts b/plugins/auth-backend/src/providers/atlassian/provider.test.ts index 29241dd17b..c41a18aa22 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.test.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.test.ts @@ -16,11 +16,9 @@ import { AtlassianAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { OAuthResult } from '../../lib/oauth'; import { PassportProfile } from '../../lib/passport/types'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -28,19 +26,8 @@ const mockFrameHandler = jest.spyOn( ) as unknown as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; describe('createAtlassianProvider', () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new AtlassianAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 91e18c7e10..e45b1dfedd 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -38,21 +38,17 @@ import { import { AuthHandler, AuthProviderFactory, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; import express from 'express'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { Logger } from 'winston'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export const atlassianDefaultAuthHandler: AuthHandler = async ({ @@ -66,14 +62,10 @@ export class AtlassianAuthProvider implements OAuthHandlers { private readonly _strategy: AtlassianStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: AtlassianAuthProviderOptions) { - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this.tokenIssuer = options.tokenIssuer; + this.resolverContext = options.resolverContext; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; @@ -120,12 +112,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult): Promise { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -143,7 +130,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -206,15 +193,7 @@ export const createAtlassianProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -224,11 +203,6 @@ export const createAtlassianProvider = (options?: { customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ?? atlassianDefaultAuthHandler; @@ -239,14 +213,11 @@ export const createAtlassianProvider = (options?: { callbackUrl, authHandler, signInResolver: options?.signIn?.resolver, - catalogIdentityClient, - logger, - tokenIssuer, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { providerId, - tokenIssuer, callbackUrl, }); }); From 796e84bc20fe24bda8b214ed5b0b56be5de65a36 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:19:17 +0200 Subject: [PATCH 15/49] auth-backend: migrate auth0 provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/auth0/provider.ts | 45 ++++--------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 8536c97420..185cdd0c79 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -41,10 +41,8 @@ import { AuthProviderFactory, AuthHandler, SignInResolver, + AuthResolverContext, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -54,25 +52,19 @@ export type Auth0AuthProviderOptions = OAuthProviderOptions & { domain: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Auth0AuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new Auth0Strategy( { clientID: options.clientId, @@ -149,12 +141,7 @@ export class Auth0AuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -172,7 +159,7 @@ export class Auth0AuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -219,15 +206,7 @@ export const createAuth0Provider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -237,11 +216,6 @@ export const createAuth0Provider = (options?: { customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({ @@ -257,15 +231,12 @@ export const createAuth0Provider = (options?: { domain, authHandler, signInResolver, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: true, providerId, - tokenIssuer, callbackUrl, }); }); From 42315f5015a9c81b0d15e52032019ea8f6f4a28c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:22:37 +0200 Subject: [PATCH 16/49] auth-backend: migrate aws-alb provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/aws-alb/provider.test.ts | 91 +++++++------------ .../src/providers/aws-alb/provider.ts | 47 +++------- 2 files changed, 45 insertions(+), 93 deletions(-) diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index eb801fdd46..a917858743 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -13,18 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; + import express from 'express'; import { JWT } from 'jose'; - import { ALB_ACCESS_TOKEN_HEADER, ALB_JWT_HEADER, AwsAlbAuthProvider, } from './provider'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { makeProfileInfo } from '../../lib/passport'; +import { AuthResolverContext } from '../types'; +import { AuthenticationError } from '@backstage/errors'; const jwtMock = JWT as jest.Mocked; @@ -66,16 +65,6 @@ beforeEach(() => { }); describe('AwsAlbAuthProvider', () => { - const tokenIssuer: TokenIssuer = { - listPublicKeys: jest.fn(), - async issueToken(params) { - return `token-for-${params.claims.sub}`; - }, - }; - const catalogIdentityClient: CatalogIdentityClient = { - findUser: jest.fn(), - } as unknown as CatalogIdentityClient; - const mockRequest = { header: jest.fn(name => { if (name === ALB_JWT_HEADER) { @@ -115,9 +104,7 @@ describe('AwsAlbAuthProvider', () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -161,9 +148,7 @@ describe('AwsAlbAuthProvider', () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -172,18 +157,16 @@ describe('AwsAlbAuthProvider', () => { }, }); - await provider.refresh(mockRequestWithoutAccessToken, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect( + provider.refresh(mockRequestWithoutAccessToken, mockResponse), + ).rejects.toThrow(AuthenticationError); }); it('JWT is missing', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -192,18 +175,16 @@ describe('AwsAlbAuthProvider', () => { }, }); - await provider.refresh(mockRequestWithoutJwt, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect( + provider.refresh(mockRequestWithoutJwt, mockResponse), + ).rejects.toThrow(AuthenticationError); }); it('JWT is invalid', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -216,18 +197,16 @@ describe('AwsAlbAuthProvider', () => { throw new Error('bad JWT'); }); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('issuer is missing', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -238,17 +217,16 @@ describe('AwsAlbAuthProvider', () => { jwtMock.verify.mockReturnValueOnce({}); - await provider.refresh(mockRequest, mockResponse); - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('issuer is invalid', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -261,17 +239,16 @@ describe('AwsAlbAuthProvider', () => { iss: 'INVALID_ISSUE_URL', }); - await provider.refresh(mockRequest, mockResponse); - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('SignInResolver rejects', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -282,19 +259,16 @@ describe('AwsAlbAuthProvider', () => { jwtMock.verify.mockReturnValueOnce(mockClaims); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.end).toHaveBeenCalledTimes(1); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('AuthHandler rejects', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async () => { throw new Error(); }, @@ -305,10 +279,9 @@ describe('AwsAlbAuthProvider', () => { jwtMock.verify.mockReturnValueOnce(mockClaims); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.end).toHaveBeenCalledTimes(1); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); }); }); diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 20b6f6f2ef..e65eab4455 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -18,6 +18,7 @@ import { AuthHandler, AuthProviderFactory, AuthProviderRouteHandlers, + AuthResolverContext, AuthResponse, SignInResolver, } from '../types'; @@ -25,11 +26,8 @@ import express from 'express'; import fetch from 'node-fetch'; import * as crypto from 'crypto'; import { KeyObject } from 'crypto'; -import { Logger } from 'winston'; import NodeCache from 'node-cache'; import { JWT } from 'jose'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; @@ -41,11 +39,9 @@ export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken'; type Options = { region: string; issuer?: string; - logger: Logger; authHandler: AuthHandler; signInResolver: SignInResolver; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; + resolverContext: AuthResolverContext; }; export const getJWTHeaders = (input: string): AwsAlbHeaders => { @@ -95,9 +91,7 @@ export type AwsAlbResponse = AuthResponse; export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private readonly region: string; private readonly issuer?: string; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; private readonly keyCache: NodeCache; private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; @@ -107,9 +101,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { this.issuer = options.issuer; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this.keyCache = new NodeCache({ stdTTL: 3600 }); } @@ -123,9 +115,10 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { const response = await this.handleResult(result); res.json(response); } catch (e) { - this.logger.error('Exception occurred during AWS ALB token refresh', e); - res.status(401); - res.end(); + throw new AuthenticationError( + 'Exception occurred during AWS ALB token refresh', + e, + ); } } @@ -182,18 +175,13 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } private async handleResult(result: AwsAlbResult): Promise { - const context = { - tokenIssuer: this.tokenIssuer, - catalogIdentityClient: this.catalogIdentityClient, - logger: this.logger, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const backstageIdentity = await this.signInResolver( { result, profile, }, - context, + this.resolverContext, ); return { @@ -258,7 +246,7 @@ export const createAwsAlbProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { + return ({ config, resolverContext }) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); @@ -268,27 +256,18 @@ export const createAwsAlbProvider = (options?: { ); } - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }); - const signInResolver = options?.signIn.resolver; - return new AwsAlbAuthProvider({ region, issuer, - signInResolver, + signInResolver: options?.signIn.resolver, authHandler, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); }; }; From 3046eb8498e6b531915c810d3eeeb70af04abc33 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:35:09 +0200 Subject: [PATCH 17/49] auth-backend: migrate bitbucket provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/bitbucket/provider.test.ts | 17 +----- .../src/providers/bitbucket/provider.ts | 59 ++++--------------- 2 files changed, 12 insertions(+), 64 deletions(-) diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts index 690729a5fb..9eeedc3d7b 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts @@ -16,9 +16,7 @@ import { BitbucketAuthProvider, BitbucketOAuthResult } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -29,19 +27,8 @@ const mockFrameHandler = jest.spyOn( describe('createBitbucketProvider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new BitbucketAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 30ea877843..c59e755130 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -17,8 +17,6 @@ import express from 'express'; import passport, { Profile as PassportProfile } from 'passport'; import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -43,8 +41,8 @@ import { AuthHandler, RedirectInfo, SignInResolver, + AuthResolverContext, } from '../types'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -53,9 +51,7 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export type BitbucketOAuthResult = { @@ -87,16 +83,12 @@ export class BitbucketAuthProvider implements OAuthHandlers { private readonly _strategy: BitbucketStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new BitbucketStrategy( { clientID: options.clientId, @@ -174,12 +166,7 @@ export class BitbucketAuthProvider implements OAuthHandlers { private async handleResult(result: BitbucketOAuthResult) { result.fullProfile.avatarUrl = result.fullProfile._json!.links!.avatar!.href; - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -197,7 +184,7 @@ export class BitbucketAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -214,16 +201,11 @@ export const bitbucketUsernameSignInResolver: SignInResolver< throw new Error('Bitbucket profile contained no Username'); } - const entity = await ctx.catalogIdentityClient.findUser({ + return ctx.signInWithCatalogUser({ annotations: { 'bitbucket.org/username': result.fullProfile.username, }, }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; }; export const bitbucketUserIdSignInResolver: SignInResolver< @@ -235,16 +217,11 @@ export const bitbucketUserIdSignInResolver: SignInResolver< throw new Error('Bitbucket profile contained no User ID'); } - const entity = await ctx.catalogIdentityClient.findUser({ + return ctx.signInWithCatalogUser({ annotations: { 'bitbucket.org/user-id': result.fullProfile.id, }, }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; }; /** @@ -285,15 +262,7 @@ export const createBitbucketProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -302,11 +271,6 @@ export const createBitbucketProvider = (options?: { customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler @@ -320,15 +284,12 @@ export const createBitbucketProvider = (options?: { callbackUrl, signInResolver: options?.signIn?.resolver, authHandler, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); From ca6f259244af428a957d98ea497ff6650bbaf1ba Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:36:55 +0200 Subject: [PATCH 18/49] auth-backend: migrate gcp-iap provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/gcp-iap/provider.test.ts | 7 +-- .../src/providers/gcp-iap/provider.ts | 43 ++++--------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts index eb2f5c478d..b1b2a3f66e 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; +import { AuthResolverContext } from '../types'; import { GcpIapProvider } from './provider'; beforeEach(() => { @@ -27,16 +27,13 @@ describe('GcpIapProvider', () => { const authHandler = jest.fn(); const signInResolver = jest.fn(); const tokenValidator = jest.fn(); - const logger = getVoidLogger(); it('runs the happy path', async () => { const provider = new GcpIapProvider({ authHandler, signInResolver, tokenValidator, - tokenIssuer: {} as any, - catalogIdentityClient: {} as any, - logger, + resolverContext: {} as AuthResolverContext, }); // { "sub": "user:default/me", "ent": ["group:default/home"] } diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 388d1902c4..e1fe5a4f01 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -16,14 +16,12 @@ import express from 'express'; import { TokenPayload } from 'google-auth-library'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; import { AuthHandler, AuthProviderFactory, AuthProviderRouteHandlers, + AuthResolverContext, SignInResolver, } from '../types'; import { @@ -31,35 +29,24 @@ import { defaultAuthHandler, parseRequestToken, } from './helpers'; -import { - GcpIapProviderOptions, - GcpIapResponse, - GcpIapResult, - IAP_JWT_HEADER, -} from './types'; +import { GcpIapResponse, GcpIapResult, IAP_JWT_HEADER } from './types'; export class GcpIapProvider implements AuthProviderRouteHandlers { private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; private readonly tokenValidator: (token: string) => Promise; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: { authHandler: AuthHandler; signInResolver: SignInResolver; tokenValidator: (token: string) => Promise; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }) { this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; this.tokenValidator = options.tokenValidator; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; } async start() {} @@ -71,17 +58,12 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { req.header(IAP_JWT_HEADER), this.tokenValidator, ); - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const backstageIdentity = await this.signInResolver( { profile, result }, - context, + this.resolverContext, ); const response: GcpIapResponse = { @@ -118,25 +100,18 @@ export function createGcpIapProvider(options: { resolver: SignInResolver; }; }): AuthProviderFactory { - return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { + return ({ config, resolverContext }) => { const audience = config.getString('audience'); const authHandler = options.authHandler ?? defaultAuthHandler; const signInResolver = options.signIn.resolver; const tokenValidator = createTokenValidator(audience); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - return new GcpIapProvider({ authHandler, signInResolver, tokenValidator, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); }; } From 2ae89a1c8b6b572155e83ff9c75d5f27f7b98fa3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:40:54 +0200 Subject: [PATCH 19/49] auth-backend: migrate gitlab provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/gitlab/provider.test.ts | 31 +++------ .../src/providers/gitlab/provider.ts | 67 +++---------------- 2 files changed, 18 insertions(+), 80 deletions(-) diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index f51ba58365..169930786b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -21,9 +21,7 @@ import { import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { PassportProfile } from '../../lib/passport/types'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -31,26 +29,16 @@ const mockFrameHandler = jest.spyOn( ) as unknown as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; describe('GitlabAuthProvider', () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - resolveCatalogMembership: async ({ - entityRefs, - }: { - entityRefs: string[]; - }) => entityRefs, - } as unknown as CatalogIdentityClient; - const provider = new GitlabAuthProvider({ clientId: 'mock', clientSecret: 'mock', callbackUrl: 'mock', baseUrl: 'mock', - catalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: { + signInWithCatalogUser: jest.fn(async ({ entityRef }) => ({ + token: `token-for-user:${entityRef.name}`, + })), + } as unknown as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, @@ -59,7 +47,6 @@ describe('GitlabAuthProvider', () => { }, }), signInResolver: gitlabUsernameEntityNameSignInResolver, - logger: getVoidLogger(), }); it('should transform to type OAuthResponse', async () => { @@ -92,7 +79,7 @@ describe('GitlabAuthProvider', () => { }, expect: { backstageIdentity: { - id: 'jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -134,7 +121,7 @@ describe('GitlabAuthProvider', () => { }, expect: { backstageIdentity: { - id: 'daveboyle', + token: 'token-for-user:daveboyle', }, providerInfo: { accessToken: @@ -196,7 +183,7 @@ describe('GitlabAuthProvider', () => { expect(result).toEqual({ response: { backstageIdentity: { - id: 'mockuser', + token: 'token-for-user:mockuser', }, profile: { displayName: 'Mocked User', diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index a26313fe95..292ed3efa5 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { Strategy as GitlabStrategy } from 'passport-gitlab2'; -import { Logger } from 'winston'; import { executeRedirectStrategy, executeFrameHandlerStrategy, @@ -34,6 +29,7 @@ import { AuthProviderFactory, SignInResolver, AuthHandler, + AuthResolverContext, } from '../types'; import { OAuthAdapter, @@ -46,8 +42,6 @@ import { encodeState, OAuthResult, } from '../../lib/oauth'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; type PrivateInfo = { refreshToken: string; @@ -57,9 +51,7 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export const gitlabUsernameEntityNameSignInResolver: SignInResolver< @@ -72,23 +64,7 @@ export const gitlabUsernameEntityNameSignInResolver: SignInResolver< throw new Error(`GitLab user profile does not contain a username`); } - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - const ownershipEntityRefs = - await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [entityRef], - }); - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: ownershipEntityRefs, - }, - }); - - return { id, token }; + return ctx.signInWithCatalogUser({ entityRef: { name: id } }); }; export const gitlabDefaultAuthHandler: AuthHandler = async ({ @@ -102,14 +78,10 @@ export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: GitlabAuthProviderOptions) { - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this.tokenIssuer = options.tokenIssuer; + this.resolverContext = options.resolverContext; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; @@ -180,12 +152,7 @@ export class GitlabAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult): Promise { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -203,7 +170,7 @@ export class GitlabAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -255,15 +222,7 @@ export const createGitlabProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -274,11 +233,6 @@ export const createGitlabProvider = (options?: { customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ?? gitlabDefaultAuthHandler; @@ -289,15 +243,12 @@ export const createGitlabProvider = (options?: { baseUrl, authHandler, signInResolver: options?.signIn?.resolver, - catalogIdentityClient, - logger, - tokenIssuer, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); From 837751840161ab590e980977ed78f84d69695405 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:49:55 +0200 Subject: [PATCH 20/49] auth-backend: migrate microsoft provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/microsoft/provider.test.ts | 26 +----- .../src/providers/microsoft/provider.ts | 86 ++++++------------- 2 files changed, 30 insertions(+), 82 deletions(-) diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 3a0fc5ad86..e9b4424a95 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -18,11 +18,10 @@ import { MicrosoftAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -87,19 +86,10 @@ const setupHandlers = () => { describe('createMicrosoftProvider', () => { it('should auth', async () => { setupHandlers(); - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; const provider = new MicrosoftAuthProvider({ logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, @@ -131,19 +121,9 @@ describe('createMicrosoftProvider', () => { it('should return the base64 encoded photo data of the profile', async () => { setupHandlers(); - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new MicrosoftAuthProvider({ logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 954f7742c4..053ab64547 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -17,8 +17,6 @@ import express from 'express'; import passport from 'passport'; import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -43,6 +41,7 @@ import { AuthHandler, RedirectInfo, SignInResolver, + AuthResolverContext, } from '../types'; import { Logger } from 'winston'; import fetch from 'node-fetch'; @@ -54,9 +53,8 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; logger: Logger; + resolverContext: AuthResolverContext; authorizationUrl?: string; tokenUrl?: string; }; @@ -65,16 +63,14 @@ export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; this.logger = options.logger; - this.catalogIdentityClient = options.catalogIdentityClient; + this.resolverContext = options.resolverContext; this._strategy = new MicrosoftStrategy( { @@ -143,12 +139,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { const photo = await this.getUserPhoto(result.accessToken); result.fullProfile.photos = photo ? [{ value: photo }] : undefined; - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -166,35 +157,32 @@ export class MicrosoftAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } return response; } - private getUserPhoto(accessToken: string): Promise { - return new Promise(resolve => { - fetch('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', { - headers: { - Authorization: `Bearer ${accessToken}`, + private async getUserPhoto(accessToken: string): Promise { + try { + const res = await fetch( + 'https://graph.microsoft.com/v1.0/me/photos/48x48/$value', + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, }, - }) - .then(response => response.arrayBuffer()) - .then(arrayBuffer => { - const imageUrl = `data:image/jpeg;base64,${Buffer.from( - arrayBuffer, - ).toString('base64')}`; - resolve(imageUrl); - }) - .catch(error => { - this.logger.warn( - `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, - ); - // User profile photo is optional, ignore errors and resolve undefined - resolve(undefined); - }); - }); + ); + const data = await res.buffer(); + + return `data:image/jpeg;base64,${data.toString('base64')}`; + } catch (error) { + this.logger.warn( + `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, + ); + return undefined; + } } } @@ -208,16 +196,11 @@ export const microsoftEmailSignInResolver: SignInResolver = async ( throw new Error('Microsoft profile contained no email'); } - const entity = await ctx.catalogIdentityClient.findUser({ + return ctx.signInWithCatalogUser({ annotations: { 'microsoft.com/email': profile.email, }, }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; }; /** @@ -258,15 +241,7 @@ export const createMicrosoftProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, logger, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -279,11 +254,6 @@ export const createMicrosoftProvider = (options?: { const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({ @@ -298,15 +268,13 @@ export const createMicrosoftProvider = (options?: { tokenUrl, authHandler, signInResolver: options?.signIn?.resolver, - catalogIdentityClient, logger, - tokenIssuer, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); From 70b592c9a6ba5717c9f92518f05c2c1e61789a76 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:50:21 +0200 Subject: [PATCH 21/49] auth-backend: undeprecate logger field in auth provider factory options Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index d3f6a72aaf..b1e45bc8a9 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -219,10 +219,9 @@ export type AuthProviderFactory = (options: { providerId: string; globalConfig: AuthProviderConfig; config: Config; + logger: Logger; resolverContext: AuthResolverContext; - /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ - logger: Logger; /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ tokenManager: TokenManager; /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ From 2d126853ef4585afc02d1645f525952a29a87e30 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:51:55 +0200 Subject: [PATCH 22/49] auth-backend: migrate oauth2 provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/oauth2/provider.test.ts | 17 +------ .../src/providers/oauth2/provider.ts | 45 ++++--------------- 2 files changed, 10 insertions(+), 52 deletions(-) diff --git a/plugins/auth-backend/src/providers/oauth2/provider.test.ts b/plugins/auth-backend/src/providers/oauth2/provider.test.ts index 7030d91343..23e08dd729 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.test.ts @@ -17,9 +17,7 @@ import { OAuth2AuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -30,19 +28,8 @@ const mockFrameHandler = jest.spyOn( describe('createOAuth2Provider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new OAuth2AuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 5c5df2a748..312efd6d1f 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -39,12 +39,10 @@ import { import { AuthHandler, AuthProviderFactory, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -53,12 +51,10 @@ type PrivateInfo = { export type OAuth2AuthProviderOptions = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; authorizationUrl: string; tokenUrl: string; scope?: string; - logger: Logger; + resolverContext: AuthResolverContext; includeBasicAuth?: boolean; }; @@ -66,16 +62,12 @@ export class OAuth2AuthProvider implements OAuthHandlers { private readonly _strategy: OAuth2Strategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: OAuth2AuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new OAuth2Strategy( { @@ -163,12 +155,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -186,7 +173,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -216,15 +203,7 @@ export const createOAuth2Provider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -239,11 +218,6 @@ export const createOAuth2Provider = (options?: { const disableRefresh = envConfig.getOptionalBoolean('disableRefresh') ?? false; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({ @@ -253,22 +227,19 @@ export const createOAuth2Provider = (options?: { const provider = new OAuth2AuthProvider({ clientId, clientSecret, - tokenIssuer, - catalogIdentityClient, callbackUrl, signInResolver: options?.signIn?.resolver, authHandler, authorizationUrl, tokenUrl, scope, - logger, includeBasicAuth, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh, providerId, - tokenIssuer, callbackUrl, }); }); From 3811722d98aa3edb8e7bdbe681bbff9ef889af04 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:58:36 +0200 Subject: [PATCH 23/49] auth-backend: migrate oauth2-proxy provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../providers/oauth2-proxy/provider.test.ts | 34 ++++++-------- .../src/providers/oauth2-proxy/provider.ts | 45 ++++--------------- 2 files changed, 23 insertions(+), 56 deletions(-) diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts index efede36212..e27713acb3 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts @@ -21,18 +21,14 @@ jest.mock('jose', () => ({ })); jest.mock('@backstage/catalog-client'); +import { AuthenticationError } from '@backstage/errors'; import express from 'express'; import { JWT } from 'jose'; import { Logger } from 'winston'; -import { AuthHandler, SignInResolver } from '../types'; - -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity/types'; - +import { AuthHandler, AuthResolverContext, SignInResolver } from '../types'; import { createOauth2ProxyProvider, Oauth2ProxyAuthProvider, - Oauth2ProxyProviderOptions, OAuth2ProxyResult, OAUTH2_PROXY_JWT_HEADER, } from './provider'; @@ -72,10 +68,10 @@ describe('Oauth2ProxyAuthProvider', () => { provider = new Oauth2ProxyAuthProvider({ authHandler, - logger, signInResolver, - catalogIdentityClient: {} as CatalogIdentityClient, - tokenIssuer: {} as TokenIssuer, + resolverContext: { + _: 'resolver-context', + } as unknown as AuthResolverContext, }); }); @@ -99,17 +95,17 @@ describe('Oauth2ProxyAuthProvider', () => { it('should throw an error when auth header is missing', async () => { mockRequest.header.mockReturnValue(undefined); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('should throw an error if the bearer token is invalid', async () => { mockRequest.header.mockReturnValue('Basic asdf='); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('should return if auth header is set and valid', async () => { @@ -152,7 +148,7 @@ describe('Oauth2ProxyAuthProvider', () => { fullProfile: decodedToken, }, }, - { catalogIdentityClient: {}, logger, tokenIssuer: {} }, + { _: 'resolver-context' }, ); expect(mockResponse.json).toHaveBeenCalledWith({ backstageIdentity: { @@ -183,12 +179,10 @@ describe('Oauth2ProxyAuthProvider', () => { }); it('should create a valid provider', async () => { - const providerOptions = { + const factory = createOauth2ProxyProvider({ authHandler, signIn: { resolver: signInResolver }, - } as Oauth2ProxyProviderOptions; - - const factory = createOauth2ProxyProvider(providerOptions); + }); const handler = factory({ logger, catalogApi: {}, diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index f89f759270..ed3e5bce81 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -15,7 +15,6 @@ */ import express from 'express'; -import { Logger } from 'winston'; import { AuthenticationError } from '@backstage/errors'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { @@ -24,10 +23,9 @@ import { AuthProviderFactory, AuthProviderRouteHandlers, AuthResponse, + AuthResolverContext, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { JWT } from 'jose'; -import { TokenIssuer } from '../../identity/types'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN'; @@ -71,28 +69,22 @@ export type Oauth2ProxyProviderOptions = { }; interface Options { - logger: Logger; + resolverContext: AuthResolverContext; signInResolver: SignInResolver>; authHandler: AuthHandler>; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; } export class Oauth2ProxyAuthProvider implements AuthProviderRouteHandlers { - private readonly logger: Logger; - private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly resolverContext: AuthResolverContext; private readonly signInResolver: SignInResolver< OAuth2ProxyResult >; private readonly authHandler: AuthHandler>; - private readonly tokenIssuer: TokenIssuer; constructor(options: Options) { - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this.tokenIssuer = options.tokenIssuer; + this.resolverContext = options.resolverContext; this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; } @@ -104,17 +96,10 @@ export class Oauth2ProxyAuthProvider async refresh(req: express.Request, res: express.Response): Promise { try { const result = this.getResult(req); - const response = await this.handleResult(result); - res.json(response); } catch (e) { - this.logger.error( - `Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, - e, - ); - res.status(401); - res.end(); + throw new AuthenticationError('Refresh failed', e); } } @@ -125,20 +110,14 @@ export class Oauth2ProxyAuthProvider private async handleResult( result: OAuth2ProxyResult, ): Promise> { - const ctx = { - logger: this.logger, - tokenIssuer: this.tokenIssuer, - catalogIdentityClient: this.catalogIdentityClient, - }; - - const { profile } = await this.authHandler(result, ctx); + const { profile } = await this.authHandler(result, this.resolverContext); const backstageSignInResult = await this.signInResolver( { result, profile, }, - ctx, + this.resolverContext, ); return { @@ -193,18 +172,12 @@ export const createOauth2ProxyProvider = resolver: SignInResolver>; }; }): AuthProviderFactory => - ({ catalogApi, logger, tokenIssuer, tokenManager }) => { + ({ resolverContext }) => { const signInResolver = options.signIn.resolver; const authHandler = options.authHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); return new Oauth2ProxyAuthProvider({ - logger, + resolverContext, signInResolver, authHandler, - tokenIssuer, - catalogIdentityClient, }); }; From 116a740067993479d0cd89ed0b4dfae95e2e2d67 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 13:59:59 +0200 Subject: [PATCH 24/49] auth-backend: migrate oidc provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/oidc/provider.test.ts | 14 +----- .../src/providers/oidc/provider.ts | 44 ++++--------------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 13fe1ae46b..5312789a28 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -24,7 +24,7 @@ import { setupServer } from 'msw/node'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { OAuthAdapter } from '../../lib/oauth'; import { createOidcProvider, OidcAuthProvider, Options } from './provider'; -import { getVoidLogger } from '@backstage/backend-common'; +import { AuthResolverContext } from '../types'; const issuerMetadata = { issuer: 'https://oidc.test', @@ -42,23 +42,13 @@ const issuerMetadata = { request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], }; -const catalogIdentityClient = { - findUser: jest.fn(), -}; -const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), -}; - const clientMetadata: Options = { authHandler: async input => ({ profile: { displayName: input.userinfo.email, }, }), - catalogIdentityClient: catalogIdentityClient as unknown as any, - logger: getVoidLogger(), - tokenIssuer: tokenIssuer as unknown as any, + resolverContext: {} as AuthResolverContext, callbackUrl: 'https://oidc.test/callback', clientId: 'testclientid', clientSecret: 'testclientsecret', diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 82f6df8faf..53729ce7cc 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -40,12 +40,10 @@ import { import { AuthHandler, AuthProviderFactory, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken?: string; @@ -72,9 +70,7 @@ export type Options = OAuthProviderOptions & { tokenSignedResponseAlg?: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class OidcAuthProvider implements OAuthHandlers { @@ -84,9 +80,7 @@ export class OidcAuthProvider implements OAuthHandlers { private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.implementation = this.setupStrategy(options); @@ -94,9 +88,7 @@ export class OidcAuthProvider implements OAuthHandlers { this.prompt = options.prompt; this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; } async start(req: OAuthStartRequest): Promise { @@ -182,12 +174,7 @@ export class OidcAuthProvider implements OAuthHandlers { // Use this function to grab the user profile info from the token // Then populate the profile with it private async handleResult(result: OidcAuthResult): Promise { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { idToken: result.tokenset.id_token, @@ -203,7 +190,7 @@ export class OidcAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -229,15 +216,7 @@ export const createOidcProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -251,10 +230,6 @@ export const createOidcProvider = (options?: { ); const scope = envConfig.getOptionalString('scope'); const prompt = envConfig.getOptionalString('prompt'); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); const authHandler: AuthHandler = options?.authHandler ? options.authHandler @@ -276,15 +251,12 @@ export const createOidcProvider = (options?: { prompt, signInResolver: options?.signIn?.resolver, authHandler, - logger, - tokenIssuer, - catalogIdentityClient, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); From 90fe0e2534bf23f4e45de2da89eb6aacccc83ec5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:02:16 +0200 Subject: [PATCH 25/49] auth-backend: migrate okta provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/okta/provider.test.ts | 17 +--- .../src/providers/okta/provider.ts | 80 ++++++------------- 2 files changed, 25 insertions(+), 72 deletions(-) diff --git a/plugins/auth-backend/src/providers/okta/provider.test.ts b/plugins/auth-backend/src/providers/okta/provider.test.ts index 939981e81c..468c4cdcc8 100644 --- a/plugins/auth-backend/src/providers/okta/provider.test.ts +++ b/plugins/auth-backend/src/providers/okta/provider.test.ts @@ -17,9 +17,7 @@ import { OktaAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -30,19 +28,8 @@ const mockFrameHandler = jest.spyOn( describe('createOktaProvider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new OktaAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 74fbe8159c..606a9bbce7 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -41,11 +41,9 @@ import { AuthHandler, RedirectInfo, SignInResolver, + AuthResolverContext, } from '../types'; import { StateStore } from 'passport-oauth2'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -55,18 +53,14 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & { audience: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class OktaAuthProvider implements OAuthHandlers { - private readonly _strategy: any; - private readonly _signInResolver?: SignInResolver; - private readonly _authHandler: AuthHandler; - private readonly _tokenIssuer: TokenIssuer; - private readonly _catalogIdentityClient: CatalogIdentityClient; - private readonly _logger: Logger; + private readonly strategy: any; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly resolverContext: AuthResolverContext; /** * Due to passport-okta-oauth forcing options.state = true, @@ -76,7 +70,7 @@ export class OktaAuthProvider implements OAuthHandlers { * passport-oauth2, which is the StateStore implementation used when options.state = false, * allowing us to avoid using express-session in order to integrate with Okta. */ - private _store: StateStore = { + private store: StateStore = { store(_req: express.Request, cb: any) { cb(null, null); }, @@ -86,20 +80,18 @@ export class OktaAuthProvider implements OAuthHandlers { }; constructor(options: OktaAuthProviderOptions) { - this._signInResolver = options.signInResolver; - this._authHandler = options.authHandler; - this._tokenIssuer = options.tokenIssuer; - this._catalogIdentityClient = options.catalogIdentityClient; - this._logger = options.logger; + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.resolverContext = options.resolverContext; - this._strategy = new OktaStrategy( + this.strategy = new OktaStrategy( { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, audience: options.audience, passReqToCallback: false as true, - store: this._store, + store: this.store, response_type: 'code', }, ( @@ -126,7 +118,7 @@ export class OktaAuthProvider implements OAuthHandlers { } async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { + return await executeRedirectStrategy(req, this.strategy, { accessType: 'offline', prompt: 'consent', scope: req.scope, @@ -138,7 +130,7 @@ export class OktaAuthProvider implements OAuthHandlers { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo - >(req, this._strategy); + >(req, this.strategy); return { response: await this.handleResult(result), @@ -149,13 +141,13 @@ export class OktaAuthProvider implements OAuthHandlers { async refresh(req: OAuthRefreshRequest) { const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy( - this._strategy, + this.strategy, req.refreshToken, req.scope, ); const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, + this.strategy, accessToken, ); @@ -170,12 +162,7 @@ export class OktaAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this._logger, - catalogIdentityClient: this._catalogIdentityClient, - tokenIssuer: this._tokenIssuer, - }; - const { profile } = await this._authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -187,13 +174,13 @@ export class OktaAuthProvider implements OAuthHandlers { profile, }; - if (this._signInResolver) { - response.backstageIdentity = await this._signInResolver( + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( { result, profile, }, - context, + this.resolverContext, ); } @@ -211,16 +198,11 @@ export const oktaEmailSignInResolver: SignInResolver = async ( throw new Error('Okta profile contained no email'); } - const entity = await ctx.catalogIdentityClient.findUser({ + return ctx.signInWithCatalogUser({ annotations: { 'okta.com/email': profile.email, }, }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; }; /** @@ -261,15 +243,7 @@ export const createOktaProvider = (_options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -286,11 +260,6 @@ export const createOktaProvider = (_options?: { throw new Error("URL for 'audience' must start with 'https://'."); } - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = _options?.authHandler ? _options.authHandler : async ({ fullProfile, params }) => ({ @@ -304,15 +273,12 @@ export const createOktaProvider = (_options?: { callbackUrl, authHandler, signInResolver: _options?.signIn?.resolver, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); From b812909da4ac0b05cb16ff2aba2628949784879c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:03:10 +0200 Subject: [PATCH 26/49] auth-backend: migrate onelogin provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/onelogin/provider.ts | 45 ++++--------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 526d432b82..05699fcb17 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -41,10 +41,8 @@ import { AuthProviderFactory, AuthHandler, SignInResolver, + AuthResolverContext, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; type PrivateInfo = { refreshToken: string; @@ -54,25 +52,19 @@ export type Options = OAuthProviderOptions & { issuer: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new OneLoginStrategy( { issuer: options.issuer, @@ -148,12 +140,7 @@ export class OneLoginProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -171,7 +158,7 @@ export class OneLoginProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -218,15 +205,7 @@ export const createOneLoginProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => + return ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -236,11 +215,6 @@ export const createOneLoginProvider = (options?: { customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({ @@ -254,15 +228,12 @@ export const createOneLoginProvider = (options?: { issuer, authHandler, signInResolver: options?.signIn?.resolver, - tokenIssuer, - catalogIdentityClient, - logger, + resolverContext, }); return OAuthAdapter.fromConfig(globalConfig, provider, { disableRefresh: false, providerId, - tokenIssuer, callbackUrl, }); }); From e5c533e6c3b19ed8dfa4fe84aec29faa9b1406db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:06:14 +0200 Subject: [PATCH 27/49] auth-backend: migrate saml provider to use resolver context Signed-off-by: Patrik Oldsberg --- .../src/providers/saml/provider.ts | 73 ++++--------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index bcd77fb7d3..592510acf5 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { SamlConfig } from 'passport-saml/lib/passport-saml/types'; import { @@ -36,12 +32,10 @@ import { AuthHandler, SignInResolver, AuthResponse, + AuthResolverContext, } from '../types'; import { postMessageResponse } from '../../lib/flow'; -import { TokenIssuer } from '../../identity/types'; -import { isError } from '@backstage/errors'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { Logger } from 'winston'; +import { AuthenticationError, isError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; /** @public */ @@ -52,9 +46,7 @@ export type SamlAuthResult = { type Options = SamlConfig & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; appUrl: string; }; @@ -62,18 +54,14 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; private readonly appUrl: string; constructor(options: Options) { this.appUrl = options.appUrl; this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this.strategy = new SamlStrategy({ ...options }, (( fullProfile: SamlProfile, done: PassportDoneCallback, @@ -97,18 +85,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res: express.Response, ): Promise { try { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { result } = await executeFrameHandlerStrategy( req, this.strategy, ); - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: AuthResponse<{}> = { profile, @@ -121,7 +103,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { result, profile, }, - context, + this.resolverContext, ); response.backstageIdentity = @@ -153,23 +135,13 @@ export const samlNameIdEntityNameSignInResolver: SignInResolver< > = async (info, ctx) => { const id = info.result.fullProfile.nameID; - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - const ownershipEntityRefs = - await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [entityRef], - }); - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: ownershipEntityRefs, - }, - }); + if (!id) { + throw new AuthenticationError('No nameID found in SAML response'); + } - return { id, token }; + return ctx.signInWithCatalogUser({ + entityRef: { name: id }, + }); }; type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; @@ -213,20 +185,7 @@ export const createSamlProvider = (options?: { resolver: SignInResolver; }; }): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => { - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - + return ({ providerId, globalConfig, config, resolverContext }) => { const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ fullProfile }) => ({ @@ -253,12 +212,10 @@ export const createSamlProvider = (options?: { digestAlgorithm: config.getOptionalString('digestAlgorithm'), acceptedClockSkewMs: config.getOptionalNumber('acceptedClockSkewMs'), - tokenIssuer, appUrl: globalConfig.appUrl, authHandler, signInResolver: options?.signIn?.resolver, - logger, - catalogIdentityClient, + resolverContext, }); }; }; From 55b2abac93263e9704bec7bb005ec7069b455e93 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:19:16 +0200 Subject: [PATCH 28/49] auth-backend: tweak auth provider integration to allow no resolvers Signed-off-by: Patrik Oldsberg --- .../src/providers/createAuthProviderIntegration.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts index 8b58e89062..c6e9107353 100644 --- a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts +++ b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts @@ -25,18 +25,20 @@ import { AuthProviderFactory, SignInResolver } from './types'; */ export function createAuthProviderIntegration< TCreateOptions extends unknown[], - TResolvers extends { - [name in string]: (...args: any[]) => SignInResolver; - }, + TResolvers extends + | { + [name in string]: (...args: any[]) => SignInResolver; + }, >(config: { create: (...args: TCreateOptions) => AuthProviderFactory; - resolvers: TResolvers; + resolvers?: TResolvers; }): Readonly<{ create: (...args: TCreateOptions) => AuthProviderFactory; - resolvers: Readonly; + // If no resolvers are defined, this receives the type `never` + resolvers: Readonly; }> { return Object.freeze({ ...config, - resolvers: Object.freeze(config.resolvers), + resolvers: Object.freeze(config.resolvers ?? ({} as any)), }); } From 7738a004b09063fd061b1071f6d00f3548e59dac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:20:59 +0200 Subject: [PATCH 29/49] auth-backend: migrate atlassian provider to use integration helper Signed-off-by: Patrik Oldsberg --- .../src/providers/atlassian/provider.ts | 91 +++++++++++-------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index e45b1dfedd..1cb83d4589 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -37,12 +37,12 @@ import { } from '../../lib/passport'; import { AuthHandler, - AuthProviderFactory, AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; import express from 'express'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; @@ -179,46 +179,59 @@ export type AtlassianProviderOptions = { }; }; -export const createAtlassianProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; +/** + * Auth provider integration for atlassian auth + * + * @public + */ +export const atlassian = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const scopes = envConfig.getString('scopes'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const scopes = envConfig.getString('scopes'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authHandler: AuthHandler = - options?.authHandler ?? atlassianDefaultAuthHandler; + const authHandler: AuthHandler = + options?.authHandler ?? atlassianDefaultAuthHandler; - const provider = new AtlassianAuthProvider({ - clientId, - clientSecret, - scopes, - callbackUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, + const provider = new AtlassianAuthProvider({ + clientId, + clientSecret, + scopes, + callbackUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.atlassian.create` instead + */ +export const createAtlassianProvider = atlassian.create; From dff8ce7cadecf47e3e9e42f2d274a98705800515 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:23:47 +0200 Subject: [PATCH 30/49] auth-backend: migrate auth0 provider to use integration helper Signed-off-by: Patrik Oldsberg --- .../src/providers/auth0/provider.ts | 105 ++++++++++-------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 185cdd0c79..beb2b5fba4 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -38,11 +38,11 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, AuthHandler, SignInResolver, AuthResolverContext, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -188,56 +188,67 @@ export type Auth0ProviderOptions = { }; }; -/** @public */ -export const createAuth0Provider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for auth0 auth + * + * @public + */ +export const auth0 = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const domain = envConfig.getString('domain'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + authHandler?: AuthHandler; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const signInResolver = options?.signIn?.resolver; + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const provider = new Auth0AuthProvider({ - clientId, - clientSecret, - callbackUrl, - domain, - authHandler, - signInResolver, - resolverContext, + const signInResolver = options?.signIn?.resolver; + + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + authHandler, + signInResolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - providerId, - callbackUrl, - }); - }); -}; +/** + * @deprecated Use `providers.auth0.create` instead. + */ +export const createAuth0Provider = auth0.create; From 0d1f36cd6d6368507a4195fd08f9b8b0216a11bd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:24:55 +0200 Subject: [PATCH 31/49] auth-backend: migrate aws-alb provider to use integration helper Signed-off-by: Patrik Oldsberg --- .../src/providers/aws-alb/provider.ts | 85 ++++++++++--------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index e65eab4455..ac699d6eda 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -16,7 +16,6 @@ import { AuthHandler, - AuthProviderFactory, AuthProviderRouteHandlers, AuthResolverContext, AuthResponse, @@ -32,6 +31,7 @@ import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -229,45 +229,54 @@ export type AwsAlbProviderOptions = { }; }; -export const createAwsAlbProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn: { +/** + * Auth provider integration for AWS ALB auth + * + * @public + */ +export const awsAlb = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ config, resolverContext }) => { - const region = config.getString('region'); - const issuer = config.getOptionalString('iss'); + authHandler?: AuthHandler; - if (options?.signIn.resolver === undefined) { - throw new Error( - 'SignInResolver is required to use this authentication provider', - ); - } + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ config, resolverContext }) => { + const region = config.getString('region'); + const issuer = config.getOptionalString('iss'); - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }); + if (options?.signIn.resolver === undefined) { + throw new Error( + 'SignInResolver is required to use this authentication provider', + ); + } - return new AwsAlbAuthProvider({ - region, - issuer, - signInResolver: options?.signIn.resolver, - authHandler, - resolverContext, - }); - }; -}; + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + + return new AwsAlbAuthProvider({ + region, + issuer, + signInResolver: options?.signIn.resolver, + authHandler, + resolverContext, + }); + }; + }, +}); + +export const createAwsAlbProvider = awsAlb.create; From 01d4bd510999f638725dea7506c38f3c3e7be3fd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:37:55 +0200 Subject: [PATCH 32/49] auth-backend: migrate all other providers to use integration helper Signed-off-by: Patrik Oldsberg --- .../src/providers/bitbucket/provider.ts | 101 +++++----- .../src/providers/gcp-iap/provider.ts | 72 ++++---- .../src/providers/github/provider.ts | 173 ++++++++++-------- .../src/providers/gitlab/provider.ts | 107 ++++++----- .../src/providers/microsoft/provider.ts | 111 ++++++----- .../src/providers/oauth2-proxy/provider.ts | 36 ++-- .../src/providers/oauth2/provider.ts | 102 ++++++----- .../src/providers/oidc/provider.ts | 109 ++++++----- .../src/providers/okta/provider.ts | 115 ++++++------ .../src/providers/onelogin/provider.ts | 104 ++++++----- .../src/providers/saml/provider.ts | 110 ++++++----- 11 files changed, 637 insertions(+), 503 deletions(-) diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index c59e755130..7a3e6c9a60 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -36,8 +36,8 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { - AuthProviderFactory, AuthHandler, RedirectInfo, SignInResolver, @@ -245,52 +245,65 @@ export type BitbucketProviderOptions = { }; }; -export const createBitbucketProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for BitBucket auth + * + * @public + */ +export const bitbucket = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + authHandler?: AuthHandler; - const authHandler: AuthHandler = - options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new BitbucketAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - resolverContext, + const authHandler: AuthHandler = + options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new BitbucketAuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.bitbucket.create` instead + */ +export const createBitbucketProvider = bitbucket.create; diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index e1fe5a4f01..cb5d5979cd 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -16,10 +16,10 @@ import express from 'express'; import { TokenPayload } from 'google-auth-library'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; import { AuthHandler, - AuthProviderFactory, AuthProviderRouteHandlers, AuthResolverContext, SignInResolver, @@ -77,41 +77,49 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { } /** - * Creates an auth provider for Google Identity-Aware Proxy. + * Auth provider integration for Google Identity-Aware Proxy auth * * @public */ -export function createGcpIapProvider(options: { - /** - * The profile transformation function used to verify and convert the auth - * response into the profile that will be presented to the user. The default - * implementation just provides the authenticated email that the IAP - * presented. - */ - authHandler?: AuthHandler; - - /** - * Configures sign-in for this provider. - */ - signIn: { +export const gcpIap = createAuthProviderIntegration({ + create(options: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth + * response into the profile that will be presented to the user. The default + * implementation just provides the authenticated email that the IAP + * presented. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory { - return ({ config, resolverContext }) => { - const audience = config.getString('audience'); + authHandler?: AuthHandler; - const authHandler = options.authHandler ?? defaultAuthHandler; - const signInResolver = options.signIn.resolver; - const tokenValidator = createTokenValidator(audience); + /** + * Configures sign-in for this provider. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ config, resolverContext }) => { + const audience = config.getString('audience'); - return new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext, - }); - }; -} + const authHandler = options.authHandler ?? defaultAuthHandler; + const signInResolver = options.signIn.resolver; + const tokenValidator = createTokenValidator(audience); + + return new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + resolverContext, + }); + }; + }, +}); + +/** + * @public + * @deprecated Use `providers.gcpIap.create` instead + */ +export const createGcpIapProvider = gcpIap.create; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 770715d0ae..d84c89261f 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -27,7 +27,6 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, AuthHandler, SignInResolver, StateEncoder, @@ -42,6 +41,7 @@ import { encodeState, OAuthRefreshRequest, } from '../../lib/oauth'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; const ACCESS_TOKEN_PREFIX = 'access-token.'; @@ -279,91 +279,106 @@ export type GithubProviderOptions = { stateEncoder?: StateEncoder; }; -export const createGithubProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for GitHub auth + * + * @public + */ +export const github = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; + authHandler?: AuthHandler; - /** - * The state encoder used to encode the 'state' parameter on the OAuth request. - * - * It should return a string that takes the state params (from the request), url encodes the params - * and finally base64 encodes them. - * - * Providing your own stateEncoder will allow you to add addition parameters to the state field. - * - * It is typed as follows: - * `export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>;` - * - * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail - * (These two values will be set by the req.state by default) - * - * For more information, please see the helper module in ../../oauth/helpers #readState - */ - stateEncoder?: StateEncoder; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig - .getOptionalString('enterpriseInstanceUrl') - ?.replace(/\/$/, ''); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), + /** + * The state encoder used to encode the 'state' parameter on the OAuth request. + * + * It should return a string that takes the state params (from the request), url encodes the params + * and finally base64 encodes them. + * + * Providing your own stateEncoder will allow you to add addition parameters to the state field. + * + * It is typed as follows: + * `export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>;` + * + * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail + * (These two values will be set by the req.state by default) + * + * For more information, please see the helper module in ../../oauth/helpers #readState + */ + stateEncoder?: StateEncoder; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig + .getOptionalString('enterpriseInstanceUrl') + ?.replace(/\/$/, ''); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + + const stateEncoder: StateEncoder = + options?.stateEncoder ?? + (async ( + req: OAuthStartRequest, + ): Promise<{ encodedState: string }> => { + return { encodedState: encodeState(req.state) }; }); - const stateEncoder: StateEncoder = - options?.stateEncoder ?? - (async (req: OAuthStartRequest): Promise<{ encodedState: string }> => { - return { encodedState: encodeState(req.state) }; + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + stateEncoder, + resolverContext, }); - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - stateEncoder, - resolverContext, + return OAuthAdapter.fromConfig(globalConfig, provider, { + persistScopes: true, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - persistScopes: true, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.github.create` instead + */ +export const createGithubProvider = github.create; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 292ed3efa5..b8de3845a5 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -26,7 +26,6 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, SignInResolver, AuthHandler, AuthResolverContext, @@ -42,6 +41,7 @@ import { encodeState, OAuthResult, } from '../../lib/oauth'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -202,54 +202,67 @@ export type GitlabProviderOptions = { }; }; -export const createGitlabProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; +/** + * Auth provider integration for GitLab auth + * + * @public + */ +export const gitlab = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - /** - * Maps an auth result to a Backstage identity for the user. - * - * Set to `'email'` to use the default email-based sign in resolver, which will search - * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. - */ - signIn?: { - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getOptionalString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + /** + * Maps an auth result to a Backstage identity for the user. + * + * Set to `'email'` to use the default email-based sign in resolver, which will search + * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. + */ + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getOptionalString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authHandler: AuthHandler = - options?.authHandler ?? gitlabDefaultAuthHandler; + const authHandler: AuthHandler = + options?.authHandler ?? gitlabDefaultAuthHandler; - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.gitlab.create` instead + */ +export const createGitlabProvider = gitlab.create; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 053ab64547..106ab66b6d 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -37,12 +37,12 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderFactory, AuthHandler, RedirectInfo, SignInResolver, AuthResolverContext, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { Logger } from 'winston'; import fetch from 'node-fetch'; @@ -224,58 +224,71 @@ export type MicrosoftProviderOptions = { }; }; -export const createMicrosoftProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for Microsoft auth + * + * @public + */ +export const microsoft = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, logger, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const tenantId = envConfig.getString('tenantId'); + authHandler?: AuthHandler; - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, logger, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantId = envConfig.getString('tenantId'); - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; - const provider = new MicrosoftAuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - logger, - resolverContext, + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + logger, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.microsoft.create` instead + */ +export const createMicrosoftProvider = microsoft.create; diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index ed3e5bce81..a7a8befd5e 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -20,13 +20,13 @@ import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-no import { AuthHandler, SignInResolver, - AuthProviderFactory, AuthProviderRouteHandlers, AuthResponse, AuthResolverContext, } from '../types'; import { JWT } from 'jose'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN'; @@ -151,12 +151,12 @@ export class Oauth2ProxyAuthProvider } /** - * Factory function for oauth2-proxy auth provider + * Auth provider integration for oauth2-proxy auth * * @public */ -export const createOauth2ProxyProvider = - (options: { +export const oauth2Proxy = createAuthProviderIntegration({ + create(options: { /** * Configure an auth handler to generate a profile for the user. */ @@ -171,13 +171,21 @@ export const createOauth2ProxyProvider = */ resolver: SignInResolver>; }; - }): AuthProviderFactory => - ({ resolverContext }) => { - const signInResolver = options.signIn.resolver; - const authHandler = options.authHandler; - return new Oauth2ProxyAuthProvider({ - resolverContext, - signInResolver, - authHandler, - }); - }; + }) { + return ({ resolverContext }) => { + const signInResolver = options.signIn.resolver; + const authHandler = options.authHandler; + return new Oauth2ProxyAuthProvider({ + resolverContext, + signInResolver, + authHandler, + }); + }; + }, +}); + +/** + * @public + * @deprecated Use `providers.oauth2Proxy.create` instead + */ +export const createOauth2ProxyProvider = oauth2Proxy.create; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 312efd6d1f..7af10a522f 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -38,11 +38,11 @@ import { } from '../../lib/passport'; import { AuthHandler, - AuthProviderFactory, AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -196,51 +196,65 @@ export type OAuth2ProviderOptions = { }; }; -export const createOAuth2Provider = (options?: { - authHandler?: AuthHandler; +/** + * Auth provider integration for generic OAuth2 auth + * + * @public + */ +export const oauth2 = createAuthProviderIntegration({ + create(options?: { + authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); - const scope = envConfig.getOptionalString('scope'); - const includeBasicAuth = envConfig.getOptionalBoolean('includeBasicAuth'); - const disableRefresh = - envConfig.getOptionalBoolean('disableRefresh') ?? false; + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); + const scope = envConfig.getOptionalString('scope'); + const includeBasicAuth = + envConfig.getOptionalBoolean('includeBasicAuth'); + const disableRefresh = + envConfig.getOptionalBoolean('disableRefresh') ?? false; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const provider = new OAuth2AuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - authorizationUrl, - tokenUrl, - scope, - includeBasicAuth, - resolverContext, + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + authorizationUrl, + tokenUrl, + scope, + includeBasicAuth, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.oauth2.create` instead + */ +export const createOAuth2Provider = oauth2.create; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 53729ce7cc..71f58df354 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -39,11 +39,11 @@ import { } from '../../lib/passport'; import { AuthHandler, - AuthProviderFactory, AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken?: string; @@ -209,55 +209,68 @@ export type OidcProviderOptions = { }; }; -export const createOidcProvider = (options?: { - authHandler?: AuthHandler; +/** + * Auth provider integration for generic OpenID Connect auth + * + * @public + */ +export const oidc = createAuthProviderIntegration({ + create(options?: { + authHandler?: AuthHandler; - signIn?: { - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const metadataUrl = envConfig.getString('metadataUrl'); - const tokenSignedResponseAlg = envConfig.getOptionalString( - 'tokenSignedResponseAlg', - ); - const scope = envConfig.getOptionalString('scope'); - const prompt = envConfig.getOptionalString('prompt'); + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const metadataUrl = envConfig.getString('metadataUrl'); + const tokenSignedResponseAlg = envConfig.getOptionalString( + 'tokenSignedResponseAlg', + ); + const scope = envConfig.getOptionalString('scope'); + const prompt = envConfig.getOptionalString('prompt'); - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ userinfo }) => ({ - profile: { - displayName: userinfo.name, - email: userinfo.email, - picture: userinfo.picture, - }, - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ userinfo }) => ({ + profile: { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }, + }); - const provider = new OidcAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenSignedResponseAlg, - metadataUrl, - scope, - prompt, - signInResolver: options?.signIn?.resolver, - authHandler, - resolverContext, + const provider = new OidcAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenSignedResponseAlg, + metadataUrl, + scope, + prompt, + signInResolver: options?.signIn?.resolver, + authHandler, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.oidc.create` instead + */ +export const createOidcProvider = oidc.create; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 606a9bbce7..bbfa4d05d8 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -37,12 +37,12 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderFactory, AuthHandler, RedirectInfo, SignInResolver, AuthResolverContext, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { StateStore } from 'passport-oauth2'; type PrivateInfo = { @@ -226,60 +226,73 @@ export type OktaProviderOptions = { }; }; -export const createOktaProvider = (_options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for Okta auth + * + * @public + */ +export const okta = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + authHandler?: AuthHandler; - // This is a safe assumption as `passport-okta-oauth` uses the audience - // as the base for building the authorization, token, and user info URLs. - // https://github.com/fischerdan/passport-okta-oauth/blob/ea9ac42d/lib/passport-okta-oauth/oauth2.js#L12-L14 - if (!audience.startsWith('https://')) { - throw new Error("URL for 'audience' must start with 'https://'."); - } + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authHandler: AuthHandler = _options?.authHandler - ? _options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + // This is a safe assumption as `passport-okta-oauth` uses the audience + // as the base for building the authorization, token, and user info URLs. + // https://github.com/fischerdan/passport-okta-oauth/blob/ea9ac42d/lib/passport-okta-oauth/oauth2.js#L12-L14 + if (!audience.startsWith('https://')) { + throw new Error("URL for 'audience' must start with 'https://'."); + } - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - authHandler, - signInResolver: _options?.signIn?.resolver, - resolverContext, + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.okta.create` instead + */ +export const createOktaProvider = okta.create; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 05699fcb17..09e3eeeddb 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -38,11 +38,11 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, AuthHandler, SignInResolver, AuthResolverContext, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -187,54 +187,66 @@ export type OneLoginProviderOptions = { }; }; -/** @public */ -export const createOneLoginProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for OneLogin auth + * + * @public + */ +export const onelogin = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const issuer = envConfig.getString('issuer'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; + authHandler?: AuthHandler; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const issuer = envConfig.getString('issuer'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const provider = new OneLoginProvider({ - clientId, - clientSecret, - callbackUrl, - issuer, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new OneLoginProvider({ + clientId, + clientSecret, + callbackUrl, + issuer, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.onelogin.create` instead + */ +export const createOneLoginProvider = onelogin.create; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 592510acf5..3009b6bedf 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -28,13 +28,13 @@ import { } from '../../lib/passport'; import { AuthProviderRouteHandlers, - AuthProviderFactory, AuthHandler, SignInResolver, AuthResponse, AuthResolverContext, } from '../types'; import { postMessageResponse } from '../../lib/flow'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { AuthenticationError, isError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; @@ -167,55 +167,67 @@ export type SamlProviderOptions = { }; }; -/** @public */ -export const createSamlProvider = (options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { +/** + * Auth provider integration for SAML auth + * + * @public + */ +export const saml = createAuthProviderIntegration({ + create(options?: { /** - * Maps an auth result to a Backstage identity for the user. + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. */ - resolver: SignInResolver; - }; -}): AuthProviderFactory => { - return ({ providerId, globalConfig, config, resolverContext }) => { - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: { - email: fullProfile.email, - displayName: fullProfile.displayName, - }, - }); + authHandler?: AuthHandler; - return new SamlAuthProvider({ - callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, - entryPoint: config.getString('entryPoint'), - logoutUrl: config.getOptionalString('logoutUrl'), - audience: config.getOptionalString('audience'), - issuer: config.getString('issuer'), - cert: config.getString('cert'), - privateKey: config.getOptionalString('privateKey'), - authnContext: config.getOptionalStringArray('authnContext'), - identifierFormat: config.getOptionalString('identifierFormat'), - decryptionPvk: config.getOptionalString('decryptionPvk'), - signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as - | SignatureAlgorithm - | undefined, - digestAlgorithm: config.getOptionalString('digestAlgorithm'), - acceptedClockSkewMs: config.getOptionalNumber('acceptedClockSkewMs'), + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => { + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: { + email: fullProfile.email, + displayName: fullProfile.displayName, + }, + }); - appUrl: globalConfig.appUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, - }); - }; -}; + return new SamlAuthProvider({ + callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, + entryPoint: config.getString('entryPoint'), + logoutUrl: config.getOptionalString('logoutUrl'), + audience: config.getOptionalString('audience'), + issuer: config.getString('issuer'), + cert: config.getString('cert'), + privateKey: config.getOptionalString('privateKey'), + authnContext: config.getOptionalStringArray('authnContext'), + identifierFormat: config.getOptionalString('identifierFormat'), + decryptionPvk: config.getOptionalString('decryptionPvk'), + signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as + | SignatureAlgorithm + | undefined, + digestAlgorithm: config.getOptionalString('digestAlgorithm'), + acceptedClockSkewMs: config.getOptionalNumber('acceptedClockSkewMs'), + + appUrl: globalConfig.appUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + }; + }, +}); + +/** + * @public + * @deprecated Use `providers.saml.create` instead + */ +export const createSamlProvider = saml.create; From e675cf879d519055c330c10a5657a4f047352e88 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:43:02 +0200 Subject: [PATCH 33/49] auth-backend: populate exported providers object Signed-off-by: Patrik Oldsberg --- .../auth-backend/src/providers/providers.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index ffe833ff47..002a8eb9ae 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -14,8 +14,36 @@ * limitations under the License. */ +import { atlassian } from './atlassian/provider'; +import { auth0 } from './auth0/provider'; +import { awsAlb } from './aws-alb/provider'; +import { bitbucket } from './bitbucket/provider'; +import { gcpIap } from './gcp-iap/provider'; +import { github } from './github/provider'; +import { gitlab } from './gitlab/provider'; import { google } from './google/provider'; +import { microsoft } from './microsoft/provider'; +import { oauth2 } from './oauth2/provider'; +import { oauth2Proxy } from './oauth2-proxy/provider'; +import { oidc } from './oidc/provider'; +import { okta } from './okta/provider'; +import { onelogin } from './onelogin/provider'; +import { saml } from './saml/provider'; export const providers = Object.freeze({ + atlassian, + auth0, + awsAlb, + bitbucket, + gcpIap, + github, + gitlab, google, + microsoft, + oauth2, + oauth2Proxy, + oidc, + okta, + onelogin, + saml, }); From 39295c1e1c0c939478a7b62e3298eff2435e0b57 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:50:51 +0200 Subject: [PATCH 34/49] auth-backend: migrate bitbucket sign-in resolvers Signed-off-by: Patrik Oldsberg --- .../src/providers/bitbucket/provider.ts | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 7a3e6c9a60..c448c222b4 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -192,38 +192,6 @@ export class BitbucketAuthProvider implements OAuthHandlers { } } -export const bitbucketUsernameSignInResolver: SignInResolver< - BitbucketOAuthResult -> = async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile.username) { - throw new Error('Bitbucket profile contained no Username'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'bitbucket.org/username': result.fullProfile.username, - }, - }); -}; - -export const bitbucketUserIdSignInResolver: SignInResolver< - BitbucketOAuthResult -> = async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile.id) { - throw new Error('Bitbucket profile contained no User ID'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'bitbucket.org/user-id': result.fullProfile.id, - }, - }); -}; - /** * @deprecated This type has been inlined into the create method and will be removed. */ @@ -300,6 +268,44 @@ export const bitbucket = createAuthProviderIntegration({ }); }); }, + resolvers: { + /** + * Looks up the user by matching their username to the `bitbucket.org/username` annotation. + */ + lookupUsernameAnnotation(): SignInResolver { + return async (info, ctx) => { + const { result } = info; + + if (!result.fullProfile.username) { + throw new Error('Bitbucket profile contained no Username'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'bitbucket.org/username': result.fullProfile.username, + }, + }); + }; + }, + /** + * Looks up the user by matching their user ID to the `bitbucket.org/user-id` annotation. + */ + lookupUserIdAnnotation(): SignInResolver { + return async (info, ctx) => { + const { result } = info; + + if (!result.fullProfile.id) { + throw new Error('Bitbucket profile contained no User ID'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'bitbucket.org/user-id': result.fullProfile.id, + }, + }); + }; + }, + }, }); /** @@ -307,3 +313,17 @@ export const bitbucket = createAuthProviderIntegration({ * @deprecated Use `providers.bitbucket.create` instead */ export const createBitbucketProvider = bitbucket.create; + +/** + * @public + * @deprecated Use `providers.bitbucket.resolvers.lookupUsernameAnnotation()` instead. + */ +export const bitbucketUsernameSignInResolver = + bitbucket.resolvers.lookupUsernameAnnotation(); + +/** + * @public + * @deprecated Use `providers.bitbucket.resolvers.lookupUserIdAnnotation()` instead. + */ +export const bitbucketUserIdSignInResolver = + bitbucket.resolvers.lookupUserIdAnnotation(); From ee08d32106dfa004055f1f14b133ed4299e5a3e1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 14:56:24 +0200 Subject: [PATCH 35/49] auth-backend: update google provider to match other providers Signed-off-by: Patrik Oldsberg --- .../src/providers/google/provider.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 10187c4d97..bc05104ec6 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -17,7 +17,6 @@ import express from 'express'; import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import { getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -189,6 +188,11 @@ export type GoogleProviderOptions = { }; }; +/** + * Auth provider integration for Google auth + * + * @public + */ export const google = createAuthProviderIntegration({ create(options?: { /** @@ -239,7 +243,13 @@ export const google = createAuthProviderIntegration({ }); }, resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ byEmailLocalPart: () => commonByEmailLocalPartResolver, + /** + * Looks up the user by matching their email to the `google.com/email` annotation. + */ lookupEmailAnnotation(): SignInResolver { return async (info, ctx) => { const { profile } = info; @@ -248,27 +258,25 @@ export const google = createAuthProviderIntegration({ throw new Error('Google profile contained no email'); } - const entity = await ctx.catalogIdentityClient.findUser({ + return ctx.signInWithCatalogUser({ annotations: { 'google.com/email': profile.email, }, }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; }; }, }, }); /** + * @public * @deprecated Use `providers.google.create` instead. */ export const createGoogleProvider = google.create; /** - * @deprecated Use `google.resolvers.lookupEmailAnnotation` instead. + * @public + * @deprecated Use `providers.google.resolvers.lookupEmailAnnotation()` instead. */ -export const googleEmailSignInResolver = google.resolvers.lookupEmailAnnotation; +export const googleEmailSignInResolver = + google.resolvers.lookupEmailAnnotation(); From eaf35ccd2d98f21bdb784427d79f0d681b1894e3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:03:14 +0200 Subject: [PATCH 36/49] auth-backend: migrate github sign-in resolver Signed-off-by: Patrik Oldsberg --- .../src/providers/github/index.ts | 5 +--- .../src/providers/github/provider.test.ts | 8 ++--- .../src/providers/github/provider.ts | 30 +++++++++++-------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index 3b0c605f0e..a855b13b83 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -14,8 +14,5 @@ * limitations under the License. */ -export { - createGithubProvider, - githubUsernameEntityNameSignInResolver, -} from './provider'; +export { createGithubProvider } from './provider'; export type { GithubOAuthResult, GithubProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index e425cef0ff..5cd605dcfe 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -15,11 +15,7 @@ */ import { Profile as PassportProfile } from 'passport'; -import { - GithubAuthProvider, - GithubOAuthResult, - githubUsernameEntityNameSignInResolver, -} from './provider'; +import { GithubAuthProvider, GithubOAuthResult, github } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; import { OAuthStartRequest, encodeState } from '../../lib/oauth'; @@ -42,7 +38,7 @@ describe('GithubAuthProvider', () => { token: `token-for-user:${entityRef.name}`, })), } as unknown as AuthResolverContext, - signInResolver: githubUsernameEntityNameSignInResolver, + signInResolver: github.resolvers.byUsername(), authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index d84c89261f..5f095ea414 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -227,19 +227,6 @@ export class GithubAuthProvider implements OAuthHandlers { } } -export const githubUsernameEntityNameSignInResolver: SignInResolver< - GithubOAuthResult -> = async (info, ctx) => { - const { fullProfile } = info.result; - - const userId = fullProfile.username; - if (!userId) { - throw new Error(`GitHub user profile does not contain a username`); - } - - return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); -}; - /** * @deprecated This type has been inlined into the create method and will be removed. */ @@ -375,6 +362,23 @@ export const github = createAuthProviderIntegration({ }); }); }, + resolvers: { + /** + * Looks up the user by matching their GitHub username to the entity name. + */ + byUsername: (): SignInResolver => { + return async (info, ctx) => { + const { fullProfile } = info.result; + + const userId = fullProfile.username; + if (!userId) { + throw new Error(`GitHub user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); + }; + }, + }, }); /** From 327c3f01eb3d321f00e0024a74abcf966bf95d25 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:12:14 +0200 Subject: [PATCH 37/49] auth-backend: migrate microsoft sign-in resolver Signed-off-by: Patrik Oldsberg --- .../src/providers/microsoft/provider.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 106ab66b6d..bbb4afae40 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -186,23 +186,6 @@ export class MicrosoftAuthProvider implements OAuthHandlers { } } -export const microsoftEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Microsoft profile contained no email'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'microsoft.com/email': profile.email, - }, - }); -}; - /** * @deprecated This type has been inlined into the create method and will be removed. */ @@ -285,6 +268,26 @@ export const microsoft = createAuthProviderIntegration({ }); }); }, + resolvers: { + /** + * Looks up the user by matching their email to the `microsoft.com/email` annotation. + */ + lookupEmailAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Microsoft profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'microsoft.com/email': profile.email, + }, + }); + }; + }, + }, }); /** @@ -292,3 +295,10 @@ export const microsoft = createAuthProviderIntegration({ * @deprecated Use `providers.microsoft.create` instead */ export const createMicrosoftProvider = microsoft.create; + +/** + * @public + * @deprecated Use `providers.microsoft.resolvers.lookupEmailAnnotation()` instead. + */ +export const microsoftEmailSignInResolver = + microsoft.resolvers.lookupEmailAnnotation(); From c0629e16ca7efb56f7a8083b9742480d800d4e6c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:15:10 +0200 Subject: [PATCH 38/49] auth-backend: migrate okta sign-in resolver Signed-off-by: Patrik Oldsberg --- .../src/providers/okta/provider.ts | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index bbfa4d05d8..edbca32c7c 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -188,23 +188,6 @@ export class OktaAuthProvider implements OAuthHandlers { } } -export const oktaEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Okta profile contained no email'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'okta.com/email': profile.email, - }, - }); -}; - /** * @deprecated This type has been inlined into the create method and will be removed. */ @@ -289,6 +272,26 @@ export const okta = createAuthProviderIntegration({ }); }); }, + resolvers: { + /** + * Looks up the user by matching their email to the `okta.com/email` annotation. + */ + lookupEmailAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Okta profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'okta.com/email': profile.email, + }, + }); + }; + }, + }, }); /** @@ -296,3 +299,9 @@ export const okta = createAuthProviderIntegration({ * @deprecated Use `providers.okta.create` instead */ export const createOktaProvider = okta.create; + +/** + * @public + * @deprecated Use `providers.okta.resolvers.lookupEmailAnnotation()` instead. + */ +export const oktaEmailSignInResolver = okta.resolvers.lookupEmailAnnotation(); From 9f03434cfbf36d176a80e0917903a675c0ed134a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:18:41 +0200 Subject: [PATCH 39/49] auth-backend: migrate saml sign-in resolver Signed-off-by: Patrik Oldsberg --- .../src/providers/saml/provider.ts | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 3009b6bedf..3f4b7a960d 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -130,20 +130,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } -export const samlNameIdEntityNameSignInResolver: SignInResolver< - SamlAuthResult -> = async (info, ctx) => { - const id = info.result.fullProfile.nameID; - - if (!id) { - throw new AuthenticationError('No nameID found in SAML response'); - } - - return ctx.signInWithCatalogUser({ - entityRef: { name: id }, - }); -}; - type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; /** @@ -224,6 +210,24 @@ export const saml = createAuthProviderIntegration({ }); }; }, + resolvers: { + /** + * Looks up the user by matching their nameID to the entity name. + */ + byNameId(): SignInResolver { + return async (info, ctx) => { + const id = info.result.fullProfile.nameID; + + if (!id) { + throw new AuthenticationError('No nameID found in SAML response'); + } + + return ctx.signInWithCatalogUser({ + entityRef: { name: id }, + }); + }; + }, + }, }); /** @@ -231,3 +235,9 @@ export const saml = createAuthProviderIntegration({ * @deprecated Use `providers.saml.create` instead */ export const createSamlProvider = saml.create; + +/** + * @public + * @deprecated Use `providers.saml.resolvers.byNameId()` instead. + */ +export const samlNameIdEntityNameSignInResolver = saml.resolvers.byNameId(); From c6899b490211882dcb446f49865ad6d3f500b2f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:20:41 +0200 Subject: [PATCH 40/49] auth-backend: modernize the common local part resolver Signed-off-by: Patrik Oldsberg --- .../auth-backend/src/providers/resolvers.ts | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts index dd9a12f90a..8a2785099f 100644 --- a/plugins/auth-backend/src/providers/resolvers.ts +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import { SignInResolver } from './types'; +/** + * A common sign-in resolver that looks up the user using the local part of + * their email address as the entity name. + */ export const commonByEmailLocalPartResolver: SignInResolver = async ( info, ctx, @@ -29,23 +29,9 @@ export const commonByEmailLocalPartResolver: SignInResolver = async ( if (!profile.email) { throw new Error('Login failed, user profile does not contain an email'); } - const [userId] = profile.email.split('@'); + const [localPart] = profile.email.split('@'); - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, + return ctx.signInWithCatalogUser({ + entityRef: { name: localPart }, }); - const ownershipEntityRefs = - await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [entityRef], - }); - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: ownershipEntityRefs, - }, - }); - - return { id: userId, token }; }; From f30c2b0757ebf67f56a6232604a9e47e32cb14af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:23:32 +0200 Subject: [PATCH 41/49] auth-backend: update API report Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 504 +++++++++++++++++++++++------ 1 file changed, 411 insertions(+), 93 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 19fa69a6aa..8cfc55c513 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -46,7 +46,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { // Warning: (ae-missing-release-tag) "AtlassianProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type AtlassianProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -54,7 +54,9 @@ export type AtlassianProviderOptions = { }; }; -// @public (undocumented) +// Warning: (ae-missing-release-tag) "Auth0ProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export type Auth0ProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -73,6 +75,16 @@ export type AuthHandlerResult = { profile: ProfileInfo; }; +// Warning: (ae-missing-release-tag) "AuthProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AuthProviderConfig = { + baseUrl: string; + appUrl: string; + isOriginAllowed: (origin: string) => boolean; + cookieConfigurer?: CookieConfigurer; +}; + // Warning: (ae-missing-release-tag) "AuthProviderFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -80,8 +92,8 @@ export type AuthProviderFactory = (options: { providerId: string; globalConfig: AuthProviderConfig; config: Config; - resolverContext: AuthResolverContext; logger: Logger; + resolverContext: AuthResolverContext; tokenManager: TokenManager; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; @@ -159,7 +171,7 @@ export type AuthResponse = { // Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type AwsAlbProviderOptions = { authHandler?: AuthHandler; signIn: { @@ -200,7 +212,7 @@ export type BitbucketPassportProfile = Profile & { // Warning: (ae-missing-release-tag) "BitbucketProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -208,15 +220,11 @@ export type BitbucketProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "bitbucketUserIdSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const bitbucketUserIdSignInResolver: SignInResolver; +// @public @deprecated (undocumented) +export const bitbucketUserIdSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "bitbucketUsernameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const bitbucketUsernameSignInResolver: SignInResolver; +// @public @deprecated (undocumented) +export const bitbucketUsernameSignInResolver: SignInResolver; // Warning: (ae-missing-release-tag) "CatalogIdentityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -240,53 +248,101 @@ export type CookieConfigurer = (ctx: { secure: boolean; }; -// Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createAtlassianProvider: ( - options?: AtlassianProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public (undocumented) +// Warning: (ae-missing-release-tag) "createAuth0Provider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export const createAuth0Provider: ( - options?: Auth0ProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const createAwsAlbProvider: ( - options?: AwsAlbProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createBitbucketProvider: ( - options?: BitbucketProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public -export function createGcpIapProvider( - options: GcpIapProviderOptions, -): AuthProviderFactory; +// @public @deprecated (undocumented) +export const createGcpIapProvider: (options: { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; +}) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createGithubProvider: ( - options?: GithubProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + stateEncoder?: StateEncoder | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createGitlabProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createGitlabProvider: ( - options?: GitlabProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createGoogleProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const createGoogleProvider: ( options?: @@ -301,42 +357,82 @@ export const createGoogleProvider: ( | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createMicrosoftProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createMicrosoftProvider: ( - options?: MicrosoftProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createOAuth2Provider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createOAuth2Provider: ( - options?: OAuth2ProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public -export const createOauth2ProxyProvider: ( - options: Oauth2ProxyProviderOptions, -) => AuthProviderFactory; +// @public @deprecated (undocumented) +export const createOauth2ProxyProvider: (options: { + authHandler: AuthHandler>; + signIn: { + resolver: SignInResolver>; + }; +}) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createOidcProvider: ( - options?: OidcProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createOktaProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createOktaProvider: ( - _options?: OktaProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public (undocumented) +// @public @deprecated (undocumented) export const createOneLoginProvider: ( - options?: OneLoginProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -349,9 +445,18 @@ export function createOriginFilter(config: Config): (origin: string) => boolean; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; -// @public (undocumented) +// @public @deprecated (undocumented) export const createSamlProvider: ( - options?: SamlProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -371,7 +476,9 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public +// Warning: (ae-missing-release-tag) "GcpIapProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export type GcpIapProviderOptions = { authHandler?: AuthHandler; signIn: { @@ -415,7 +522,7 @@ export type GithubOAuthResult = { // Warning: (ae-missing-release-tag) "GithubProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type GithubProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -424,14 +531,9 @@ export type GithubProviderOptions = { stateEncoder?: StateEncoder; }; -// Warning: (ae-missing-release-tag) "githubUsernameEntityNameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const githubUsernameEntityNameSignInResolver: SignInResolver; - // Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type GitlabProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -439,10 +541,8 @@ export type GitlabProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "googleEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) -export const googleEmailSignInResolver: () => SignInResolver; +export const googleEmailSignInResolver: SignInResolver; // Warning: (ae-missing-release-tag) "GoogleProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -454,14 +554,12 @@ export type GoogleProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "microsoftEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const microsoftEmailSignInResolver: SignInResolver; // Warning: (ae-missing-release-tag) "MicrosoftProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type MicrosoftProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -471,7 +569,7 @@ export type MicrosoftProviderOptions = { // Warning: (ae-missing-release-tag) "OAuth2ProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuth2ProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -479,7 +577,9 @@ export type OAuth2ProviderOptions = { }; }; -// @public +// Warning: (ae-missing-release-tag) "Oauth2ProxyProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export type Oauth2ProxyProviderOptions = { authHandler: AuthHandler>; signIn: { @@ -629,7 +729,9 @@ export type OidcAuthResult = { userinfo: UserinfoResponse; }; -// @public +// Warning: (ae-missing-release-tag) "OidcProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export type OidcProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -637,14 +739,12 @@ export type OidcProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const oktaEmailSignInResolver: SignInResolver; // Warning: (ae-missing-release-tag) "OktaProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type OktaProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -652,7 +752,9 @@ export type OktaProviderOptions = { }; }; -// @public (undocumented) +// Warning: (ae-missing-release-tag) "OneLoginProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export type OneLoginProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -685,6 +787,109 @@ export type ProfileInfo = { // // @public (undocumented) export const providers: Readonly<{ + atlassian: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + auth0: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + awsAlb: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + bitbucket: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + lookupUsernameAnnotation(): SignInResolver; + lookupUserIdAnnotation(): SignInResolver; + }>; + }>; + gcpIap: Readonly<{ + create: (options: { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + }) => AuthProviderFactory; + resolvers: never; + }>; + github: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + stateEncoder?: StateEncoder | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + byUsername: () => SignInResolver; + }>; + }>; + gitlab: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; google: Readonly<{ create: ( options?: @@ -703,6 +908,111 @@ export const providers: Readonly<{ lookupEmailAnnotation(): SignInResolver; }>; }>; + microsoft: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + lookupEmailAnnotation(): SignInResolver; + }>; + }>; + oauth2: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + oauth2Proxy: Readonly<{ + create: (options: { + authHandler: AuthHandler>; + signIn: { + resolver: SignInResolver>; + }; + }) => AuthProviderFactory; + resolvers: never; + }>; + oidc: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + okta: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + lookupEmailAnnotation(): SignInResolver; + }>; + }>; + onelogin: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + saml: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + byNameId(): SignInResolver; + }>; + }>; }>; // Warning: (ae-missing-release-tag) "readState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -735,12 +1045,12 @@ export type SamlAuthResult = { fullProfile: any; }; -// Warning: (ae-missing-release-tag) "samlNameIdEntityNameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const samlNameIdEntityNameSignInResolver: SignInResolver; -// @public (undocumented) +// Warning: (ae-missing-release-tag) "SamlProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public @deprecated (undocumented) export type SamlProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -770,6 +1080,16 @@ export type TokenIssuer = { }>; }; +// Warning: (ae-missing-release-tag) "TokenParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type TokenParams = { + claims: { + sub: string; + ent?: string[]; + }; +}; + // Warning: (ae-missing-release-tag) "verifyNonce" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -791,8 +1111,6 @@ export type WebMessageResponse = // Warnings were encountered during analysis: // // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:50:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:180:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/aws-alb/provider.d.ts:73:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts +// src/providers/github/provider.d.ts:175:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts ``` From 0f201ad50e51f07767a4275664b0e3a7897da160 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 15:39:15 +0200 Subject: [PATCH 42/49] auth-backend: API report cleanup Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 68 +++++-------------- plugins/auth-backend/src/identity/types.ts | 10 ++- .../src/providers/atlassian/provider.ts | 1 + .../src/providers/auth0/provider.ts | 2 + .../src/providers/aws-alb/index.ts | 2 +- .../src/providers/aws-alb/provider.ts | 6 ++ .../src/providers/bitbucket/provider.ts | 1 + .../src/providers/gcp-iap/types.ts | 1 + .../src/providers/github/provider.ts | 1 + .../src/providers/gitlab/provider.ts | 1 + .../src/providers/google/provider.ts | 1 + plugins/auth-backend/src/providers/index.ts | 9 +-- .../src/providers/microsoft/provider.ts | 1 + .../src/providers/oauth2-proxy/provider.ts | 1 + .../src/providers/oauth2/provider.ts | 1 + .../src/providers/oidc/provider.ts | 1 + .../src/providers/okta/provider.ts | 1 + .../src/providers/onelogin/provider.ts | 1 + .../auth-backend/src/providers/providers.ts | 5 ++ .../src/providers/saml/provider.ts | 1 + plugins/auth-backend/src/providers/types.ts | 5 ++ 21 files changed, 60 insertions(+), 60 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 8cfc55c513..fb5f8ce86d 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -44,8 +44,6 @@ export class AtlassianAuthProvider implements OAuthHandlers { start(req: OAuthStartRequest): Promise; } -// Warning: (ae-missing-release-tag) "AtlassianProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type AtlassianProviderOptions = { authHandler?: AuthHandler; @@ -54,8 +52,6 @@ export type AtlassianProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "Auth0ProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type Auth0ProviderOptions = { authHandler?: AuthHandler; @@ -75,8 +71,6 @@ export type AuthHandlerResult = { profile: ProfileInfo; }; -// Warning: (ae-missing-release-tag) "AuthProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AuthProviderConfig = { baseUrl: string; @@ -124,8 +118,6 @@ export interface AuthProviderRouteHandlers { start(req: express.Request, res: express.Response): Promise; } -// Warning: (ae-missing-release-tag) "AuthResolverCatalogUserQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AuthResolverCatalogUserQuery = | { @@ -160,8 +152,6 @@ export type AuthResolverContext = { ): Promise; }; -// Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AuthResponse = { providerInfo: ProviderInfo; @@ -169,8 +159,6 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; -// Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type AwsAlbProviderOptions = { authHandler?: AuthHandler; @@ -179,6 +167,13 @@ export type AwsAlbProviderOptions = { }; }; +// @public (undocumented) +export type AwsAlbResult = { + fullProfile: Profile; + expiresInSeconds?: number; + accessToken: string; +}; + // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -210,8 +205,6 @@ export type BitbucketPassportProfile = Profile & { }; }; -// Warning: (ae-missing-release-tag) "BitbucketProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type BitbucketProviderOptions = { authHandler?: AuthHandler; @@ -262,8 +255,6 @@ export const createAtlassianProvider: ( | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createAuth0Provider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const createAuth0Provider: ( options?: @@ -278,9 +269,7 @@ export const createAuth0Provider: ( | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createAwsAlbProvider: ( options?: | { @@ -476,8 +465,6 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// Warning: (ae-missing-release-tag) "GcpIapProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type GcpIapProviderOptions = { authHandler?: AuthHandler; @@ -520,8 +507,6 @@ export type GithubOAuthResult = { refreshToken?: string; }; -// Warning: (ae-missing-release-tag) "GithubProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type GithubProviderOptions = { authHandler?: AuthHandler; @@ -531,8 +516,6 @@ export type GithubProviderOptions = { stateEncoder?: StateEncoder; }; -// Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type GitlabProviderOptions = { authHandler?: AuthHandler; @@ -544,8 +527,6 @@ export type GitlabProviderOptions = { // @public @deprecated (undocumented) export const googleEmailSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "GoogleProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type GoogleProviderOptions = { authHandler?: AuthHandler; @@ -557,8 +538,6 @@ export type GoogleProviderOptions = { // @public @deprecated (undocumented) export const microsoftEmailSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "MicrosoftProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type MicrosoftProviderOptions = { authHandler?: AuthHandler; @@ -567,8 +546,6 @@ export type MicrosoftProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "OAuth2ProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type OAuth2ProviderOptions = { authHandler?: AuthHandler; @@ -577,8 +554,6 @@ export type OAuth2ProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "Oauth2ProxyProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type Oauth2ProxyProviderOptions = { authHandler: AuthHandler>; @@ -729,8 +704,6 @@ export type OidcAuthResult = { userinfo: UserinfoResponse; }; -// Warning: (ae-missing-release-tag) "OidcProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type OidcProviderOptions = { authHandler?: AuthHandler; @@ -742,8 +715,6 @@ export type OidcProviderOptions = { // @public @deprecated (undocumented) export const oktaEmailSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "OktaProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type OktaProviderOptions = { authHandler?: AuthHandler; @@ -752,8 +723,6 @@ export type OktaProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "OneLoginProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type OneLoginProviderOptions = { authHandler?: AuthHandler; @@ -783,9 +752,7 @@ export type ProfileInfo = { picture?: string; }; -// Warning: (ae-missing-release-tag) "providers" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const providers: Readonly<{ atlassian: Readonly<{ create: ( @@ -1048,8 +1015,6 @@ export type SamlAuthResult = { // @public @deprecated (undocumented) export const samlNameIdEntityNameSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "SamlProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export type SamlProviderOptions = { authHandler?: AuthHandler; @@ -1070,9 +1035,12 @@ export type SignInResolver = ( context: AuthResolverContext, ) => Promise; -// Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @public (undocumented) +export type StateEncoder = (req: OAuthStartRequest) => Promise<{ + encodedState: string; +}>; + +// @public @deprecated export type TokenIssuer = { issueToken(params: TokenParams): Promise; listPublicKeys(): Promise<{ @@ -1080,8 +1048,6 @@ export type TokenIssuer = { }>; }; -// Warning: (ae-missing-release-tag) "TokenParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type TokenParams = { claims: { @@ -1110,7 +1076,5 @@ export type WebMessageResponse = // Warnings were encountered during analysis: // -// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/aws-alb/provider.d.ts:73:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:175:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts +// src/identity/types.d.ts:38:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index faafd2b68e..5a350284e5 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -22,7 +22,11 @@ export interface AnyJWK extends Record { kty: string; } -/** Parameters used to issue new ID Tokens */ +/** + * Parameters used to issue new ID Tokens + * + * @public + */ export type TokenParams = { /** The claims that will be embedded within the token */ claims: { @@ -33,8 +37,12 @@ export type TokenParams = { }; }; +// TODO(Rugvip): This should at least be made internal /** * A TokenIssuer is able to issue verifiable ID Tokens on demand. + * + * @public + * @deprecated This interface is deprecated and will be removed in a future release. */ export type TokenIssuer = { /** diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 1cb83d4589..2aa85dab96 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -162,6 +162,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type AtlassianProviderOptions = { diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index beb2b5fba4..b4446caa95 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -168,6 +168,7 @@ export class Auth0AuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type Auth0ProviderOptions = { @@ -249,6 +250,7 @@ export const auth0 = createAuthProviderIntegration({ }); /** + * @public * @deprecated Use `providers.auth0.create` instead. */ export const createAuth0Provider = auth0.create; diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index a8130b3575..710d86ca3d 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -15,4 +15,4 @@ */ export { createAwsAlbProvider } from './provider'; -export type { AwsAlbProviderOptions } from './provider'; +export type { AwsAlbProviderOptions, AwsAlbResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index ac699d6eda..741058ac7a 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -69,6 +69,7 @@ export type AwsAlbClaims = { iss: string; }; +/** @public */ export type AwsAlbResult = { fullProfile: PassportProfile; expiresInSeconds?: number; @@ -209,6 +210,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type AwsAlbProviderOptions = { @@ -279,4 +281,8 @@ export const awsAlb = createAuthProviderIntegration({ }, }); +/** + * @public + * @deprecated Use `providers.awsAlb.create` instead + */ export const createAwsAlbProvider = awsAlb.create; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index c448c222b4..c26c501666 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -193,6 +193,7 @@ export class BitbucketAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type BitbucketProviderOptions = { diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 3ef8c049b9..6e5a022a3d 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -71,6 +71,7 @@ export type GcpIapProviderInfo = { export type GcpIapResponse = AuthResponse; /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type GcpIapProviderOptions = { diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 5f095ea414..9cc3a32b90 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -228,6 +228,7 @@ export class GithubAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type GithubProviderOptions = { diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index b8de3845a5..0cff94a0d5 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -179,6 +179,7 @@ export class GitlabAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type GitlabProviderOptions = { diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index bc05104ec6..277c3144b0 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -168,6 +168,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type GoogleProviderOptions = { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index c3ecfe8913..1f33a60e55 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -34,8 +34,6 @@ export { providers } from './providers'; export { factories as defaultAuthProviderFactories } from './factories'; -// Export the minimal interface required for implementing a -// custom Authorization Handler export type { AuthProviderConfig, AuthProviderRouteHandlers, @@ -48,10 +46,9 @@ export type { SignInResolver, SignInInfo, CookieConfigurer, + StateEncoder, + AuthResponse, + ProfileInfo, } from './types'; -// These types are needed for a postMessage from the login pop-up -// to the frontend -export type { AuthResponse, ProfileInfo } from './types'; - export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index bbb4afae40..bbd17944ee 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -187,6 +187,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type MicrosoftProviderOptions = { diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index a7a8befd5e..374811198f 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -49,6 +49,7 @@ export type OAuth2ProxyResult = { }; /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type Oauth2ProxyProviderOptions = { diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 7af10a522f..8b0eac948a 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -186,6 +186,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type OAuth2ProviderOptions = { diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 71f58df354..ed6bdf8c34 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -199,6 +199,7 @@ export class OidcAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type OidcProviderOptions = { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index edbca32c7c..471052b832 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -189,6 +189,7 @@ export class OktaAuthProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type OktaProviderOptions = { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 09e3eeeddb..06a57e4560 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -167,6 +167,7 @@ export class OneLoginProvider implements OAuthHandlers { } /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type OneLoginProviderOptions = { diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index 002a8eb9ae..3d49e2bbbb 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -30,6 +30,11 @@ import { okta } from './okta/provider'; import { onelogin } from './onelogin/provider'; import { saml } from './saml/provider'; +/** + * All built-in auth provider integrations. + * + * @public + */ export const providers = Object.freeze({ atlassian, auth0, diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 3f4b7a960d..597dd24687 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -133,6 +133,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; /** + * @public * @deprecated This type has been inlined into the create method and will be removed. */ export type SamlProviderOptions = { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index b1e45bc8a9..128dea12ef 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -43,6 +43,8 @@ import { Entity } from '@backstage/catalog-model'; * * Regardless of the query method, the query must match exactly one entity * in the catalog, or an error will be thrown. + * + * @public */ export type AuthResolverCatalogUserQuery = | { @@ -112,6 +114,7 @@ export type CookieConfigurer = (ctx: { callbackUrl: string; }) => { domain: string; path: string; secure: boolean }; +/** @public */ export type AuthProviderConfig = { /** * The protocol://domain[:port] where the app is hosted. This is used to construct the @@ -232,6 +235,7 @@ export type AuthProviderFactory = (options: { catalogApi: CatalogApi; }) => AuthProviderRouteHandlers; +/** @public */ export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; @@ -319,6 +323,7 @@ export type AuthHandler = ( context: AuthResolverContext, ) => Promise; +/** @public */ export type StateEncoder = ( req: OAuthStartRequest, ) => Promise<{ encodedState: string }>; From c5aeaf339d9a1fa88059959affc7f071b233eee5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 16:24:38 +0200 Subject: [PATCH 43/49] changesets: added changesets for auth-backend changes Signed-off-by: Patrik Oldsberg --- .changeset/big-teachers-count.md | 5 +++ .changeset/four-onions-pretend.md | 5 +++ .changeset/loud-bags-run.md | 62 +++++++++++++++++++++++++++++ .changeset/neat-countries-hammer.md | 58 +++++++++++++++++++++++++++ .changeset/six-ravens-behave.md | 5 +++ .changeset/sour-kiwis-impress.md | 5 +++ 6 files changed, 140 insertions(+) create mode 100644 .changeset/big-teachers-count.md create mode 100644 .changeset/four-onions-pretend.md create mode 100644 .changeset/loud-bags-run.md create mode 100644 .changeset/neat-countries-hammer.md create mode 100644 .changeset/six-ravens-behave.md create mode 100644 .changeset/sour-kiwis-impress.md diff --git a/.changeset/big-teachers-count.md b/.changeset/big-teachers-count.md new file mode 100644 index 0000000000..5a8c632ab5 --- /dev/null +++ b/.changeset/big-teachers-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +**DEPRECATION**: The `AuthProviderFactoryOptions` type has been deprecated, as the options are now instead inlined in the `AuthProviderFactory` type. This will make it possible to more easily introduce new options in the future without a possibly breaking change. diff --git a/.changeset/four-onions-pretend.md b/.changeset/four-onions-pretend.md new file mode 100644 index 0000000000..db38cc2178 --- /dev/null +++ b/.changeset/four-onions-pretend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +**DEPRECATION**: The `getEntityClaims` helper has been deprecated, with `getDefaultOwnershipEntityRefs` being added to replace it. diff --git a/.changeset/loud-bags-run.md b/.changeset/loud-bags-run.md new file mode 100644 index 0000000000..7d8bb18be0 --- /dev/null +++ b/.changeset/loud-bags-run.md @@ -0,0 +1,62 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +**DEPRECATION**: All `createProvider` and `*SignInResolver` have been deprecated. Instead, a single `providers` object is exported which contains all built-in auth providers. + +If you have a setup that currently looks for example like this: + +```ts +import { + createRouter, + defaultAuthProviderFactories, + createGoogleProvider, + googleEmailSignInResolver, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: createGoogleProvider({ + signIn: { + resolver: googleEmailSignInResolver, + }, + }), + }, + }); +} +``` + +You would migrate it to something like this: + +```ts +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: providers.google.create({ + signIn: { + resolver: providers.google.resolvers.lookupEmailAnnotation(), + }, + }), + }, + }); +} +``` diff --git a/.changeset/neat-countries-hammer.md b/.changeset/neat-countries-hammer.md new file mode 100644 index 0000000000..a9b39f44cf --- /dev/null +++ b/.changeset/neat-countries-hammer.md @@ -0,0 +1,58 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +**DEPRECATION** The `AuthResolverContext` has received a number of changes, which is the context used by auth handlers and sign-in resolvers. + +The following fields deprecated: `logger`, `tokenIssuer`, `catalogIdentityClient`. If you need to access the `logger`, you can do so through a closure instead. The `tokenIssuer` has been replaced with an `issueToken` method, which is available directory on the context. The `catalogIdentityClient` has been replaced by the `signInWithCatalogUser` method, as well as the lower level `findCatalogUser` method and `getDefaultOwnershipEntityRefs` helper. + +It should be possible to migrate most sign-in resolvers to more or less only use `signInWithCatalogUser`, for example an email lookup resolver like this one: + +```ts +async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; +}; +``` + +can be migrated to the following: + +```ts +async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); +}; +``` + +While a direct entity name lookup using a user ID might look like this: + +```ts +async ({ result: { fullProfile } }, ctx) => { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.userId, + }, + }); +}; +``` + +If you want more control over the way that users are looked up, ownership is assigned, or tokens are issued, you can use a combination of the `findCatalogUser`, `getDefaultOwnershipEntityRefs`, and `issueToken` instead. diff --git a/.changeset/six-ravens-behave.md b/.changeset/six-ravens-behave.md new file mode 100644 index 0000000000..ad3ee8815d --- /dev/null +++ b/.changeset/six-ravens-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +**BREAKING**: All auth providers have had their default sign-in resolvers removed. This means that if you want to use a particular provider for sign-in, you must provide an explicit sign-in resolver. For more information on how to configure sign-in resolvers, see the [sign-in resolver documentation](https://backstage.io/docs/auth/identity-resolver). diff --git a/.changeset/sour-kiwis-impress.md b/.changeset/sour-kiwis-impress.md new file mode 100644 index 0000000000..44c7e50fe1 --- /dev/null +++ b/.changeset/sour-kiwis-impress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added exports of the following types: `AuthProviderConfig`, `StateEncoder`, `TokenParams`, `AwsAlbResult`. From 5d14f55b71a05a29694ca13f558977eee974e76c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Apr 2022 16:30:47 +0200 Subject: [PATCH 44/49] backend: update auth plugin setup Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/auth.ts | 52 +++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 39a14139e6..4b7348225b 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -33,17 +33,53 @@ export default async function createPlugin( tokenManager: env.tokenManager, providerFactories: { ...defaultAuthProviderFactories, - google: providers.google.create({ + + // NOTE: DO NOT add this many resolvers in your own instance! + // It is important that each real user always gets resolved to + // the same sign-in identity. The code below will not do that. + // It is here for demo purposes only. + github: providers.github.create({ signIn: { - resolver({ profile }, ctx) { - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } + resolver: providers.github.resolvers.byUsername(), + }, + }), + gitlab: providers.gitlab.create({ + signIn: { + async resolver({ result: { fullProfile } }, ctx) { return ctx.signInWithCatalogUser({ entityRef: { - name: profile.email.split('@')[0], + name: fullProfile.id, + }, + }); + }, + }, + }), + microsoft: providers.microsoft.create({ + signIn: { + resolver: providers.microsoft.resolvers.lookupEmailAnnotation(), + }, + }), + google: providers.google.create({ + signIn: { + resolver: providers.google.resolvers.byEmailLocalPart(), + }, + }), + okta: providers.okta.create({ + signIn: { + resolver: providers.okta.resolvers.lookupEmailAnnotation(), + }, + }), + bitbucket: providers.bitbucket.create({ + signIn: { + resolver: providers.bitbucket.resolvers.lookupUsernameAnnotation(), + }, + }), + onelogin: providers.onelogin.create({ + signIn: { + async resolver({ result: { fullProfile } }, ctx) { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.id, }, }); }, From 5a0798284782c87aaeb812f5b45bc88d2b693f3c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 9 Apr 2022 10:53:16 +0200 Subject: [PATCH 45/49] auth-backend: remove dead code Signed-off-by: Patrik Oldsberg --- .../resolvers/CatalogAuthResolverContext.ts | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 1e2ec1685e..a55bf0789a 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -138,39 +138,4 @@ export class CatalogAuthResolverContext implements AuthResolverContext { }); return { token }; } - /* - async expandCatalogOwnership(query: { entityRefs: string[] }) { - const { entityRefs } = query; - - const compoundRefs = entityRefs.map(ref => - parseEntityRef(ref.toLocaleLowerCase('en-US'), { - defaultKind: 'user', - defaultNamespace: DEFAULT_NAMESPACE, - }), - ); - const stringRefs = compoundRefs.map(e => stringifyEntityRef(e)); - - const { token } = await this.tokenManager.getToken(); - const { items: entities } = await this.catalogApi.getEntities( - { - filter: compoundRefs.map(ref => ({ - kind: ref.kind, - 'metadata.namespace': ref.namespace, - 'metadata.name': ref.name, - })), - }, - { token }, - ); - - if (compoundRefs.length !== entities.length) { - const found = entities.map(e => stringifyEntityRef(e)); - const missing = stringRefs.filter(ref => !found.includes(ref)); - throw new NotFoundError(`Entities not found for refs ${missing.join()}`); - } - - const memberOf = entities.flatMap(e => getDefaultOwnershipEntityRefs(e)); - - return Array.from(new Set(memberOf)); - } -*/ } From f9ece810dc57a9a193910ad79e9ec47513362734 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 9 Apr 2022 10:53:29 +0200 Subject: [PATCH 46/49] auth-backend: remove duplicate comment Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/gitlab/provider.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 0cff94a0d5..23867f6990 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -219,12 +219,6 @@ export const gitlab = createAuthProviderIntegration({ /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ - /** - * Maps an auth result to a Backstage identity for the user. - * - * Set to `'email'` to use the default email-based sign in resolver, which will search - * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. - */ signIn?: { resolver: SignInResolver; }; From c93a63586d438d2186cffdcc1ac2a889e2693881 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Apr 2022 10:13:29 +0200 Subject: [PATCH 47/49] auth-backend: resolve context user -> User Signed-off-by: Patrik Oldsberg --- .../src/lib/resolvers/CatalogAuthResolverContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index a55bf0789a..28bc066d10 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -90,7 +90,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext { if ('entityRef' in query) { const entityRef = parseEntityRef(query.entityRef, { - defaultKind: 'user', + defaultKind: 'User', defaultNamespace: DEFAULT_NAMESPACE, }); result = await this.catalogApi.getEntityByRef(entityRef, { token }); From a479e76604d4ab74bb854713975ce584e3c0c08c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Apr 2022 10:43:43 +0200 Subject: [PATCH 48/49] auth-backend: rename all built-in resolvers Signed-off-by: Patrik Oldsberg --- .changeset/loud-bags-run.md | 3 ++- packages/backend/src/plugins/auth.ts | 14 +++++++++----- plugins/auth-backend/api-report.md | 16 ++++++++-------- .../src/providers/bitbucket/provider.ts | 12 ++++++------ .../src/providers/github/provider.test.ts | 2 +- .../src/providers/github/provider.ts | 2 +- .../src/providers/google/provider.ts | 8 ++++---- .../src/providers/microsoft/provider.ts | 6 +++--- .../auth-backend/src/providers/okta/provider.ts | 7 ++++--- .../auth-backend/src/providers/saml/provider.ts | 7 ++++--- 10 files changed, 42 insertions(+), 35 deletions(-) diff --git a/.changeset/loud-bags-run.md b/.changeset/loud-bags-run.md index 7d8bb18be0..24d9c7d288 100644 --- a/.changeset/loud-bags-run.md +++ b/.changeset/loud-bags-run.md @@ -53,7 +53,8 @@ export default async function createPlugin( ...defaultAuthProviderFactories, google: providers.google.create({ signIn: { - resolver: providers.google.resolvers.lookupEmailAnnotation(), + resolver: + providers.google.resolvers.emailMatchingUserEntityAnnotation(), }, }), }, diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 4b7348225b..8a60b4b4bd 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -40,7 +40,7 @@ export default async function createPlugin( // It is here for demo purposes only. github: providers.github.create({ signIn: { - resolver: providers.github.resolvers.byUsername(), + resolver: providers.github.resolvers.usernameMatchingUserEntityName(), }, }), gitlab: providers.gitlab.create({ @@ -56,22 +56,26 @@ export default async function createPlugin( }), microsoft: providers.microsoft.create({ signIn: { - resolver: providers.microsoft.resolvers.lookupEmailAnnotation(), + resolver: + providers.microsoft.resolvers.emailMatchingUserEntityAnnotation(), }, }), google: providers.google.create({ signIn: { - resolver: providers.google.resolvers.byEmailLocalPart(), + resolver: + providers.google.resolvers.emailLocalPartMatchingUserEntityName(), }, }), okta: providers.okta.create({ signIn: { - resolver: providers.okta.resolvers.lookupEmailAnnotation(), + resolver: + providers.okta.resolvers.emailMatchingUserEntityAnnotation(), }, }), bitbucket: providers.bitbucket.create({ signIn: { - resolver: providers.bitbucket.resolvers.lookupUsernameAnnotation(), + resolver: + providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation(), }, }), onelogin: providers.onelogin.create({ diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index fb5f8ce86d..a01a309dd4 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -811,8 +811,8 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - lookupUsernameAnnotation(): SignInResolver; - lookupUserIdAnnotation(): SignInResolver; + usernameMatchingUserEntityAnnotation(): SignInResolver; + userIdMatchingUserEntityAnnotation(): SignInResolver; }>; }>; gcpIap: Readonly<{ @@ -839,7 +839,7 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - byUsername: () => SignInResolver; + usernameMatchingUserEntityName: () => SignInResolver; }>; }>; gitlab: Readonly<{ @@ -871,8 +871,8 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - byEmailLocalPart: () => SignInResolver; - lookupEmailAnnotation(): SignInResolver; + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; }>; }>; microsoft: Readonly<{ @@ -889,7 +889,7 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - lookupEmailAnnotation(): SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; }>; }>; oauth2: Readonly<{ @@ -945,7 +945,7 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - lookupEmailAnnotation(): SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; }>; }>; onelogin: Readonly<{ @@ -977,7 +977,7 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - byNameId(): SignInResolver; + nameIdMatchingUserEntityName(): SignInResolver; }>; }>; }>; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index c26c501666..b149aa6d28 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -273,7 +273,7 @@ export const bitbucket = createAuthProviderIntegration({ /** * Looks up the user by matching their username to the `bitbucket.org/username` annotation. */ - lookupUsernameAnnotation(): SignInResolver { + usernameMatchingUserEntityAnnotation(): SignInResolver { return async (info, ctx) => { const { result } = info; @@ -291,7 +291,7 @@ export const bitbucket = createAuthProviderIntegration({ /** * Looks up the user by matching their user ID to the `bitbucket.org/user-id` annotation. */ - lookupUserIdAnnotation(): SignInResolver { + userIdMatchingUserEntityAnnotation(): SignInResolver { return async (info, ctx) => { const { result } = info; @@ -317,14 +317,14 @@ export const createBitbucketProvider = bitbucket.create; /** * @public - * @deprecated Use `providers.bitbucket.resolvers.lookupUsernameAnnotation()` instead. + * @deprecated Use `providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation()` instead. */ export const bitbucketUsernameSignInResolver = - bitbucket.resolvers.lookupUsernameAnnotation(); + bitbucket.resolvers.usernameMatchingUserEntityAnnotation(); /** * @public - * @deprecated Use `providers.bitbucket.resolvers.lookupUserIdAnnotation()` instead. + * @deprecated Use `providers.bitbucket.resolvers.userIdMatchingUserEntityAnnotation()` instead. */ export const bitbucketUserIdSignInResolver = - bitbucket.resolvers.lookupUserIdAnnotation(); + bitbucket.resolvers.userIdMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 5cd605dcfe..18690a986b 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -38,7 +38,7 @@ describe('GithubAuthProvider', () => { token: `token-for-user:${entityRef.name}`, })), } as unknown as AuthResolverContext, - signInResolver: github.resolvers.byUsername(), + signInResolver: github.resolvers.usernameMatchingUserEntityName(), authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 9cc3a32b90..e018a6cf99 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -367,7 +367,7 @@ export const github = createAuthProviderIntegration({ /** * Looks up the user by matching their GitHub username to the entity name. */ - byUsername: (): SignInResolver => { + usernameMatchingUserEntityName: (): SignInResolver => { return async (info, ctx) => { const { fullProfile } = info.result; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 277c3144b0..68925e7817 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -247,11 +247,11 @@ export const google = createAuthProviderIntegration({ /** * Looks up the user by matching their email local part to the entity name. */ - byEmailLocalPart: () => commonByEmailLocalPartResolver, + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, /** * Looks up the user by matching their email to the `google.com/email` annotation. */ - lookupEmailAnnotation(): SignInResolver { + emailMatchingUserEntityAnnotation(): SignInResolver { return async (info, ctx) => { const { profile } = info; @@ -277,7 +277,7 @@ export const createGoogleProvider = google.create; /** * @public - * @deprecated Use `providers.google.resolvers.lookupEmailAnnotation()` instead. + * @deprecated Use `providers.google.resolvers.emailMatchingUserEntityAnnotation()` instead. */ export const googleEmailSignInResolver = - google.resolvers.lookupEmailAnnotation(); + google.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index bbd17944ee..5b3e80e679 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -273,7 +273,7 @@ export const microsoft = createAuthProviderIntegration({ /** * Looks up the user by matching their email to the `microsoft.com/email` annotation. */ - lookupEmailAnnotation(): SignInResolver { + emailMatchingUserEntityAnnotation(): SignInResolver { return async (info, ctx) => { const { profile } = info; @@ -299,7 +299,7 @@ export const createMicrosoftProvider = microsoft.create; /** * @public - * @deprecated Use `providers.microsoft.resolvers.lookupEmailAnnotation()` instead. + * @deprecated Use `providers.microsoft.resolvers.emailMatchingUserEntityAnnotation()` instead. */ export const microsoftEmailSignInResolver = - microsoft.resolvers.lookupEmailAnnotation(); + microsoft.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 471052b832..b83e640731 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -277,7 +277,7 @@ export const okta = createAuthProviderIntegration({ /** * Looks up the user by matching their email to the `okta.com/email` annotation. */ - lookupEmailAnnotation(): SignInResolver { + emailMatchingUserEntityAnnotation(): SignInResolver { return async (info, ctx) => { const { profile } = info; @@ -303,6 +303,7 @@ export const createOktaProvider = okta.create; /** * @public - * @deprecated Use `providers.okta.resolvers.lookupEmailAnnotation()` instead. + * @deprecated Use `providers.okta.resolvers.emailMatchingUserEntityAnnotation()` instead. */ -export const oktaEmailSignInResolver = okta.resolvers.lookupEmailAnnotation(); +export const oktaEmailSignInResolver = + okta.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 597dd24687..4f5076b410 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -215,7 +215,7 @@ export const saml = createAuthProviderIntegration({ /** * Looks up the user by matching their nameID to the entity name. */ - byNameId(): SignInResolver { + nameIdMatchingUserEntityName(): SignInResolver { return async (info, ctx) => { const id = info.result.fullProfile.nameID; @@ -239,6 +239,7 @@ export const createSamlProvider = saml.create; /** * @public - * @deprecated Use `providers.saml.resolvers.byNameId()` instead. + * @deprecated Use `providers.saml.resolvers.nameIdMatchingUserEntityName()` instead. */ -export const samlNameIdEntityNameSignInResolver = saml.resolvers.byNameId(); +export const samlNameIdEntityNameSignInResolver = + saml.resolvers.nameIdMatchingUserEntityName(); From cd0105ff42d66e7644a941e4ef7bc34b916b7268 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Apr 2022 11:35:58 +0200 Subject: [PATCH 49/49] auth-backend: update getDefaultOwnershipEntityRefs to only consider groups Signed-off-by: Patrik Oldsberg --- .../src/lib/resolvers/CatalogAuthResolverContext.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 28bc066d10..1a1fdd7a36 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -42,7 +42,9 @@ import { CatalogIdentityClient } from '../catalog'; export function getDefaultOwnershipEntityRefs(entity: Entity) { const membershipRefs = entity.relations - ?.filter(r => r.type === RELATION_MEMBER_OF) + ?.filter( + r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'), + ) .map(r => r.targetRef) ?? []; return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));