From d09cee28ba428ea0a036929ca31ef36730988126 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 13:11:30 +0200 Subject: [PATCH 1/6] docs: add sign-in resolvers in getting started auth Signed-off-by: Himanshu Mishra --- docs/auth/identity-resolver.md | 151 ++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 61 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index c7f90d0bbf..3acdf7ea20 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -79,6 +79,7 @@ Now let's look at the example, with the rest of the commentary being made with i the code comments: ```ts +// File: packages/backend/src/plugins/auth.ts import { createRouter, providers, @@ -139,11 +140,21 @@ property of each of the auth provider integrations. For example, the Google prov a built in resolver that works just like the one we defined above: ```ts -providers.google.create({ - signIn: { - resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(), - }, -}); +// File: packages/backend/src/plugins/auth.ts +export default async function createPlugin( + // ... + return await createRouter({ + // ... + providerFactories: { + // ... + google: providers.google.create({ + signIn: { + resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(), + }, + }); + } + }) +) ``` There are also other options, like the this one that looks up a user @@ -164,40 +175,49 @@ that happens during sign-in you can replace `ctx.signInWithCatalogUser` with a s of lower-level calls: ```ts +// File: packages/backend/src/plugins/auth.ts import { getDefaultOwnershipRefs } from '@backstage/plugin-auth-backend'; -// This example only shows the resolver function itself. -async ({ profile: { email } }, ctx) => { - if (!email) { - throw new Error('User profile contained no email'); - } +export default async function createPlugin( + // ... + return await createRouter({ + // ... + providerFactories: { + // ... + google: async ({ profile: { email } }, ctx) => { + if (!email) { + throw new Error('User profile contained no email'); + } - // This step calls the catalog to look up a user entity. You could for example - // replace it with a call to a different external system. - const { entity } = await ctx.findCatalogUser({ - annotations: { - 'acme.org/email': email, - }, - }); + // This step calls the catalog to look up a user entity. You could for example + // replace it with a call to a different external system. + const { entity } = await ctx.findCatalogUser({ + annotations: { + 'acme.org/email': email, + }, + }); - // In this step we extract the ownership references from the user entity using - // the standard logic. It uses a reference to the entity itself, as well as the - // target of each `memberOf` relation where the target is of the kind `Group`. - // - // If you replace the catalog lookup with something does not return - // an entity you will need to replace this step as well. - // - // You might also replace it if you for example want to filter out certain groups. - const ownershipRefs = getDefaultOwnershipRefs(entity); + // In this step we extract the ownership references from the user entity using + // the standard logic. It uses a reference to the entity itself, as well as the + // target of each `memberOf` relation where the target is of the kind `Group`. + // + // If you replace the catalog lookup with something does not return + // an entity you will need to replace this step as well. + // + // You might also replace it if you for example want to filter out certain groups. + const ownershipRefs = getDefaultOwnershipRefs(entity); - // The last step is to issue the token, where we might provide more options in the future. - return ctx.issueToken({ - claims: { - sub: stringifyEntityRef(entity), - ent: ownershipRefs, - }, - }); -}; + // The last step is to issue the token, where we might provide more options in the future. + return ctx.issueToken({ + claims: { + sub: stringifyEntityRef(entity), + ent: ownershipRefs, + }, + }); + }; + } + }) +) ``` ## Sign-In without Users in the Catalog @@ -221,38 +241,47 @@ check like in the example below, or you might for example look up the GitHub org that the user belongs to using the user access token in the provided result object. ```ts +// File: packages/backend/src/plugins/auth.ts import { DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; -// This example only shows the resolver function itself. -async ({ profile }, ctx) => { - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - // Split the email into the local part and the domain. - const [localPart, domain] = profile.email.split('@'); +export default async function createPlugin( + // ... + return await createRouter({ + // ... + providerFactories: { + // ... + google: async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + // Split the email into the local part and the domain. + const [localPart, domain] = profile.email.split('@'); - // Next we verify the email domain. It is recommended to include this - // kind of check if you don't look up the user in an external service. - if (domain !== 'acme.org') { - throw new Error('Login failed, user email domain check failed'); - } + // Next we verify the email domain. It is recommended to include this + // kind of check if you don't look up the user in an external service. + if (domain !== 'acme.org') { + throw new Error('Login failed, user email domain check failed'); + } - // By using `stringifyEntityRef` we ensure that the reference is formatted correctly - const userEntityRef = stringifyEntityRef({ - kind: 'User', - name: localPart, - namespace: DEFAULT_NAMESPACE, - }); + // By using `stringifyEntityRef` we ensure that the reference is formatted correctly + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: localPart, + namespace: DEFAULT_NAMESPACE, + }); - return ctx.issueToken({ - claims: { - sub: userEntityRef, - ent: [userEntityRef], - }, - }); -}, + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + }, + } + }) +) ``` ## AuthHandler From 9cee3738fc0a4fc6f6d1f61f7a19076dfedf2d6b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 13:12:04 +0200 Subject: [PATCH 2/6] docs: nits to identity resolver page Signed-off-by: Himanshu Mishra --- docs/getting-started/configuration.md | 73 ++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 1dc1ffc1b1..c16f4145ed 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -155,6 +155,8 @@ auth: clientSecret: YOUR CLIENT SECRET ``` +### Add sign-in option to the frontend + Backstage will re-read the configuration. If there's no errors, that's great! We can continue with the last part of the configuration. The next step is needed to change the sign-in page, this you actually need to add in the source code. @@ -187,10 +189,77 @@ components: { }, ``` +### Add sign-in resolver in the backend + +The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The identity is mainly used in determining the ownership of Backstage Catalog entities for the user. This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. + > Since [v1.1.0](https://github.com/backstage/backstage/releases/tag/v1.1.0-next.3), you must provide an [explicit sign-in resolver](../auth/identity-resolver.md). -That should be it. You can stop your Backstage App. When you start it again and -go to your Backstage portal in your browser, you should have your login prompt! +For now, let's create a simple Sign In Resolver which uses the first part of the user's email address to assign a Backstage User Identity. + +Replace your `packages/backend/src/plugin/auth.ts` file with the following + +```tsx +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, + github: providers.github.create({ + signIn: { + resolver: async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + // Split the email into the local part and the domain. + // You can use the email domain and verify that it belongs to your own org + // e.g. acme.org. It is recommended to include this kind of check for security. + const [localPart, _] = profile.email.split('@'); + + // By using `stringifyEntityRef` we ensure that the reference is formatted correctly + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: localPart, + namespace: DEFAULT_NAMESPACE, + }); + + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + }, + }, + }), + }, + }); +} +``` + +Restart Backstage from the terminal, by stopping it with `Control-C`, and starting it with `yarn dev` . You should be welcomed by a login prompt! + +> Note: Sometimes the frontend starts before the backend resulting in errors on the sign in page. Wait for the backend to start and then reload Backstage to proceed. To learn more about Authentication in Backstage, here are some docs you could read: From 04fa958718a74359fab77ddd1b13a19aee98a606 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 17:46:56 +0200 Subject: [PATCH 3/6] correct explanation of user identity and include domain check in email addresses Signed-off-by: Himanshu Mishra --- docs/auth/identity-resolver.md | 2 +- docs/getting-started/configuration.md | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 3acdf7ea20..3ae2f46360 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -201,7 +201,7 @@ export default async function createPlugin( // the standard logic. It uses a reference to the entity itself, as well as the // target of each `memberOf` relation where the target is of the kind `Group`. // - // If you replace the catalog lookup with something does not return + // If you replace the catalog lookup with something that does not return // an entity you will need to replace this step as well. // // You might also replace it if you for example want to filter out certain groups. diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index c16f4145ed..7c7fa4d98f 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -191,13 +191,15 @@ components: { ### Add sign-in resolver in the backend -The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The identity is mainly used in determining the ownership of Backstage Catalog entities for the user. This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. +The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The Backstage User Identity is fundamentally a JSON Web Token which consists of two parts. The `sub` part is your identity and that identifies your user for any purpose within the Backstage ecosystem. It can for example be used as a primary key in a per-user settings backend, or any other similar purpose. The ent is what relates to ownership (the list of entity refs through which you claim ownership). This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. > Since [v1.1.0](https://github.com/backstage/backstage/releases/tag/v1.1.0-next.3), you must provide an [explicit sign-in resolver](../auth/identity-resolver.md). For now, let's create a simple Sign In Resolver which uses the first part of the user's email address to assign a Backstage User Identity. -Replace your `packages/backend/src/plugin/auth.ts` file with the following +Replace your `packages/backend/src/plugin/auth.ts` file with the following. + +> NOTE: Please update `acme.org` below with your actual company domain to allow login requests from email address with that domain. ```tsx import { @@ -232,9 +234,13 @@ export default async function createPlugin( ); } // Split the email into the local part and the domain. - // You can use the email domain and verify that it belongs to your own org - // e.g. acme.org. It is recommended to include this kind of check for security. - const [localPart, _] = profile.email.split('@'); + const [localPart, domain] = profile.email.split('@'); + + // Next we verify the email domain. It is recommended to include this + // kind of check if you don't look up the user in an external service. + if (domain !== 'acme.org') { + throw new Error('Login failed, user email domain check failed'); + } // By using `stringifyEntityRef` we ensure that the reference is formatted correctly const userEntityRef = stringifyEntityRef({ From 88c697dc89309e3cca1853df228e308d9e7b94e7 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 8 Jun 2022 17:51:09 +0200 Subject: [PATCH 4/6] vale: add ent in keyword Signed-off-by: Himanshu Mishra --- .github/vale/Vocab/Backstage/accept.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 7631640199..b87f5d3881 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -92,6 +92,7 @@ elasticsearch esbuild eslint etag +ent Expedia facto failover From 13767bd7b69d745cad8ec9bc54c8e0ac78ee2af1 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 9 Jun 2022 13:49:43 +0200 Subject: [PATCH 5/6] remove custom sign in resolver Signed-off-by: Himanshu Mishra --- docs/getting-started/configuration.md | 74 +-------------------------- 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 7c7fa4d98f..286c6cff1e 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -189,79 +189,7 @@ components: { }, ``` -### Add sign-in resolver in the backend - -The Auth backend plugin is responsible for assigning a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) when a user signs in. The Backstage User Identity is fundamentally a JSON Web Token which consists of two parts. The `sub` part is your identity and that identifies your user for any purpose within the Backstage ecosystem. It can for example be used as a primary key in a per-user settings backend, or any other similar purpose. The ent is what relates to ownership (the list of entity refs through which you claim ownership). This is achieved by writing a [Sign In Resolver](../auth/identity-resolver.md#sign-in-resolvers) which takes care of assigning the right identity to the user and even reject SignIn attempts from unrecognized email domains. - -> Since [v1.1.0](https://github.com/backstage/backstage/releases/tag/v1.1.0-next.3), you must provide an [explicit sign-in resolver](../auth/identity-resolver.md). - -For now, let's create a simple Sign In Resolver which uses the first part of the user's email address to assign a Backstage User Identity. - -Replace your `packages/backend/src/plugin/auth.ts` file with the following. - -> NOTE: Please update `acme.org` below with your actual company domain to allow login requests from email address with that domain. - -```tsx -import { - createRouter, - providers, - defaultAuthProviderFactories, -} from '@backstage/plugin-auth-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - tokenManager: env.tokenManager, - providerFactories: { - ...defaultAuthProviderFactories, - github: providers.github.create({ - signIn: { - resolver: async ({ profile }, ctx) => { - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - // Split the email into the local part and the domain. - const [localPart, domain] = profile.email.split('@'); - - // Next we verify the email domain. It is recommended to include this - // kind of check if you don't look up the user in an external service. - if (domain !== 'acme.org') { - throw new Error('Login failed, user email domain check failed'); - } - - // By using `stringifyEntityRef` we ensure that the reference is formatted correctly - const userEntityRef = stringifyEntityRef({ - kind: 'User', - name: localPart, - namespace: DEFAULT_NAMESPACE, - }); - - return ctx.issueToken({ - claims: { - sub: userEntityRef, - ent: [userEntityRef], - }, - }); - }, - }, - }), - }, - }); -} -``` +> Note: The default Backstage app comes with a guest Sign In Resolver. This resolver makes all users share a single "guest" identity and is only intended as a minimum requirement to quickly get up and running. You can read more about how [Sign In Resolvers](../auth/identity-resolver.md#sign-in-resolvers) play a role in creating a [Backstage User Identity](../auth/identity-resolver.md#backstage-user-identity) for logged in users. Restart Backstage from the terminal, by stopping it with `Control-C`, and starting it with `yarn dev` . You should be welcomed by a login prompt! From c7e311789daebf42dc1870f1bf1869a2e4df1bb7 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 21 Jun 2022 16:36:59 +0200 Subject: [PATCH 6/6] Remove unused vale keyword ent Signed-off-by: Himanshu Mishra --- .github/vale/Vocab/Backstage/accept.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index b87f5d3881..7631640199 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -92,7 +92,6 @@ elasticsearch esbuild eslint etag -ent Expedia facto failover