Merge pull request #22565 from sennyeya/guest-tokens

feat(auth): Add support for guest tokens with a new provider
This commit is contained in:
Patrik Oldsberg
2024-02-27 12:25:03 +01:00
committed by GitHub
23 changed files with 541 additions and 31 deletions
+23
View File
@@ -0,0 +1,23 @@
---
'@backstage/plugin-auth-backend-module-guest-provider': minor
---
Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.providers.guest.userEntityRef` config key) like so,
```yaml title=app-config.yaml
auth:
providers:
guest:
userEntityRef: user:default/guest
```
This also adds a new property to control the ownership entity refs,
```yaml title=app-config.yaml
auth:
providers:
guest:
ownershipEntityRefs:
- guests
- development/custom
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
`SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback.
+4 -1
View File
@@ -242,7 +242,7 @@ catalog:
- Domain
- Location
providers:
openapi:
backstageOpenapi:
plugins:
- catalog
- search
@@ -399,6 +399,9 @@ auth:
scopes: ${AUTH_ATLASSIAN_SCOPES}
myproxy:
development: {}
guest:
development: {}
costInsights:
engineerCost: 200000
engineerThreshold: 0.5
+66
View File
@@ -0,0 +1,66 @@
---
id: provider
title: Guest Authentication Provider
sidebar_label: Guest
description: Adding a guest authentication provider in Backstage
---
Audience: Admins or developers
## Summary
The goal of this guide is to get you set up with a guest authentication provider that emits tokens. This is different than the old guest authentication that is purely stored on the frontend and does not have tokens. The main reason you'd want to use this provider is to use permissioned plugins.
:::caution
This provider should only ever be enabled for `development`. To prevent unauthorized access to your data, this package is _explicitly_ disabled for non-development environments.
:::
## Installation
### Backend
:::note
This will only work with the new backend system. There is no support for this in the old backend.
:::
Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation.
```
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider
```
Then, add it to your backend's `index.ts` file,
```diff
const backend = createBackend();
backend.add('@backstage/plugin-auth-backend');
+backend.add('@backstage/plugin-auth-backend-module-guest-provider');
await backend.start();
```
### Frontend
Add the following to your `SignInPage` providers,
```diff
const providers = [
+ 'guest',
...
]
```
### Config
Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`,
```diff
auth:
providers:
+ guest:
+ userEntityRef: user:default/guest
+ development: {}
```
We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object.
+1
View File
@@ -307,6 +307,7 @@
"auth/gitlab/provider",
"auth/google/provider",
"auth/google/gcp-iap-auth",
"auth/guest/provider",
"auth/okta/provider",
"auth/oauth2-proxy/provider",
"auth/onelogin/provider",
+1
View File
@@ -161,6 +161,7 @@ nav:
- GitLab: 'auth/gitlab/provider.md'
- Google: 'auth/google/provider.md'
- Google IAP: 'auth/google/gcp-iap-auth.md'
- Guest: 'auth/guest/provider.md'
- OAuth2Proxy: 'auth/oauth2-proxy/provider.md'
- Okta: 'auth/okta/provider.md'
- OneLogin: 'auth/onelogin/provider.md'
+1
View File
@@ -33,6 +33,7 @@
"@backstage/plugin-app-backend": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-azure-devops-backend": "workspace:^",
"@backstage/plugin-badges-backend": "workspace:^",
+1
View File
@@ -20,6 +20,7 @@ const backend = createBackend();
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('./authModuleGithubProvider'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.add(import('@backstage/plugin-adr-backend'));
backend.add(import('@backstage/plugin-app-backend/alpha'));
@@ -57,3 +57,17 @@ spec:
displayName: Guest User
email: guest@example.com
memberOf: [team-a]
---
# This user is added as an example, to make it more easy for the "Guest"
# sign-in option to demonstrate some entities being owned. In a regular org,
# a guest user would probably not be registered like this.
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: guest
namespace: development
spec:
profile:
displayName: Guest User
email: guest@example.com
memberOf: [group:default/team-a]
@@ -20,6 +20,9 @@ import {
BackstageUserIdentity,
} from '@backstage/core-plugin-api';
/**
* @deprecated Use `@backstage/plugin-auth-backend-module-guest-provider` instead.
*/
export class GuestUserIdentity implements IdentityApi {
getUserId(): string {
return 'guest';
@@ -20,39 +20,108 @@ import Button from '@material-ui/core/Button';
import { InfoCard } from '../InfoCard/InfoCard';
import { GridItem } from './styles';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { ProxiedSignInIdentity } from '../ProxiedSignInPage/ProxiedSignInIdentity';
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
import { GuestUserIdentity } from './GuestUserIdentity';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { ResponseError } from '@backstage/errors';
const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => (
<GridItem>
<InfoCard
title="Guest"
variant="fullHeight"
actions={
<Button
color="primary"
variant="outlined"
onClick={() => {
onSignInStarted();
onSignInSuccess(new GuestUserIdentity());
}}
>
Enter
</Button>
const getIdentity = async (identity: ProxiedSignInIdentity) => {
try {
const identityResponse = await identity.getBackstageIdentity();
return identityResponse;
} catch (error) {
if (
error.name === 'ResponseError' &&
(error as ResponseError).cause.name === 'NotFoundError'
) {
return undefined;
}
throw error;
}
};
const Component: ProviderComponent = ({
onSignInStarted,
onSignInSuccess,
onSignInFailure,
}) => {
const discoveryApi = useApi(discoveryApiRef);
const [_, setUseLegacyGuestToken] = useLocalStorage('enableLegacyGuestToken');
const handle = async () => {
onSignInStarted();
const identity = new ProxiedSignInIdentity({
provider: 'guest',
discoveryApi,
});
const identityResponse = await getIdentity(identity);
if (!identityResponse) {
// eslint-disable-next-line no-alert
const useLegacyGuestTokenResponse = confirm(
'Failed to sign in as a guest using the auth backend. Do you want to fallback to the legacy guest token?',
);
if (useLegacyGuestTokenResponse) {
setUseLegacyGuestToken(true);
onSignInSuccess(new GuestUserIdentity());
return;
}
>
<Typography variant="body1">
Enter as a Guest User.
<br />
You will not have a verified identity,
<br />
meaning some features might be unavailable.
</Typography>
</InfoCard>
</GridItem>
);
onSignInFailure();
throw new Error(
`You cannot sign in as a guest, you must either enable the legacy guest token or configure the auth backend to support guest sign in.`,
);
}
const loader: ProviderLoader = async () => {
return new GuestUserIdentity();
onSignInSuccess(identity);
};
return (
<GridItem>
<InfoCard
title="Guest"
variant="fullHeight"
actions={
<Button color="primary" variant="outlined" onClick={handle}>
Enter
</Button>
}
>
<Typography variant="body1">Sign in as a Guest.</Typography>
</InfoCard>
</GridItem>
);
};
const loader: ProviderLoader = async apis => {
const useLegacyGuestToken =
localStorage.getItem('enableLegacyGuestToken') === 'true';
const identity = new ProxiedSignInIdentity({
provider: 'guest',
discoveryApi: apis.get(discoveryApiRef)!,
});
const identityResponse = await getIdentity(identity);
if (!identityResponse && !useLegacyGuestToken) {
return undefined;
} else if (identityResponse && useLegacyGuestToken) {
// eslint-disable-next-line no-alert
const switchToNewGuestToken = confirm(
'You are currently using the legacy guest token, but you have the new guest backend module installed. Do you want to use the new module?',
);
if (switchToNewGuestToken) {
localStorage.removeItem('enableLegacyGuestToken');
} else {
return new GuestUserIdentity();
}
} else if (useLegacyGuestToken) {
return new GuestUserIdentity();
}
return identity;
};
export const guestProvider: SignInProvider = { Component, loader };
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,8 @@
# Auth Module: Guest Provider
This module provides a guest auth provider implementation for `@backstage/plugin-auth-backend`. This is meant to supersede the existing `'guest'` option for authentication that does not emit tokens and is completely stored as frontend state.
## Links
- [Backstage](https://backstage.io)
- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-guest-provider)
@@ -0,0 +1,11 @@
## API Report File for "@backstage/plugin-auth-backend-module-guest-provider"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
// @public (undocumented)
const authModuleGuestProvider: () => BackendFeature;
export default authModuleGuestProvider;
```
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-auth-backend-module-guest-provider
title: '@backstage/plugin-auth-backend-module-guest-provider'
description: The guest-provider backend module for the auth plugin.
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright 2024 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 interface Config {
/** Configuration options for the auth plugin */
auth?: {
providers: {
guest?: {
/**
* The entity reference to use for the guest user.
* @default user:development/guest
*/
userEntityRef?: string;
/**
* A list of entity references to user for ownership of the guest user if the user
* is not found in the catalog.
* @default [userEntityRef]
*/
ownershipEntityRefs?: string[];
/**
* Allow users to sign in with the guest provider outside of their development environments.
*/
dangerouslyAllowOutsideDevelopment?: boolean;
};
};
};
}
@@ -0,0 +1,47 @@
{
"name": "@backstage/plugin-auth-backend-module-guest-provider",
"description": "The guest-provider backend module for the auth plugin.",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/auth-backend-module-guest-provider"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"passport-oauth2": "^1.7.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"express": "^4.18.2"
},
"files": [
"dist"
]
}
@@ -0,0 +1,27 @@
/*
* Copyright 2024 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 { createProxyAuthenticator } from '@backstage/plugin-auth-node';
export const guestAuthenticator = createProxyAuthenticator({
defaultProfileTransform: async () => {
return { profile: {} };
},
initialize() {},
async authenticate() {
return { result: {} };
},
});
@@ -0,0 +1,23 @@
/*
* Copyright 2024 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.
*/
/**
* The guest-provider backend module for the auth plugin.
*
* @packageDocumentation
*/
export { authModuleGuestProvider as default } from './module';
@@ -0,0 +1,56 @@
/*
* Copyright 2024 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 {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import {
authProvidersExtensionPoint,
createProxyAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import { guestAuthenticator } from './authenticator';
import { signInAsGuestUser } from './resolvers';
/** @public */
export const authModuleGuestProvider = createBackendModule({
pluginId: 'auth',
moduleId: 'guest-provider',
register(reg) {
reg.registerInit({
deps: {
logger: coreServices.logger,
providers: authProvidersExtensionPoint,
config: coreServices.rootConfig,
},
async init({ providers, logger, config }) {
if (process.env.NODE_ENV !== 'development') {
logger.warn(
'You should NOT be using the guest provider outside of a development environment.',
);
}
providers.registerProvider({
providerId: 'guest',
factory: createProxyAuthProviderFactory({
authenticator: guestAuthenticator,
signInResolver: signInAsGuestUser(
config.getConfig('auth.providers.guest'),
),
}),
});
},
});
},
});
@@ -0,0 +1,59 @@
/*
* Copyright 2024 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 { stringifyEntityRef } from '@backstage/catalog-model';
import type { Config } from '@backstage/config';
import { SignInResolver } from '@backstage/plugin-auth-node';
import { NotImplementedError } from '@backstage/errors';
/**
* Provide a default implementation of the user to resolve to. By default, this
* is `user:development/guest`. We will attempt to get that user if they're in the
* catalog. If that user doesn't exist in the catalog, we will still create a
* token for them so they can keep viewing.
*/
export const signInAsGuestUser: (config: Config) => SignInResolver<{}> =
(config: Config) => async (_, ctx) => {
if (
process.env.NODE_ENV !== 'development' &&
config.getOptionalBoolean('dangerouslyAllowOutsideDevelopment') !== true
) {
throw new NotImplementedError(
'The guest provider is NOT recommended for use outside of a development environment. If you want to enable this, set `auth.providers.guest.dangerouslyAllowOutsideDevelopment: true` in your app config.',
);
}
const userRef =
config.getOptionalString('userEntityRef') ??
stringifyEntityRef({
kind: 'user',
namespace: 'development',
name: 'guest',
});
const ownershipRefs = config.getOptionalStringArray(
'ownershipEntityRefs',
) ?? [userRef];
try {
return ctx.signInWithCatalogUser({ entityRef: userRef });
} catch (err) {
// We can't guarantee that a guest user exists in the catalog, so we issue a token directly,
return ctx.issueToken({
claims: {
sub: userRef,
ent: ownershipRefs,
},
});
}
};
@@ -0,0 +1,21 @@
/*
* Copyright 2024 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 { ProfileTransform } from '@backstage/plugin-auth-node';
export interface GuestAuthenticator {
defaultProfileTransform: ProfileTransform<{}>;
}
+19 -1
View File
@@ -4682,6 +4682,23 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-auth-backend-module-guest-provider@workspace:^, @backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
express: ^4.18.2
passport-oauth2: ^1.7.0
languageName: unknown
linkType: soft
"@backstage/plugin-auth-backend-module-microsoft-provider@workspace:^, @backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider"
@@ -27400,6 +27417,7 @@ __metadata:
"@backstage/plugin-app-backend": "workspace:^"
"@backstage/plugin-auth-backend": "workspace:^"
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-azure-devops-backend": "workspace:^"
"@backstage/plugin-badges-backend": "workspace:^"
@@ -37336,7 +37354,7 @@ __metadata:
languageName: node
linkType: hard
"passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1":
"passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0":
version: 1.8.0
resolution: "passport-oauth2@npm:1.8.0"
dependencies: