Merge pull request #24154 from backstage/freben/sign-in
update the sign-in resolver docs for the new backend system
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
---
|
||||
id: identity-resolver--old
|
||||
title: Sign-in Identities and Resolvers (Old Backend System)
|
||||
description: An introduction to Backstage user identities and sign-in resolvers in the old backend system
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by
|
||||
[the new backend system](../backend-system/index.md), being the default since
|
||||
Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new
|
||||
backend system, you may want to read [its own article](./identity-resolver.md)
|
||||
instead. Also, check out [the migration docs](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
By default, every Backstage auth provider is configured only for the use-case of
|
||||
access delegation. This enables Backstage to request resources and actions from
|
||||
external systems on behalf of the user, for example re-triggering a build in CI.
|
||||
|
||||
If you want to use an auth provider to sign in users, you need to explicitly configure
|
||||
it have sign-in enabled and also tell it how the external identities should
|
||||
be mapped to user identities within Backstage.
|
||||
|
||||
## Quick Start
|
||||
|
||||
> See [providers](../reference/plugin-auth-backend.providers.md)
|
||||
> for a full list of auth providers and their built-in sign-in resolvers.
|
||||
|
||||
Backstage projects created with `npx @backstage/create-app` come configured with a
|
||||
sign-in resolver for GitHub guest access. 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 replace `github` for any of the other providers if you need.
|
||||
|
||||
This resolver should not be used in production, as it uses a single shared identity,
|
||||
and has no restrictions on who is able to sign-in. Be sure to read through the rest
|
||||
of this page to understand the Backstage identity system once you need to install
|
||||
a resolver for your production environment.
|
||||
|
||||
The guest resolver can be useful for testing purposes too, and it looks like this:
|
||||
|
||||
```ts
|
||||
signIn: {
|
||||
resolver(_, ctx) {
|
||||
const userRef = 'user:default/guest'
|
||||
return ctx.issueToken({
|
||||
claims: {
|
||||
sub: userRef,
|
||||
ent: [userRef],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## Backstage User Identity
|
||||
|
||||
A user identity within Backstage is built up from two pieces of information, a
|
||||
user [entity reference](../features/software-catalog/references.md), and a
|
||||
set of ownership entity references.
|
||||
When a user signs in, a Backstage token is generated with these two pieces of information,
|
||||
which is then used to identify the user within the Backstage ecosystem.
|
||||
|
||||
The user entity reference should uniquely identify the logged in user in Backstage.
|
||||
It is encouraged that a matching user entity also exists within the Software Catalog,
|
||||
but it is not required. If the user entity exists in the catalog it can be used to
|
||||
store additional data about the user. There may even be some plugins that require
|
||||
this for them to be able to function.
|
||||
|
||||
The ownership references are also entity references, and it is likewise
|
||||
encouraged that these entities exist within the catalog, but it is not a requirement.
|
||||
The ownership references are used to determine what the user owns, as a set
|
||||
of references that the user claims ownership though. For example, a user
|
||||
Jane (`user:default/jane`) might have the ownership references `user:default/jane`,
|
||||
`group:default/team-a`, and `group:default/admins`. Given these ownership claims,
|
||||
any entity that is marked as owned by either of `user:jane`, `team-a`, or `admins` would
|
||||
be considered owned by Jane.
|
||||
|
||||
The ownership claims often contain the user entity reference itself, but it is not
|
||||
required. It is also worth noting that the ownership claims can also be used to
|
||||
resolve other relations similar to ownership, such as a claim for a `maintainer` or
|
||||
`operator` status.
|
||||
|
||||
The Backstage token that encapsulates the user identity is a JWT. The user entity
|
||||
reference is stored in the `sub` claim of the payload, while the ownership references
|
||||
are stored in a custom `ent` claim. Both the user and ownership references should
|
||||
always be full entity references, as opposed to shorthands like just `jane` or `user:jane`.
|
||||
|
||||
## Sign-in Resolvers
|
||||
|
||||
Signing in a user into Backstage requires a mapping of the user identity from the
|
||||
third-party auth provider to a Backstage user identity. This mapping can vary quite
|
||||
a lot between different organizations and auth providers, and because of that there's
|
||||
no default way to resolve user identities. The auth provider that one wants to use
|
||||
for sign-in must instead be configured with a sign-in resolver, which is a function
|
||||
that is responsible for creating this user identity mapping.
|
||||
|
||||
The input to the sign-in resolver function is the result of a successful log in with
|
||||
the given auth provider, as well as a context object that contains various helpers
|
||||
for looking up users and issuing tokens. There are also a number of built-in sign-in
|
||||
resolvers that can be used, which are covered a bit further down.
|
||||
|
||||
Note that while it possible to configure multiple auth providers to be used for sign-in,
|
||||
you should take care when doing so. It is best to make sure that the different auth
|
||||
providers either do not have any user overlap, or that any users that are able to log
|
||||
in with multiple providers always end up with the same Backstage identity.
|
||||
|
||||
### Custom Resolver Example
|
||||
|
||||
Let's look at an example of a custom sign-in resolver for the Google auth provider.
|
||||
This all typically happens within your `packages/backend/src/plugins/auth.ts` file,
|
||||
which is responsible for setting up and configuring the auth backend plugin.
|
||||
|
||||
You provide the resolver as part of the options you pass when creating a new auth
|
||||
provider factory. This means you need to replace the default Google provider with
|
||||
one that you create. Be sure to also include the existing `defaultAuthProviderFactories`
|
||||
if you want to keep all of the built-in auth providers installed.
|
||||
|
||||
Now let's look at the example, with the rest of the commentary being made with in
|
||||
the code comments:
|
||||
|
||||
```ts
|
||||
// File: packages/backend/src/plugins/auth.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<Router> {
|
||||
return await createRouter({
|
||||
...env,
|
||||
providerFactories: {
|
||||
...defaultAuthProviderFactories,
|
||||
google: providers.google.create({
|
||||
signIn: {
|
||||
resolver: async (info, ctx) => {
|
||||
const {
|
||||
profile: { email },
|
||||
} = info;
|
||||
// Profiles are not always guaranteed to to have an email address.
|
||||
// You can also find more provider-specific information in `info.result`.
|
||||
// It typically contains a `fullProfile` object as well as ID and/or access
|
||||
// tokens that you can use for additional lookups.
|
||||
if (!email) {
|
||||
throw new Error('User profile contained no email');
|
||||
}
|
||||
|
||||
// You can add your own custom validation logic here.
|
||||
// Logins can be prevented by throwing an error like the one above.
|
||||
myEmailValidator(email);
|
||||
|
||||
// This example resolver simply uses the local part of the email as the name.
|
||||
const [name] = email.split('@');
|
||||
|
||||
// This helper function handles sign-in by looking up a user in the catalog.
|
||||
// The lookup can be done either by reference, annotations, or custom filters.
|
||||
//
|
||||
// The helper also issues a token for the user, using the standard group
|
||||
// membership logic to determine the ownership references of the user.
|
||||
return ctx.signInWithCatalogUser({
|
||||
entityRef: { name },
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in Resolvers
|
||||
|
||||
You don't always have to write your own custom resolver. The auth backend plugin provides
|
||||
built-in resolvers for many of the common sign-in patterns. You access these via the `resolvers`
|
||||
property of each of the auth provider integrations. For example, the Google provider has
|
||||
a built in resolver that works just like the one we defined above:
|
||||
|
||||
```ts
|
||||
// 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
|
||||
by matching the `google.com/email` annotation of user entities in the catalog:
|
||||
|
||||
```ts
|
||||
providers.google.create({
|
||||
signIn: {
|
||||
resolver: providers.google.resolvers.emailMatchingUserEntityAnnotation(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Ownership Resolution
|
||||
|
||||
If you want to have more control over the membership resolution and token generation
|
||||
that happens during sign-in you can replace `ctx.signInWithCatalogUser` with a set
|
||||
of lower-level calls:
|
||||
|
||||
```ts
|
||||
// File: packages/backend/src/plugins/auth.ts
|
||||
import { getDefaultOwnershipEntityRefs } from '@backstage/plugin-auth-backend';
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
// 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 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.
|
||||
//
|
||||
// Note that `getDefaultOwnershipEntityRefs` only includes groups to which the
|
||||
// user has a direct MEMBER_OF relationship. It's perfectly fine to include
|
||||
// groups that the user is transitively part of in the claims array, but the
|
||||
// catalog doesn't currently provide a direct way of accessing this list of
|
||||
// groups.
|
||||
const ownershipRefs = getDefaultOwnershipEntityRefs(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,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
## Sign-In without Users in the Catalog
|
||||
|
||||
While populating the catalog with organizational data unlocks more powerful ways
|
||||
to browse your software ecosystem, it might not always be a viable or prioritized
|
||||
option. However, even if you do not have user entities populated in your catalog, you
|
||||
can still sign in users. As there are currently no built-in sign-in resolvers for
|
||||
this scenario you will need to implement your own.
|
||||
|
||||
Signing in a user that doesn't exist in the catalog is as simple as skipping the
|
||||
catalog lookup step from the above example. Rather than looking up the user, we
|
||||
instead immediately issue a token using whatever information is available. One caveat
|
||||
is that it can be tricky to determine the ownership references, although it can
|
||||
be achieved for example through a lookup to an external service. You typically
|
||||
want to at least use the user itself as a lone ownership reference.
|
||||
|
||||
Because we no longer use the catalog as an allow-list of users, it is often important
|
||||
that you limit what users are allowed to sign in. This could be a simple email domain
|
||||
check like in the example below, or you might for example look up the GitHub organizations
|
||||
that the user belongs to using the user access token in the provided result object.
|
||||
|
||||
```ts
|
||||
// File: packages/backend/src/plugins/auth.ts
|
||||
import { createRouter, providers } from '@backstage/plugin-auth-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import {
|
||||
stringifyEntityRef,
|
||||
DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
...env,
|
||||
providerFactories: {
|
||||
google: providers.google.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, this email ${profile.email} does not belong to the expected domain`,
|
||||
);
|
||||
}
|
||||
|
||||
// By using `stringifyEntityRef` we ensure that the reference is formatted correctly
|
||||
const userEntity = stringifyEntityRef({
|
||||
kind: 'User',
|
||||
name: localPart,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
});
|
||||
return ctx.issueToken({
|
||||
claims: {
|
||||
sub: userEntity,
|
||||
ent: [userEntity],
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## AuthHandler
|
||||
|
||||
Similar to a custom sign-in resolver, you can also write a custom auth handler
|
||||
function which is used to verify and convert the auth response into the profile
|
||||
that will be presented to the user. This is where you can customize things like
|
||||
display name and profile picture.
|
||||
|
||||
This is also the place where you can do authorization and validation of the user
|
||||
and throw errors if the user should not be allowed access in Backstage.
|
||||
|
||||
```ts
|
||||
// File: packages/backend/src/plugins/auth.ts
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
...
|
||||
providerFactories: {
|
||||
google: providers.google.create({
|
||||
authHandler: async ({
|
||||
fullProfile // Type: passport.Profile,
|
||||
idToken // Type: (Optional) string,
|
||||
}) => {
|
||||
// Custom validation code goes here
|
||||
return {
|
||||
profile: {
|
||||
email,
|
||||
picture,
|
||||
displayName,
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
+287
-250
@@ -4,52 +4,44 @@ title: Sign-in Identities and Resolvers
|
||||
description: An introduction to Backstage user identities and sign-in resolvers
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend
|
||||
system](../backend-system/index.md) which is the default since Backstage
|
||||
[version 1.24](../releases/v1.24.0.md). If you are still on the old backend
|
||||
system, you may want to read [its own article](./identity-resolver--old.md)
|
||||
instead.
|
||||
:::
|
||||
|
||||
By default, every Backstage auth provider is configured only for the use-case of
|
||||
access delegation. This enables Backstage to request resources and actions from
|
||||
external systems on behalf of the user, for example re-triggering a build in CI.
|
||||
|
||||
If you want to use an auth provider to sign in users, you need to explicitly configure
|
||||
it have sign-in enabled and also tell it how the external identities should
|
||||
be mapped to user identities within Backstage.
|
||||
If you want to use an auth provider to sign in users, you need to explicitly
|
||||
configure it have sign-in enabled and also tell it how the external identities
|
||||
should be mapped to user identities within Backstage. You do this by either
|
||||
choosing a built-in sign in resolver, or supplying your own. Both methods are
|
||||
listed below.
|
||||
|
||||
## Quick Start
|
||||
|
||||
> See [providers](../reference/plugin-auth-backend.providers.md)
|
||||
> for a full list of auth providers and their built-in sign-in resolvers.
|
||||
Backstage projects created with `npx @backstage/create-app` come configured with
|
||||
a [guest auth provider](https://backstage.io/docs/auth/guest/provider). This
|
||||
provider makes all users share a single "guest" identity. This is useful for
|
||||
testing purposes and quickly getting started locally, but is not safe for use in
|
||||
production and that particular provider will refuse to work there.
|
||||
|
||||
Backstage projects created with `npx @backstage/create-app` come configured with a
|
||||
sign-in resolver for GitHub guest access. 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 replace `github` for any of the other providers if you need.
|
||||
|
||||
This resolver should not be used in production, as it uses a single shared identity,
|
||||
and has no restrictions on who is able to sign-in. Be sure to read through the rest
|
||||
of this page to understand the Backstage identity system once you need to install
|
||||
a resolver for your production environment.
|
||||
|
||||
The guest resolver can be useful for testing purposes too, and it looks like this:
|
||||
|
||||
```ts
|
||||
signIn: {
|
||||
resolver(_, ctx) {
|
||||
const userRef = 'user:default/guest'
|
||||
return ctx.issueToken({
|
||||
claims: {
|
||||
sub: userRef,
|
||||
ent: [userRef],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
```
|
||||
Because of this, one of the early things you want to do when standing up your
|
||||
Backstage instance is to choose a production ready auth provider. See [the auth
|
||||
overview page](./index.md) for a full list of providers and how to install and
|
||||
configure them.
|
||||
|
||||
## Backstage User Identity
|
||||
|
||||
A user identity within Backstage is built up from two pieces of information, a
|
||||
user [entity reference](../features/software-catalog/references.md), and a
|
||||
set of ownership entity references.
|
||||
When a user signs in, a Backstage token is generated with these two pieces of information,
|
||||
which is then used to identify the user within the Backstage ecosystem.
|
||||
A user identity within Backstage is built up from two main pieces of
|
||||
information: a user [entity
|
||||
reference](../features/software-catalog/references.md), and a set of ownership
|
||||
references. When a user signs in, a Backstage token is generated which is then
|
||||
used to identify the user within the Backstage ecosystem.
|
||||
|
||||
The user entity reference should uniquely identify the logged in user in Backstage.
|
||||
It is encouraged that a matching user entity also exists within the Software Catalog,
|
||||
@@ -71,10 +63,13 @@ required. It is also worth noting that the ownership claims can also be used to
|
||||
resolve other relations similar to ownership, such as a claim for a `maintainer` or
|
||||
`operator` status.
|
||||
|
||||
The Backstage token that encapsulates the user identity is a JWT. The user entity
|
||||
reference is stored in the `sub` claim of the payload, while the ownership references
|
||||
are stored in a custom `ent` claim. Both the user and ownership references should
|
||||
always be full entity references, as opposed to shorthands like just `jane` or `user:jane`.
|
||||
The Backstage token that encapsulates the user identity is a JWT. The user
|
||||
entity reference is stored in the `sub` claim of the payload, while the
|
||||
ownership references are stored in a custom `ent` claim in the old backend
|
||||
system but instead is made available through a user info API endpoint on the
|
||||
auth backend in the new system. Both the user and ownership references should
|
||||
always be full entity references, as opposed to shorthands like just `jane` or
|
||||
`user:jane`.
|
||||
|
||||
## Sign-in Resolvers
|
||||
|
||||
@@ -90,116 +85,170 @@ the given auth provider, as well as a context object that contains various helpe
|
||||
for looking up users and issuing tokens. There are also a number of built-in sign-in
|
||||
resolvers that can be used, which are covered a bit further down.
|
||||
|
||||
Note that while it possible to configure multiple auth providers to be used for sign-in,
|
||||
you should take care when doing so. It is best to make sure that the different auth
|
||||
providers either do not have any user overlap, or that any users that are able to log
|
||||
in with multiple providers always end up with the same Backstage identity.
|
||||
Note that while it possible to configure multiple auth providers to be used for
|
||||
sign-in, you should take care when doing so. It is best to make sure that the
|
||||
different auth providers either do not have any user overlap, or that any users
|
||||
that are able to log in with multiple providers always end up with the same
|
||||
Backstage identity. For most organizations, it makes the most sense to provide
|
||||
only one sign-in method.
|
||||
|
||||
### Custom Resolver Example
|
||||
### Using Builtin Resolvers
|
||||
|
||||
Let's look at an example of a custom sign-in resolver for the Google auth provider.
|
||||
This all typically happens within your `packages/backend/src/plugins/auth.ts` file,
|
||||
which is responsible for setting up and configuring the auth backend plugin.
|
||||
Most auth providers come with a set of builtin sign in providers that you can
|
||||
choose from. They target the most common use cases, and if they fit your needs,
|
||||
you can pick one or more of them without having to write any code at all. You
|
||||
still have to make a choice - as mentioned above, even if there are a set of
|
||||
builtins, none of them are selected by default.
|
||||
|
||||
You provide the resolver as part of the options you pass when creating a new auth
|
||||
provider factory. This means you need to replace the default Google provider with
|
||||
one that you create. Be sure to also include the existing `defaultAuthProviderFactories`
|
||||
if you want to keep all of the built-in auth providers installed.
|
||||
You set up builtin sign in resolvers using [your app-config](../conf/index.md),
|
||||
next to the respective provider's configuration. Here's an example for GitHub:
|
||||
|
||||
Now let's look at the example, with the rest of the commentary being made with in
|
||||
the code comments:
|
||||
```yaml title="in e.g. app-config.yaml"
|
||||
auth:
|
||||
environment: development
|
||||
providers:
|
||||
github:
|
||||
development:
|
||||
clientId: ${AUTH_GITHUB_CLIENT_ID}
|
||||
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
|
||||
enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}
|
||||
signIn:
|
||||
resolvers:
|
||||
- resolver: usernameMatchingUserEntityName
|
||||
- resolver: emailMatchingUserEntityProfileEmail
|
||||
- resolver: emailLocalPartMatchingUserEntityName
|
||||
```
|
||||
|
||||
Note that in this instance it lists several resolvers, which means that the
|
||||
framework will try them one by one until one succeeds. If none of them do, the
|
||||
sign in attempt is rejected.
|
||||
|
||||
The list of available resolvers is different for each provider, since they often
|
||||
depend on the information model returned from the upstream provider service.
|
||||
Consult the documentation of the respective provider to find the list.
|
||||
|
||||
### Building Custom Resolvers
|
||||
|
||||
If the builtins don't work for you, you can also provide a completely custom
|
||||
sign-in resolver, through code. If you follow the installation instructions of
|
||||
[one of the available providers](./index.md), you will likely have added a
|
||||
dependency to your backend along with a line of code and some configuration.
|
||||
|
||||
Using GitHub as an example, this is the relevant parts of the backend code:
|
||||
|
||||
```ts title="in packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
|
||||
```
|
||||
|
||||
When you want to supply a custom sign-in resolver, as a general pattern you
|
||||
remove that last import and instead construct your own provider using the
|
||||
facilities from the same package. You can leave the config unchanged from
|
||||
before.
|
||||
|
||||
```ts title="in packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { githubAuthenticator } from '@backstage/plugin-auth-backend-module-github-provider';
|
||||
import {
|
||||
authProvidersExtensionPoint,
|
||||
createOAuthProviderFactory,
|
||||
} 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 },
|
||||
async init({ providers }) {
|
||||
providers.registerProvider({
|
||||
// This ID must match the actual provider config, e.g. addressing
|
||||
// auth.providers.github means that this must be "github".
|
||||
providerId: 'github',
|
||||
// Use createProxyAuthProviderFactory instead if it's one of the proxy
|
||||
// based providers rather than an OAuth based one
|
||||
factory: createOAuthProviderFactory({
|
||||
authenticator: githubAuthenticator,
|
||||
async signInResolver(info, ctx) {
|
||||
/*********************************************************************
|
||||
* Custom resolver code goes here, see farther down in this article! *
|
||||
* "info" is the sign in result from the upstream (github here), and *
|
||||
* "ctx" contains useful utilities for token issuance etc. *
|
||||
*********************************************************************/
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
/* highlight-remove-next-line */
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(customAuth);
|
||||
```
|
||||
|
||||
Check out [the naming patterns
|
||||
article](../backend-system/architecture/07-naming-patterns.md) for what rules
|
||||
apply regarding how to form valid IDs. In this example we also put the module
|
||||
declaration directly in `packages/backend/src/index.ts` but that's just for
|
||||
simplicity. You can place it anywhere you like, including in other packages, and
|
||||
import from there if you prefer.
|
||||
|
||||
The `createOAuthProviderFactory` / `createProxyAuthProviderFactory` functions
|
||||
have additional options for profile and state transforms - not covered here, but
|
||||
good to know about if you need them.
|
||||
|
||||
So what would the body of a typical sign in resolver callback look like? Here's
|
||||
an example:
|
||||
|
||||
```ts
|
||||
// File: packages/backend/src/plugins/auth.ts
|
||||
import {
|
||||
createRouter,
|
||||
providers,
|
||||
defaultAuthProviderFactories,
|
||||
} from '@backstage/plugin-auth-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
// ...
|
||||
async signInResolver(info, ctx) {
|
||||
const { profile: { email } } = info;
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
...env,
|
||||
providerFactories: {
|
||||
...defaultAuthProviderFactories,
|
||||
google: providers.google.create({
|
||||
signIn: {
|
||||
resolver: async (info, ctx) => {
|
||||
const {
|
||||
profile: { email },
|
||||
} = info;
|
||||
// Profiles are not always guaranteed to to have an email address.
|
||||
// You can also find more provider-specific information in `info.result`.
|
||||
// It typically contains a `fullProfile` object as well as ID and/or access
|
||||
// tokens that you can use for additional lookups.
|
||||
if (!email) {
|
||||
throw new Error('User profile contained no email');
|
||||
}
|
||||
// Profiles are not always guaranteed to to have an email address.
|
||||
// You can also find more provider-specific information in `info.result`.
|
||||
// It typically contains a `fullProfile` object as well as ID and/or access
|
||||
// tokens that you can use for additional lookups.
|
||||
if (!email) {
|
||||
throw new Error('User profile contained no email');
|
||||
}
|
||||
|
||||
// You can add your own custom validation logic here.
|
||||
// Logins can be prevented by throwing an error like the one above.
|
||||
myEmailValidator(email);
|
||||
// You can add your own custom validation logic here.
|
||||
// Logins can be prevented by throwing an error like the one above.
|
||||
myEmailValidator(email);
|
||||
|
||||
// This example resolver simply uses the local part of the email as the name.
|
||||
const [name] = email.split('@');
|
||||
// This example resolver simply uses the local part of the email as the name.
|
||||
const [name] = email.split('@');
|
||||
|
||||
// This helper function handles sign-in by looking up a user in the catalog.
|
||||
// The lookup can be done either by reference, annotations, or custom filters.
|
||||
//
|
||||
// The helper also issues a token for the user, using the standard group
|
||||
// membership logic to determine the ownership references of the user.
|
||||
return ctx.signInWithCatalogUser({
|
||||
entityRef: { name },
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
// This helper function handles sign-in by looking up a user in the catalog.
|
||||
// The lookup can be done either by reference, annotations, or custom filters.
|
||||
//
|
||||
// The helper also issues a token for the user, using the standard group
|
||||
// membership logic to determine the ownership references of the user.
|
||||
//
|
||||
// There are a number of other methods on the ctx, feel free to explore them!
|
||||
return ctx.signInWithCatalogUser({
|
||||
entityRef: { name },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in Resolvers
|
||||
If you throw an error in the sign in resolver function, the sign in attempt is
|
||||
immediately rejected, and the error details are presented in the user interface.
|
||||
|
||||
You don't always have to write your own custom resolver. The auth backend plugin provides
|
||||
built-in resolvers for many of the common sign-in patterns. You access these via the `resolvers`
|
||||
property of each of the auth provider integrations. For example, the Google provider has
|
||||
a built in resolver that works just like the one we defined above:
|
||||
The `ctx` context [has several useful
|
||||
functions](https://backstage.io/docs/reference/plugin-auth-node.authresolvercontext/)
|
||||
for issuing tokens in various ways.
|
||||
|
||||
```ts
|
||||
// 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
|
||||
by matching the `google.com/email` annotation of user entities in the catalog:
|
||||
|
||||
```ts
|
||||
providers.google.create({
|
||||
signIn: {
|
||||
resolver: providers.google.resolvers.emailMatchingUserEntityAnnotation(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Ownership Resolution
|
||||
### Custom Ownership Resolution
|
||||
|
||||
If you want to have more control over the membership resolution and token generation
|
||||
that happens during sign-in you can replace `ctx.signInWithCatalogUser` with a set
|
||||
@@ -209,55 +258,48 @@ of lower-level calls:
|
||||
// File: packages/backend/src/plugins/auth.ts
|
||||
import { getDefaultOwnershipEntityRefs } from '@backstage/plugin-auth-backend';
|
||||
|
||||
export default async function createPlugin(
|
||||
// ...
|
||||
return await createRouter({
|
||||
// ...
|
||||
providerFactories: {
|
||||
// ...
|
||||
google: async ({ profile: { email } }, ctx) => {
|
||||
if (!email) {
|
||||
throw new Error('User profile contained no email');
|
||||
}
|
||||
// ...
|
||||
async signInResolver({ 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 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.
|
||||
//
|
||||
// Note that `getDefaultOwnershipEntityRefs` only includes groups to which the
|
||||
// user has a direct MEMBER_OF relationship. It's perfectly fine to include
|
||||
// groups that the user is transitively part of in the claims array, but the
|
||||
// catalog doesn't currently provide a direct way of accessing this list of
|
||||
// groups.
|
||||
const ownershipRefs = getDefaultOwnershipEntityRefs(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 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.
|
||||
//
|
||||
// Note that `getDefaultOwnershipEntityRefs` only includes groups to which the
|
||||
// user has a direct MEMBER_OF relationship. It's perfectly fine to include
|
||||
// groups that the user is transitively part of in the claims array, but the
|
||||
// catalog doesn't currently provide a direct way of accessing this list of
|
||||
// groups.
|
||||
const ownershipRefs = getDefaultOwnershipEntityRefs(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
|
||||
### Sign-In without Users in the Catalog
|
||||
|
||||
While populating the catalog with organizational data unlocks more powerful ways
|
||||
to browse your software ecosystem, it might not always be a viable or prioritized
|
||||
@@ -278,63 +320,44 @@ 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 { createRouter, providers } from '@backstage/plugin-auth-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import {
|
||||
stringifyEntityRef,
|
||||
DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
import { stringifyEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
...env,
|
||||
providerFactories: {
|
||||
google: providers.google.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('@');
|
||||
// ...
|
||||
async signInResolver({ 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, this email ${profile.email} does not belong to the expected domain`,
|
||||
);
|
||||
}
|
||||
// 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, '${profile.email}' does not belong to the expected domain`,
|
||||
);
|
||||
}
|
||||
|
||||
// By using `stringifyEntityRef` we ensure that the reference is formatted correctly
|
||||
const userEntity = stringifyEntityRef({
|
||||
kind: 'User',
|
||||
name: localPart,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
});
|
||||
return ctx.issueToken({
|
||||
claims: {
|
||||
sub: userEntity,
|
||||
ent: [userEntity],
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
// By using `stringifyEntityRef` we ensure that the reference is formatted correctly
|
||||
const userEntity = stringifyEntityRef({
|
||||
kind: 'User',
|
||||
name: localPart,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
});
|
||||
return ctx.issueToken({
|
||||
claims: {
|
||||
sub: userEntity,
|
||||
ent: [userEntity],
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## AuthHandler
|
||||
## Profile Transforms
|
||||
|
||||
Similar to a custom sign-in resolver, you can also write a custom auth handler
|
||||
Similar to a custom sign-in resolver, you can also write a custom profile transform
|
||||
function which is used to verify and convert the auth response into the profile
|
||||
that will be presented to the user. This is where you can customize things like
|
||||
display name and profile picture.
|
||||
@@ -343,29 +366,43 @@ This is also the place where you can do authorization and validation of the user
|
||||
and throw errors if the user should not be allowed access in Backstage.
|
||||
|
||||
```ts
|
||||
// File: packages/backend/src/plugins/auth.ts
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
...
|
||||
providerFactories: {
|
||||
google: providers.google.create({
|
||||
authHandler: async ({
|
||||
fullProfile // Type: passport.Profile,
|
||||
idToken // Type: (Optional) string,
|
||||
}) => {
|
||||
// Custom validation code goes here
|
||||
return {
|
||||
profile: {
|
||||
email,
|
||||
picture,
|
||||
displayName,
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
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 },
|
||||
async init({ providers }) {
|
||||
providers.registerProvider({
|
||||
// This ID must match the actual provider config, e.g. addressing
|
||||
// auth.providers.github means that this must be "github".
|
||||
providerId: 'github',
|
||||
// Use createProxyAuthProviderFactory instead if it's one of the proxy
|
||||
// based providers rather than an OAuth based one
|
||||
factory: createOAuthProviderFactory({
|
||||
authenticator: githubAuthenticator,
|
||||
async profileTransform(result, ctx) {
|
||||
/**********************************************************************
|
||||
* Custom transform code goes here! *
|
||||
* "info" is the sign in result from the upstream (github here), and *
|
||||
* "ctx" contains useful utilities. *
|
||||
**********************************************************************/
|
||||
return {
|
||||
profile: {
|
||||
email,
|
||||
picture,
|
||||
displayName,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Remember to `backend.add` the created module just like above.
|
||||
|
||||
Reference in New Issue
Block a user