feat: migrate auth0 provider to nbs
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-node': minor
|
||||
---
|
||||
|
||||
Accepts an optional options object in the `PassportOAuthAuthenticatorHelper.authenticate` method.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Migrated the `Auth0` auth provider to be implemented using the new `@backstage/plugin-auth-backend-module-auth0-provider` module.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-auth0-provider': minor
|
||||
---
|
||||
|
||||
New module for `@backstage/plugin-auth-backend` that adds a Auth0 auth provider.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: provider--old
|
||||
title: Auth0 Authentication Provider
|
||||
sidebar_label: Auth0
|
||||
description: Adding Auth0 as an authentication provider in Backstage
|
||||
---
|
||||
|
||||
:::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](./provider.md)
|
||||
instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
The Backstage `core-plugin-api` package comes with an Auth0 authentication
|
||||
provider that can authenticate users using OAuth.
|
||||
|
||||
## Create an Auth0 Application
|
||||
|
||||
1. Log in to the [Auth0 dashboard](https://manage.auth0.com/dashboard/)
|
||||
2. Navigate to **Applications**
|
||||
3. Create an Application
|
||||
- Name: Backstage (or your custom app name)
|
||||
- Application type: Single Page Web Application
|
||||
4. Click on the Settings tab
|
||||
5. Add under `Application URIs` > `Allowed Callback URLs`:
|
||||
`http://localhost:7007/api/auth/auth0/handler/frame`
|
||||
6. Click `Save Changes`
|
||||
|
||||
## Configuration
|
||||
|
||||
The provider configuration can then be added to your `app-config.yaml` under the
|
||||
root `auth` configuration:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment: development
|
||||
providers:
|
||||
auth0:
|
||||
development:
|
||||
clientId: ${AUTH_AUTH0_CLIENT_ID}
|
||||
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
|
||||
domain: ${AUTH_AUTH0_DOMAIN_ID}
|
||||
audience: ${AUTH_AUTH0_AUDIENCE}
|
||||
connection: ${AUTH_AUTH0_CONNECTION}
|
||||
connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE}
|
||||
session:
|
||||
secret: ${AUTH_SESSION_SECRET}
|
||||
```
|
||||
|
||||
The Auth0 provider is a structure with these configuration keys:
|
||||
|
||||
- `clientId`: The Application client ID, found on the Auth0 Application page
|
||||
- `clientSecret`: The Application client secret, found on the Auth0 Application
|
||||
page
|
||||
- `domain`: The Application domain, found on the Auth0 Application page
|
||||
|
||||
It additionally relies on the following configuration to function:
|
||||
|
||||
- `session.secret`: The session secret is a key used for signing and/or encrypting cookies set by the application to maintain session state. In this case, 'your session secret' should be replaced with a long, complex, and unique string that only your application knows.
|
||||
|
||||
Auth0 requires a session, so you need to give the session a secret key.
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
- `audience`: The intended recipients of the token
|
||||
- `connection`: Social identity provider name. To check the available social connections, please visit [Auth0 Social Connections](https://marketplace.auth0.com/features/social-connections).
|
||||
- `connectionScope`: Additional scopes in the interactive token request. It should always be used in combination with the `connection` parameter
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
To add the provider to the frontend, add the `auth0AuthApi` reference and
|
||||
`SignInPage` component as shown in
|
||||
[Adding the provider to the sign-in page](../index.md#sign-in-configuration).
|
||||
@@ -5,6 +5,13 @@ sidebar_label: Auth0
|
||||
description: Adding Auth0 as an authentication provider in Backstage
|
||||
---
|
||||
|
||||
:::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](./provider--old.md)
|
||||
instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
The Backstage `core-plugin-api` package comes with an Auth0 authentication
|
||||
provider that can authenticate users using OAuth.
|
||||
|
||||
@@ -54,12 +61,46 @@ It additionally relies on the following configuration to function:
|
||||
|
||||
Auth0 requires a session, so you need to give the session a secret key.
|
||||
|
||||
## Optional Configuration
|
||||
### Optional
|
||||
|
||||
- `audience`: The intended recipients of the token
|
||||
- `connection`: Social identity provider name. To check the available social connections, please visit [Auth0 Social Connections](https://marketplace.auth0.com/features/social-connections).
|
||||
- `connectionScope`: Additional scopes in the interactive token request. It should always be used in combination with the `connection` parameter
|
||||
|
||||
### Resolvers
|
||||
|
||||
This provider includes several resolvers out of the box that you can use:
|
||||
|
||||
- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`.
|
||||
- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`.
|
||||
|
||||
:::note Note
|
||||
|
||||
The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
|
||||
|
||||
:::
|
||||
|
||||
If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation.
|
||||
|
||||
## Backend Installation
|
||||
|
||||
To add the provider to the backend we will first need to install the package by running this command:
|
||||
|
||||
```bash title="from your Backstage root directory"
|
||||
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-auth0-provider
|
||||
```
|
||||
|
||||
Then we will need to add this line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
//...
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
// highlight-add-next-line
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-auth0-provider'));
|
||||
//...
|
||||
```
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
To add the provider to the frontend, add the `auth0AuthApi` reference and
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,8 @@
|
||||
# Auth Module: Auth0 Provider
|
||||
|
||||
This module provides an Auth0 auth provider implementation for `@backstage/plugin-auth-backend`.
|
||||
|
||||
## Links
|
||||
|
||||
- [Repository](https://gitlab.com/backstage/backstage/tree/master/plugins/auth-backend-module-auth0-provider)
|
||||
- [Backstage Project Homepage](https://backstage.io)
|
||||
@@ -0,0 +1,51 @@
|
||||
## API Report File for "@backstage/plugin-auth-backend-module-auth0-provider"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import Auth0InternalStrategy from 'passport-auth0';
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
|
||||
import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
|
||||
import { PassportProfile } from '@backstage/plugin-auth-node';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
|
||||
// @public (undocumented)
|
||||
export const auth0Authenticator: OAuthAuthenticator<
|
||||
{
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
audience: string | undefined;
|
||||
connection: string | undefined;
|
||||
connectionScope: string | undefined;
|
||||
},
|
||||
PassportProfile
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class Auth0Strategy extends Auth0InternalStrategy {
|
||||
constructor(
|
||||
options: Auth0StrategyOptionsWithRequest,
|
||||
verify: Auth0InternalStrategy.VerifyFunction,
|
||||
);
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Auth0StrategyOptionsWithRequest {
|
||||
// (undocumented)
|
||||
callbackURL: string;
|
||||
// (undocumented)
|
||||
clientID: string;
|
||||
// (undocumented)
|
||||
clientSecret: string;
|
||||
// (undocumented)
|
||||
domain: string;
|
||||
// (undocumented)
|
||||
passReqToCallback: true;
|
||||
// (undocumented)
|
||||
store: StateStore;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
const authModuleAuth0Provider: BackendFeature;
|
||||
export default authModuleAuth0Provider;
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-auth-backend-module-auth0-provider
|
||||
title: '@backstage/plugin-auth-backend-module-auth0-provider'
|
||||
description: The auth0-provider backend module for the auth plugin.
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 {
|
||||
auth?: {
|
||||
providers?: {
|
||||
/** @visibility frontend */
|
||||
auth0?: {
|
||||
[authEnv: string]: {
|
||||
clientId: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
domain: string;
|
||||
callbackUrl?: string;
|
||||
audience?: string;
|
||||
connection?: string;
|
||||
connectionScope?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { createBackend } from '@backstage/backend-defaults';
|
||||
import authPlugin from '@backstage/plugin-auth-backend';
|
||||
import authModuleAuth0Provider from '../src';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(authPlugin);
|
||||
backend.add(authModuleAuth0Provider);
|
||||
|
||||
backend.start();
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend-module-auth0-provider",
|
||||
"version": "0.0.0",
|
||||
"description": "The auth0-provider backend module for the auth plugin.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "auth",
|
||||
"pluginPackage": "@backstage/plugin-auth-backend"
|
||||
},
|
||||
"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-auth0-provider"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@types/passport-auth0": "^1.0.5",
|
||||
"@types/passport-oauth2": "^1.4.15",
|
||||
"express": "^4.17.1",
|
||||
"passport-auth0": "^1.4.3",
|
||||
"passport-oauth2": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-defaults": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-auth-backend": "workspace:^",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 express from 'express';
|
||||
import {
|
||||
createOAuthAuthenticator,
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportOAuthDoneCallback,
|
||||
PassportProfile,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { Auth0Strategy } from './strategy';
|
||||
|
||||
/** @public */
|
||||
export const auth0Authenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientID = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const domain = config.getString('domain');
|
||||
const audience = config.getOptionalString('audience');
|
||||
const connection = config.getOptionalString('connection');
|
||||
const connectionScope = config.getOptionalString('connectionScope');
|
||||
const callbackURL = config.getOptionalString('callbackUrl') ?? callbackUrl;
|
||||
// Due to passport-auth0 forcing options.state = true,
|
||||
// passport-oauth2 requires express-session to be installed
|
||||
// so that the 'state' parameter of the oauth2 flow can be stored.
|
||||
// This implementation of StateStore matches the NullStore found within
|
||||
// passport-oauth2, which is the StateStore implementation used when options.state = false,
|
||||
// allowing us to avoid using express-session in order to integrate with auth0.
|
||||
const store = {
|
||||
store(_req: express.Request, cb: any) {
|
||||
cb(null, null);
|
||||
},
|
||||
verify(_req: express.Request, _state: string, cb: any) {
|
||||
cb(null, true);
|
||||
},
|
||||
};
|
||||
|
||||
const helper = PassportOAuthAuthenticatorHelper.from(
|
||||
new Auth0Strategy(
|
||||
{
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
domain,
|
||||
store,
|
||||
// We need passReqToCallback set to false to get params, but there's
|
||||
// no matching type signature for that, so instead behold this beauty
|
||||
passReqToCallback: false as true,
|
||||
},
|
||||
(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: any,
|
||||
fullProfile: PassportProfile,
|
||||
done: PassportOAuthDoneCallback,
|
||||
) => {
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
fullProfile,
|
||||
accessToken,
|
||||
params,
|
||||
},
|
||||
{
|
||||
refreshToken,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
return { helper, audience, connection, connectionScope };
|
||||
},
|
||||
|
||||
async start(
|
||||
input,
|
||||
{ helper, audience, connection, connectionScope: connection_scope },
|
||||
) {
|
||||
return helper.start(input, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
...(audience ? { audience } : {}),
|
||||
...(connection ? { connection } : {}),
|
||||
...(connection_scope ? { connection_scope } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async authenticate(
|
||||
input,
|
||||
{ helper, audience, connection, connectionScope: connection_scope },
|
||||
) {
|
||||
return helper.authenticate(input, {
|
||||
...(audience ? { audience } : {}),
|
||||
...(connection ? { connection } : {}),
|
||||
...(connection_scope ? { connection_scope } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async refresh(input, { helper }) {
|
||||
return helper.refresh(input);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 auth0-provider backend module for the auth plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export {
|
||||
Auth0Strategy,
|
||||
type Auth0StrategyOptionsWithRequest,
|
||||
} from './strategy';
|
||||
export { auth0Authenticator } from './authenticator';
|
||||
export { authModuleAuth0Provider as default } from './module';
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import authPlugin from '@backstage/plugin-auth-backend';
|
||||
import { authModuleAuth0Provider } from './module';
|
||||
import request from 'supertest';
|
||||
import { decodeOAuthState } from '@backstage/plugin-auth-node';
|
||||
|
||||
describe('authModuleAuth0Provider', () => {
|
||||
it('should start', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
authPlugin,
|
||||
authModuleAuth0Provider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
auth0: {
|
||||
development: {
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
domain: 'domain',
|
||||
connection: 'connection',
|
||||
connectionScope: 'connectionScope',
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
secret: 'secret',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const agent = request.agent(server);
|
||||
|
||||
const res = await agent.get('/api/auth/auth0/start?env=development');
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
|
||||
const nonceCookie = agent.jar.getCookie('auth0-nonce', {
|
||||
domain: 'localhost',
|
||||
path: '/api/auth/auth0/handler',
|
||||
script: false,
|
||||
secure: false,
|
||||
});
|
||||
expect(nonceCookie).toBeDefined();
|
||||
|
||||
const startUrl = new URL(res.get('location'));
|
||||
expect(startUrl.origin).toBe('https://domain');
|
||||
expect(startUrl.pathname).toBe('/authorize');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
scope: '',
|
||||
client_id: 'clientId',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/auth0/handler/frame`,
|
||||
prompt: 'consent',
|
||||
accessType: 'offline',
|
||||
connection: 'connection',
|
||||
connection_scope: 'connectionScope',
|
||||
nonce: expect.any(String),
|
||||
state: expect.any(String),
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
env: 'development',
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
authProvidersExtensionPoint,
|
||||
commonSignInResolvers,
|
||||
createOAuthProviderFactory,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { auth0Authenticator } from './authenticator';
|
||||
|
||||
/** @public */
|
||||
export const authModuleAuth0Provider = createBackendModule({
|
||||
pluginId: 'auth',
|
||||
moduleId: 'auth0-provider',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
providers: authProvidersExtensionPoint,
|
||||
},
|
||||
async init({ providers }) {
|
||||
providers.registerProvider({
|
||||
providerId: 'auth0',
|
||||
factory: createOAuthProviderFactory({
|
||||
authenticator: auth0Authenticator,
|
||||
signInResolverFactories: {
|
||||
...commonSignInResolvers,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 Auth0InternalStrategy from 'passport-auth0';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
|
||||
/** @public */
|
||||
export interface Auth0StrategyOptionsWithRequest {
|
||||
clientID: string;
|
||||
clientSecret: string;
|
||||
callbackURL: string;
|
||||
domain: string;
|
||||
passReqToCallback: true;
|
||||
store: StateStore;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class Auth0Strategy extends Auth0InternalStrategy {
|
||||
constructor(
|
||||
options: Auth0StrategyOptionsWithRequest,
|
||||
verify: Auth0InternalStrategy.VerifyFunction,
|
||||
) {
|
||||
const optionsWithURLs = {
|
||||
...options,
|
||||
authorizationURL: `https://${options.domain}/authorize`,
|
||||
tokenURL: `https://${options.domain}/oauth/token`,
|
||||
userInfoURL: `https://${options.domain}/userinfo`,
|
||||
apiUrl: `https://${options.domain}/api`,
|
||||
};
|
||||
super(optionsWithURLs, verify);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-auth0-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^",
|
||||
|
||||
@@ -14,40 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import Auth0Strategy from './strategy';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
OAuthResult,
|
||||
} from '../../lib/oauth';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { OAuthStartResponse, AuthHandler } from '../types';
|
||||
import { OAuthProviderOptions, OAuthResult } from '../../lib/oauth';
|
||||
|
||||
import { AuthHandler } from '../types';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
import {
|
||||
AuthResolverContext,
|
||||
createOAuthProviderFactory,
|
||||
SignInResolver,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
adaptLegacyOAuthHandler,
|
||||
adaptLegacyOAuthSignInResolver,
|
||||
} from '../../lib/legacy';
|
||||
import { auth0Authenticator } from '@backstage/plugin-auth-backend-module-auth0-provider';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated The Auth0 auth provider was extracted to `@backstage/plugin-auth-backend-module-auth0-provider`.
|
||||
*/
|
||||
export type Auth0AuthProviderOptions = OAuthProviderOptions & {
|
||||
domain: string;
|
||||
signInResolver?: SignInResolver<OAuthResult>;
|
||||
@@ -58,155 +43,6 @@ export type Auth0AuthProviderOptions = OAuthProviderOptions & {
|
||||
connectionScope?: string;
|
||||
};
|
||||
|
||||
export class Auth0AuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: Auth0Strategy;
|
||||
private readonly signInResolver?: SignInResolver<OAuthResult>;
|
||||
private readonly authHandler: AuthHandler<OAuthResult>;
|
||||
private readonly resolverContext: AuthResolverContext;
|
||||
private readonly audience?: string;
|
||||
private readonly connection?: string;
|
||||
private readonly connectionScope?: string;
|
||||
|
||||
/**
|
||||
* Due to passport-auth0 forcing options.state = true,
|
||||
* passport-oauth2 requires express-session to be installed
|
||||
* so that the 'state' parameter of the oauth2 flow can be stored.
|
||||
* This implementation of StateStore matches the NullStore found within
|
||||
* passport-oauth2, which is the StateStore implementation used when options.state = false,
|
||||
* allowing us to avoid using express-session in order to integrate with auth0.
|
||||
*/
|
||||
private store: StateStore = {
|
||||
store(_req: express.Request, cb: any) {
|
||||
cb(null, null);
|
||||
},
|
||||
verify(_req: express.Request, _state: string, cb: any) {
|
||||
cb(null, true);
|
||||
},
|
||||
};
|
||||
|
||||
constructor(options: Auth0AuthProviderOptions) {
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.authHandler = options.authHandler;
|
||||
this.resolverContext = options.resolverContext;
|
||||
this.audience = options.audience;
|
||||
this.connection = options.connection;
|
||||
this.connectionScope = options.connectionScope;
|
||||
this._strategy = new Auth0Strategy(
|
||||
{
|
||||
clientID: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
callbackURL: options.callbackUrl,
|
||||
domain: options.domain,
|
||||
// We need passReqToCallback set to false to get params, but there's
|
||||
// no matching type signature for that, so instead behold this beauty
|
||||
passReqToCallback: false as true,
|
||||
store: this.store,
|
||||
},
|
||||
(
|
||||
accessToken: any,
|
||||
refreshToken: any,
|
||||
params: any,
|
||||
fullProfile: passport.Profile,
|
||||
done: PassportDoneCallback<OAuthResult, PrivateInfo>,
|
||||
) => {
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
fullProfile,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
params,
|
||||
},
|
||||
{
|
||||
refreshToken,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
scope: req.scope,
|
||||
state: encodeState(req.state),
|
||||
...(this.audience ? { audience: this.audience } : {}),
|
||||
...(this.connection ? { connection: this.connection } : {}),
|
||||
...(this.connectionScope
|
||||
? { connection_scope: this.connectionScope }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(req: express.Request) {
|
||||
const { result, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResult,
|
||||
PrivateInfo
|
||||
>(req, this._strategy, {
|
||||
...(this.audience ? { audience: this.audience } : {}),
|
||||
...(this.connection ? { connection: this.connection } : {}),
|
||||
...(this.connectionScope
|
||||
? { connection_scope: this.connectionScope }
|
||||
: {}),
|
||||
});
|
||||
|
||||
return {
|
||||
response: await this.handleResult(result),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(req: OAuthRefreshRequest) {
|
||||
const { accessToken, refreshToken, params } =
|
||||
await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
req.refreshToken,
|
||||
req.scope,
|
||||
);
|
||||
|
||||
const fullProfile = await executeFetchUserProfileStrategy(
|
||||
this._strategy,
|
||||
accessToken,
|
||||
);
|
||||
|
||||
return {
|
||||
response: await this.handleResult({
|
||||
fullProfile,
|
||||
params,
|
||||
accessToken,
|
||||
}),
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleResult(result: OAuthResult) {
|
||||
const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
|
||||
const response: OAuthResponse = {
|
||||
providerInfo: {
|
||||
idToken: result.params.id_token,
|
||||
accessToken: result.accessToken,
|
||||
scope: result.params.scope,
|
||||
expiresInSeconds: result.params.expires_in,
|
||||
},
|
||||
profile,
|
||||
};
|
||||
|
||||
if (this.signInResolver) {
|
||||
response.backstageIdentity = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
},
|
||||
this.resolverContext,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth provider integration for auth0 auth
|
||||
*
|
||||
@@ -230,44 +66,10 @@ export const auth0 = createAuthProviderIntegration({
|
||||
resolver: SignInResolver<OAuthResult>;
|
||||
};
|
||||
}) {
|
||||
return ({ providerId, globalConfig, config, resolverContext }) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const domain = envConfig.getString('domain');
|
||||
const customCallbackUrl = envConfig.getOptionalString('callbackUrl');
|
||||
const audience = envConfig.getOptionalString('audience');
|
||||
const connection = envConfig.getOptionalString('connection');
|
||||
const connectionScope = envConfig.getOptionalString('connectionScope');
|
||||
const callbackUrl =
|
||||
customCallbackUrl ||
|
||||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
|
||||
? options.authHandler
|
||||
: async ({ fullProfile, params }) => ({
|
||||
profile: makeProfileInfo(fullProfile, params.id_token),
|
||||
});
|
||||
|
||||
const signInResolver = options?.signIn?.resolver;
|
||||
|
||||
const provider = new Auth0AuthProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
domain,
|
||||
authHandler,
|
||||
signInResolver,
|
||||
resolverContext,
|
||||
audience,
|
||||
connection,
|
||||
connectionScope,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
providerId,
|
||||
callbackUrl,
|
||||
});
|
||||
});
|
||||
return createOAuthProviderFactory({
|
||||
authenticator: auth0Authenticator,
|
||||
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
||||
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -503,6 +503,7 @@ export class PassportOAuthAuthenticatorHelper {
|
||||
// (undocumented)
|
||||
authenticate(
|
||||
input: OAuthAuthenticatorAuthenticateInput,
|
||||
options?: Record<string, string>,
|
||||
): Promise<OAuthAuthenticatorResult<PassportProfile>>;
|
||||
// (undocumented)
|
||||
static defaultProfileTransform: ProfileTransform<
|
||||
|
||||
@@ -85,12 +85,13 @@ export class PassportOAuthAuthenticatorHelper {
|
||||
|
||||
async authenticate(
|
||||
input: OAuthAuthenticatorAuthenticateInput,
|
||||
options?: Record<string, string>,
|
||||
): Promise<OAuthAuthenticatorResult<PassportProfile>> {
|
||||
const { result, privateInfo } =
|
||||
await PassportHelpers.executeFrameHandlerStrategy<
|
||||
PassportOAuthResult,
|
||||
PassportOAuthPrivateInfo
|
||||
>(input.req, this.#strategy);
|
||||
>(input.req, this.#strategy, options);
|
||||
|
||||
return {
|
||||
fullProfile: result.fullProfile as PassportProfile,
|
||||
|
||||
@@ -4795,6 +4795,25 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-auth-backend-module-auth0-provider@workspace:^, @backstage/plugin-auth-backend-module-auth0-provider@workspace:plugins/auth-backend-module-auth0-provider":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-auth-backend-module-auth0-provider@workspace:plugins/auth-backend-module-auth0-provider"
|
||||
dependencies:
|
||||
"@backstage/backend-defaults": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/plugin-auth-backend": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@types/passport-auth0": ^1.0.5
|
||||
"@types/passport-oauth2": ^1.4.15
|
||||
express: ^4.17.1
|
||||
passport-auth0: ^1.4.3
|
||||
passport-oauth2: ^1.6.1
|
||||
supertest: ^6.3.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-auth-backend-module-aws-alb-provider@workspace:^, @backstage/plugin-auth-backend-module-aws-alb-provider@workspace:plugins/auth-backend-module-aws-alb-provider":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-auth-backend-module-aws-alb-provider@workspace:plugins/auth-backend-module-aws-alb-provider"
|
||||
@@ -5118,6 +5137,7 @@ __metadata:
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-auth0-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "workspace:^"
|
||||
"@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user