+118
-53
@@ -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: <Team Name>
|
||||
# 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<Router> {
|
||||
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://<teamName>.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 => <ProxiedSignInPage {...props} provider="cfaccess" />,
|
||||
},
|
||||
/* 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user