update to a proxied sign in identity instead of a separate guest provider

Signed-off-by: Aramis <sennyeyaramis@gmail.com>
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
Aramis
2024-02-11 12:45:38 -05:00
committed by aramissennyeydd
parent 875d1137c7
commit 1c64b2af45
25 changed files with 108 additions and 475 deletions
+4 -1
View File
@@ -400,7 +400,10 @@ auth:
myproxy:
development: {}
guest:
development: {}
development:
signIn:
resolvers:
- resolver: guestUser
costInsights:
engineerCost: 200000
@@ -35,7 +35,6 @@ import {
createFetchApi,
FetchMiddlewares,
VMwareCloudAuth,
GuestAuth,
} from '@backstage/core-app-api';
import {
@@ -59,7 +58,6 @@ import {
bitbucketServerAuthApiRef,
atlassianAuthApiRef,
vmwareCloudAuthApiRef,
guestAuthApiRef,
} from '@backstage/core-plugin-api';
import {
permissionApiRef,
@@ -279,21 +277,6 @@ export const apis = [
});
},
}),
createApiFactory({
api: guestAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, configApi }) => {
return GuestAuth.create({
configApi,
discoveryApi,
environment: configApi.getOptionalString('auth.environment'),
});
},
}),
createApiFactory({
api: permissionApiRef,
deps: {
+1 -1
View File
@@ -128,7 +128,7 @@ const app = createApp({
return (
<SignInPage
{...props}
providers={['custom', ...providers]}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
-7
View File
@@ -23,16 +23,9 @@ import {
oneloginAuthApiRef,
bitbucketAuthApiRef,
bitbucketServerAuthApiRef,
guestAuthApiRef,
} from '@backstage/core-plugin-api';
export const providers = [
{
id: 'guest-auth-provider',
title: 'Guest',
message: 'Sign in as a guest',
apiRef: guestAuthApiRef,
},
{
id: 'google-auth-provider',
title: 'Google',
+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:^",
+3
View File
@@ -57,4 +57,7 @@ backend.add(import('@backstage/plugin-sonarqube-backend'));
backend.add(import('@backstage/plugin-signals-backend'));
backend.add(import('@backstage/plugin-notifications-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.start();
@@ -1,113 +0,0 @@
/*
* 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 {
AuthRequestOptions,
BackstageIdentityApi,
ProfileInfo,
ProfileInfoApi,
SessionApi,
SessionState,
BackstageIdentityResponse,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { AuthApiCreateOptions } from '../types';
import { RefreshingDirectAuthConnector } from '../../../../lib/AuthConnector/RefreshingDirectAuthConnector';
type GuestSession = {
profile: ProfileInfo;
backstageIdentity: BackstageIdentityResponse;
};
const DEFAULT_PROVIDER = {
id: 'guest',
title: 'Guest',
icon: () => null,
};
/**
* Implements a guest auth flow. Heavily based on SAML flow with added support for refreshing the token.
*
* @public
*/
export default class GuestAuth
implements ProfileInfoApi, BackstageIdentityApi, SessionApi
{
static create(options: AuthApiCreateOptions) {
const {
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
} = options;
const connector = new RefreshingDirectAuthConnector<GuestSession>({
discoveryApi,
environment,
provider,
});
const sessionManager = new RefreshingAuthSessionManager<GuestSession>({
connector,
defaultScopes: new Set([]),
sessionScopes: (_: GuestSession) => new Set<string>(),
sessionShouldRefresh: (session: GuestSession) => {
let min = Infinity;
if (session.backstageIdentity?.expiresAt) {
min = Math.min(
min,
(session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000,
);
}
return min < 60 * 5;
},
});
return new GuestAuth({ sessionManager });
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
private readonly sessionManager: SessionManager<GuestSession>;
private constructor(options: {
sessionManager: SessionManager<GuestSession>;
}) {
this.sessionManager = options.sessionManager;
}
async signIn() {
await this.getBackstageIdentity({});
}
async signOut() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentityResponse | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
}
@@ -1,16 +0,0 @@
/*
* 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 { default as GuestAuth } from './GuestAuth';
@@ -26,5 +26,4 @@ export * from './bitbucket';
export * from './bitbucketServer';
export * from './atlassian';
export * from './vmwareCloud';
export * from './guest';
export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types';
@@ -70,7 +70,7 @@ export class DirectAuthConnector<DirectAuthResponse> {
}
}
protected async buildUrl(path: string): Promise<string> {
private async buildUrl(path: string): Promise<string> {
const baseUrl = await this.discoveryApi.getBaseUrl('auth');
return `${baseUrl}/${this.provider.id}${path}?env=${this.environment}`;
}
@@ -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,55 @@ import Button from '@material-ui/core/Button';
import { InfoCard } from '../InfoCard/InfoCard';
import { GridItem } from './styles';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { GuestUserIdentity } from './GuestUserIdentity';
import { ProxiedSignInIdentity } from '../ProxiedSignInPage/ProxiedSignInIdentity';
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => (
<GridItem>
<InfoCard
title="Guest"
variant="fullHeight"
actions={
<Button
color="primary"
variant="outlined"
onClick={() => {
onSignInStarted();
onSignInSuccess(new GuestUserIdentity());
}}
>
Enter
</Button>
}
>
<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>
);
const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => {
const discoveryApi = useApi(discoveryApiRef);
return (
<GridItem>
<InfoCard
title="Guest"
variant="fullHeight"
actions={
<Button
color="primary"
variant="outlined"
onClick={() => {
onSignInStarted();
onSignInSuccess(
new ProxiedSignInIdentity({
provider: 'guest',
discoveryApi,
}),
);
}}
>
Enter
</Button>
}
>
<Typography variant="body1">Sign in as a Guest.</Typography>
</InfoCard>
</GridItem>
);
};
const loader: ProviderLoader = async () => {
return new GuestUserIdentity();
const loader: ProviderLoader = async apis => {
const identity = new ProxiedSignInIdentity({
provider: 'guest',
discoveryApi: apis.get(discoveryApiRef)!,
});
await identity.start();
const identityResponse = await identity.getBackstageIdentity();
if (!identityResponse) {
return undefined;
}
return identity;
};
export const guestProvider: SignInProvider = { Component, loader };
@@ -43,7 +43,6 @@ export type SignInProviderType = {
};
const signInProviders: { [key: string]: SignInProvider } = {
/** @deprecated Use `@backstage/plugin-auth-backend-module-guest-provider` */
guest: guestProvider,
custom: customProvider,
common: commonProvider,
@@ -469,15 +469,3 @@ export const vmwareCloudAuthApiRef: ApiRef<
> = createApiRef({
id: 'core.auth.vmware-cloud',
});
/**
* Provides guest authentication support.
*
* @public
* @remarks
*/
export const guestAuthApiRef: ApiRef<
ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: 'core.auth.guest',
});
@@ -292,7 +292,6 @@ describe('createApp', () => {
<api:core.auth.bitbucket-server out=[core.api.factory] />
<api:core.auth.atlassian out=[core.api.factory] />
<api:core.auth.vmware-cloud out=[core.api.factory] />
<api:core.auth.guest out=[core.api.factory] />
<api:plugin.permission.api out=[core.api.factory] />
]
</app>"
@@ -2,7 +2,7 @@
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.
**NOTE**: This provider should only ever be enabled for `development` or `test`. Enabling this for production is strongly discouraged as it would give everyone a way to bypass your other authentication methods.
**NOTE**: This provider should only ever be enabled for `development`. This package is explicitly disabled for non-development environments.
## Installation
@@ -20,42 +20,15 @@ const backend = createBackend();
backend.start();
```
#### Old Backend
This module was also backported for the old backend and can be used like so,
```diff
+import {
+ providers,
+} from '@backstage/plugin-auth-backend';
....
return await createRouter({
...
providerFactories: {
gitlab: providers.gitlab(),
+ guest: providers.guest(),
...
}
...
```
### Frontend
Add the following to your `SignInPage` providers,
```diff
+import {
+ guestAuthApiRef,
+} from '@backstage/core-plugin-api';
const providers = [
+ {
+ id: 'guest-auth-provider',
+ title: 'Guest',
+ message: 'Sign in as a guest',
+ apiRef: guestAuthApiRef,
+ },
+ 'guest',
...
]
```
### Config
@@ -1,3 +1,5 @@
import { createProxyAuthenticator } from '@backstage/plugin-auth-node';
/*
* Copyright 2024 The Backstage Authors
*
@@ -13,4 +15,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { guest } from './provider';
export const guestAuthenticator = createProxyAuthenticator({
defaultProfileTransform: async () => {
return { profile: {} };
},
initialize() {},
async authenticate() {
return { result: {} };
},
});
@@ -1,59 +0,0 @@
/*
* Copyright 2023 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 { SignInResolverFactory } from '@backstage/plugin-auth-node';
import type {
AuthProviderFactory,
ProfileTransform,
SignInResolver,
} from '@backstage/plugin-auth-node';
import { createGuestAuthRouteHandlers } from './createGuestAuthRouteHandlers';
import { guestResolver } from './resolvers';
const defaultTransform: ProfileTransform<{}> = async () => {
return {
profile: {
displayName: 'Guest',
},
};
};
/** @public */
export function createGuestAuthProviderFactory(options?: {
profileTransform?: ProfileTransform<{}>;
signInResolver?: SignInResolver<{}>;
signInResolverFactories?: Record<string, SignInResolverFactory<{}, unknown>>;
}): AuthProviderFactory {
return ctx => {
const signInResolver = options?.signInResolver ?? guestResolver();
if (!signInResolver) {
throw new Error(
`No sign-in resolver configured for guest auth provider '${ctx.providerId}'`,
);
}
const profileTransform = options?.profileTransform ?? defaultTransform;
return createGuestAuthRouteHandlers({
signInResolver,
baseUrl: ctx.baseUrl,
appUrl: ctx.appUrl,
config: ctx.config,
resolverContext: ctx.resolverContext,
profileTransform,
});
};
}
@@ -1,91 +0,0 @@
/*
* Copyright 2020 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 type { Request, Response } from 'express';
import type { Config } from '@backstage/config';
import {
AuthProviderRouteHandlers,
AuthResolverContext,
ClientAuthResponse,
ProfileTransform,
SignInResolver,
prepareBackstageIdentityResponse,
sendWebMessageResponse,
} from '@backstage/plugin-auth-node';
export interface GuestAuthRouteHandlersOptions {
config: Config;
baseUrl: string;
appUrl: string;
resolverContext: AuthResolverContext;
signInResolver: SignInResolver<{}>;
profileTransform: ProfileTransform<{}>;
}
export function createGuestAuthRouteHandlers(
options: GuestAuthRouteHandlersOptions,
): AuthProviderRouteHandlers {
const { resolverContext, signInResolver, appUrl, profileTransform } = options;
const createGuestSession = async (): Promise<ClientAuthResponse<{}>> => {
const { profile } = await profileTransform({}, resolverContext);
const identity = await signInResolver(
{ profile, result: {} },
resolverContext,
);
return {
profile,
providerInfo: {},
backstageIdentity: prepareBackstageIdentityResponse(identity),
};
};
return {
async start(_, res): Promise<void> {
// We are the auth provider for guests, skip this step.
res.redirect('handler/frame');
},
/**
* This is where we create the token for the guest user. You can override the
* entityRef for the guest user with `signInResolver`.
*/
async frameHandler(_, res): Promise<void> {
const session = await createGuestSession();
// post message back to popup if successful
sendWebMessageResponse(res, appUrl, {
type: 'authorization_response',
response: session,
});
},
/**
* Support refreshing the guest user's token. This should just improve the experience of
* browsing while in guest mode.
*/
async refresh(this: never, _: Request, res: Response): Promise<void> {
const session = await createGuestSession();
res.status(200).json(session);
},
async logout(_, res) {
// If we don't send a response or it gets cached into a 204, the page will hang.
res.end();
},
};
}
@@ -20,5 +20,4 @@
* @packageDocumentation
*/
export { createGuestAuthProviderFactory } from './createGuestAuthFactory';
export { authModuleGuestProvider as default } from './module';
@@ -17,8 +17,12 @@ import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { authProvidersExtensionPoint } from '@backstage/plugin-auth-node';
import { createGuestAuthProviderFactory } from './createGuestAuthFactory';
import {
authProvidersExtensionPoint,
createProxyAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import { guestAuthenticator } from './authenticator';
import { signInAsGuestUser } from './resolvers';
/** @public */
export const authModuleGuestProvider = createBackendModule({
@@ -31,14 +35,17 @@ export const authModuleGuestProvider = createBackendModule({
providers: authProvidersExtensionPoint,
},
async init({ providers }) {
if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Guest provider does not support authenticating production workloads.',
);
}
providers.registerProvider({
providerId: 'guest',
factory: createGuestAuthProviderFactory(),
factory: createProxyAuthProviderFactory({
authenticator: guestAuthenticator,
signInResolver: signInAsGuestUser,
}),
});
},
});
@@ -15,7 +15,7 @@
*/
import { stringifyEntityRef } from '@backstage/catalog-model';
import { createSignInResolverFactory } from '@backstage/plugin-auth-node';
import { SignInResolver } from '@backstage/plugin-auth-node';
/**
* Provide a default implementation of the user to resolve to. By default, this
@@ -23,24 +23,20 @@ import { createSignInResolverFactory } from '@backstage/plugin-auth-node';
* 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 guestResolver = createSignInResolverFactory({
create() {
return async (_, ctx) => {
const userRef = stringifyEntityRef({
kind: 'user',
name: 'guest',
});
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: [userRef],
},
});
}
};
},
});
export const signInAsGuestUser: SignInResolver<{}> = async (_, ctx) => {
const userRef = stringifyEntityRef({
kind: 'user',
name: 'guest',
});
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: [userRef],
},
});
}
};
@@ -1,48 +0,0 @@
/*
* 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 { createAuthProviderIntegration } from '../createAuthProviderIntegration';
import { AuthHandler, SignInResolver } from '../types';
import { createGuestAuthProviderFactory } from '@backstage/plugin-auth-backend-module-guest-provider';
/**
* Auth provider integration for Google auth
*
* @public
*/
export const guest = createAuthProviderIntegration({
create(options?: {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<{}>;
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn?: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<{}>;
};
}) {
return createGuestAuthProviderFactory({
profileTransform: options?.authHandler,
signInResolver: options?.signIn?.resolver,
});
},
});
@@ -30,7 +30,6 @@ import { oidc } from './oidc';
import { okta } from './okta';
import { onelogin } from './onelogin';
import { saml } from './saml';
import { guest } from './guest';
import { bitbucketServer } from './bitbucketServer';
import { easyAuth } from './azure-easyauth';
import { AuthProviderFactory } from '@backstage/plugin-auth-node';
@@ -59,7 +58,6 @@ export const providers = Object.freeze({
onelogin,
saml,
easyAuth,
guest,
});
/**
@@ -85,5 +83,4 @@ export const defaultAuthProviderFactories: {
bitbucket: bitbucket.create(),
bitbucketServer: bitbucketServer.create(),
atlassian: atlassian.create(),
guest: guest.create(),
};
+5 -14
View File
@@ -1,3 +1,6 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 6
cacheKey: 8
@@ -27411,6 +27414,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:^"
@@ -37347,7 +37351,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:
@@ -37360,19 +37364,6 @@ __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.7.0":
version: 1.7.0
resolution: "passport-oauth2@npm:1.7.0"
dependencies:
base64url: 3.x.x
oauth: 0.10.x
passport-strategy: 1.x.x
uid2: 0.0.x
utils-merge: 1.x.x
checksum: a9a80b968343c9c1906f74ef613b346ec2d6a6acfe17af81e673fd774779b436729252485755c3ce182f2cdba2434d75067418952d722404d65b93c0360ca02b
languageName: node
linkType: hard
"passport-oauth@npm:1.0.0, passport-oauth@npm:^1.0.0":
version: 1.0.0
resolution: "passport-oauth@npm:1.0.0"