From a2545cabbd1a68c839117be7f26e116ebd53be70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 14 Apr 2024 12:18:35 +0200 Subject: [PATCH] add docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/auth/cloudflare/access.md | 171 +++++++++++++++++++++++---------- yarn.lock | 6 +- 2 files changed, 121 insertions(+), 56 deletions(-) diff --git a/docs/auth/cloudflare/access.md b/docs/auth/cloudflare/access.md index 509c0ba052..9df816c39f 100644 --- a/docs/auth/cloudflare/access.md +++ b/docs/auth/cloudflare/access.md @@ -24,76 +24,62 @@ Let's start by adding the following `auth` configuration in your auth: providers: cfaccess: + # You can find the team name in the Cloudflare Zero Trust dashboard. teamName: + # This service tokens section is optional -- you only need it if you have + # some Cloudflare Service Tokens that you want to be able to log in to your + # Backstage instance. serviceTokens: - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' subject: 'bot-user@your-company.com' + # This picks what sign in resolver(s) you want to use. + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail ``` -You can find the team name in the Cloudflare Zero Trust dashboard. The Service -Tokens section is optional -- you only need it if you have some Cloudflare -Service Tokens that you want to be able to log in to your Backstage instance. +This config section must be in place for the provider to load at all. -This config section must be in place for the provider to load at all. Now let's -add the provider itself. +The `signIn` section picks what sign-in resolver(s) to use for sign-in attempts. +It is responsible for matching the upstream provider's sign-in result to a +corresponding Backstage identity, or to throw an error if the attempt should be +rejected for any reason. The `emailMatchingUserEntityProfileEmail` is a common +choice: it tries to match the email of the signed-in user to a `User` kind +entity in the catalog whose profile email matches that. + +If the builtin sign in resolvers do not match your needs, you can skip the +`signIn` section and instead [provide a custom resolver](#advanced-custom-sign-in-resolver). ## Backend Changes -Add a `providerFactories` entry to the router in -`packages/backend/plugin/auth.ts`. +We need to add the provider package as a dependency to our backend: -```ts title="packages/backend/plugin/auth.ts" -import { providers } from '@backstage/plugin-auth-backend'; +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-cloudflare-access-provider +``` -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - providerFactories: { - 'cfaccess': providers.cfAccess.create({ - // Replace the auth handler if you want to customize the returned user - // profile info (can be left out; the default implementation is shown - // below which only returns the email). You may want to amend this code - // with something that loads additional user profile data out. - async authHandler({ accessToken }) { - return { profile: { email: accessToken.email } }; - }, - signIn: { - // You need to supply an identity resolver, that takes the profile - // and the access token and produces the Backstage token with the - // relevant user info. - async resolver({ profile, result }, ctx) { - // Somehow compute the Backstage token claims. Just some sample code - // shown here, but you may want to query your LDAP server, or - // https://.cloudflareaccess.com/cdn-cgi/access/get-identity - // https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/#groups-within-a-jwt - const id = profile.email.split('@')[0]; - const sub = stringifyEntityRef({ kind: 'User', name: id }); - const ent = [sub, stringifyEntityRef({ kind: 'Group', name: 'team-name' }); - return ctx.issueToken({ claims: { sub, ent } }); - }, - }, - }), - }, - }); -} +And to tell the backend to load it: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), +); +/* highlight-add-end */ ``` Now the backend is ready to serve auth requests on the -`/api/auth/cfaccess/refresh` endpoint. All that's left is to update the -frontend sign-in mechanism to poll that endpoint through Cloudflare Access, on -the user's behalf. +`/api/auth/cfaccess/refresh` endpoint. All that's left is to update the frontend +sign-in mechanism to poll that endpoint through Cloudflare Access, on the user's +behalf. -## Adding the provider to the Backstage frontend +## Frontend Changes It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in `packages/app/src/App.tsx` like this: +installed in your app like this: -```tsx title="packages/app/src/App.tsx" +```tsx title="in packages/app/src/App.tsx" /* highlight-add-next-line */ import { ProxiedSignInPage } from '@backstage/core-components'; @@ -103,8 +89,87 @@ const app = createApp({ SignInPage: props => , }, /* highlight-add-end */ - // .. + // ... }); ``` -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development. +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for +pointers on how to set up the sign-in page to also work smoothly for local +development. + +## Advanced: Custom Sign-in Resolver + +If none of the built-in sign in resolvers fit your needs, you need to provide a +customized version of the module. Now you should _not_ +`backend.add(import(...))`, instead you will do the following. + +```ts title="in packages/backend/plugin/auth.ts" +/* highlight-add-start */ +import { createCloudflareAccessAuthenticator } from '@backstage/plugin-auth-backend-module-cloudflare-access-provider'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; + +const customAuth = createBackendModule({ + // This ID must be exactly "auth" because that's the plugin it targets + pluginId: 'auth', + // This ID must be unique, but can be anything + moduleId: 'custom-auth-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + cache: coreServices.cache, + }, + async init({ providers, cache }) { + providers.registerProvider({ + // This ID must match the actual provider config, e.g. addressing + // auth.providers.github means that this must be "github". + providerId: 'cfaccess', + // Use createProxyAuthProviderFactory instead if it's one of the proxy + // based providers rather than an OAuth based one + factory: createProxyAuthProviderFactory({ + authenticator: createCloudflareAccessAuthenticator({ cache }), + async signInResolver(info, ctx) { + // This is where the body of the sign-in resolver goes! + const { profile } = info; + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + return ctx.signInWithCatalogUser({ + filter: { + 'spec.profile.email': profile.email, + }, + }); + }, + }), + }); + }, + }); + }, +}); +/* highlight-add-end */ + +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-remove-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), +); +/* highlight-remove-end */ +/* highlight-add-next-line */ +backend.add(customAuth); +``` + +The body of the sign-in resolver is up to you to write! The example code above +is just a copy of what `emailMatchingUserEntityProfileEmail` does. The `info` +parameter contains all of the results of the sign-in attempt so far. The `ctx` +context [has several useful +functions](https://backstage.io/docs/reference/plugin-auth-node.authresolvercontext/) +for issuing tokens in various ways. diff --git a/yarn.lock b/yarn.lock index ec5e15e41c..fb42396c80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19276,11 +19276,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20.1.1, @types/node@npm:^20.10.6, @types/node@npm:^20.11.16": - version: 20.12.4 - resolution: "@types/node@npm:20.12.4" + version: 20.12.7 + resolution: "@types/node@npm:20.12.7" dependencies: undici-types: ~5.26.4 - checksum: c29879642bd4f1f35ffc6e2356121c5ffdb6530d41db1e6ac013c6fbd1dfa4b213b9529a1213cf1f320f801423d00301176ffb41689f790b0cd8c1e82fe3ee74 + checksum: 7cc979f7e2ca9a339ec71318c3901b9978555257929ef3666987f3e447123bc6dc92afcc89f6347e09e07d602fde7d51bcddea626c23aa2bb74aeaacfd1e1686 languageName: node linkType: hard