diff --git a/docs/README.md b/docs/README.md index 28d1047942..89160ce9e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,3 +7,4 @@ Check out or see the table of content below. - [References](reference/README.md) - [Publishing](publishing.md) - [Designing for Backstage](design.md) +- [How to add an auth provider](auth/add-auth-provider.md) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md new file mode 100644 index 0000000000..4bbbec88bb --- /dev/null +++ b/docs/auth/add-auth-provider.md @@ -0,0 +1,198 @@ +# Adding authentication providers + +## Passport + +We chose [Passport](http://www.passportjs.org/) as our authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). + +## How to add a new strategy provider + +### Quick guide + +[1.](#installing-the-dependencies) Install the passport-js based provider package. + +[2.](#create-implementation) Create a new folder structure for the provider. + +[3.](#adding-an-oauth-based-provider) Implement the provider, extending the suitable framework if needed. + +[4.](#hook-it-up-to-the-backend) Add the provider to the backend. + +### Installing the dependencies: + +```bash +cd plugins/auth-backend +yarn add passport-provider-a +yarn add @types/passport-provider-a +``` + +### Create implementation + +Make a new folder with the name of the provider following the below file structure: + +```bash +plugins/auth-backend/src/providers/providerA +├── index.ts +└── provider.ts +``` + +**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the provider class which implements a handler for the chosen framework. + +#### Adding an OAuth based provider + +If we're adding an `OAuth` based provider we would implement the [OAuthProviderHandlers](#OAuthProviderHandlers) interface. + +The provider class takes the provider's configuration as a class parameter. It also imports the `Strategy` from the passport package. + +```ts +import { Strategy as ProviderAStrategy } from 'passport-provider-a'; + +export class ProviderAAuthProvider implements OAuthProviderHandlers { + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: ProviderAStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.providerConfig = providerConfig; + this._strategy = new ProviderAStrategy( + { ...providerConfig.options }, + verifyFunction, // See the "Verify Callback" section + ); + } + + async start() {} + async handler() {} +} +``` + +#### Adding an non-OAuth based provider + +_**Note**: We have prioritized OAuth-based providers and non-OAuth providers should be considered experimental._ + +An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. + +```ts +export class ProviderAAuthProvider implements AuthProviderRouteHandlers { + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: ProviderAStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.providerConfig = providerConfig; + this._strategy = new ProviderAStrategy( + { ...providerConfig.options }, + verifyFunction, // See the "Verify Callback" section + ); + } + + async start() {} + async frameHandler() {} + async logout() {} + async refresh() {} // If supported +} +``` + +#### Create method + +Each provider exports a create method that creates the provider instance, optionally extending a supported authorization framework. This method exists to allow for flexibility if additional frameworks are supported in the future. + +Implementing OAuth by returning an instance of `OAuthProvider` based of the provider's class: + +```ts +export function createProviderAProvider(config: AuthProviderConfig) { + const provider = new ProviderAAuthProvider(config); + const oauthProvider = new OAuthProvider(provider, config.provider, true); + return oauthProvider; +} +``` + +Not extending with OAuth, the main difference here is that the create method is returning a instance of the class without adding the OAuth authorization framework to it. + +```ts +export function createProviderAProvider(config: AuthProviderConfig) { + return new ProviderAAuthProvider(config); +} +``` + +#### Verify Callback + +> Strategies require what is known as a verify callback. The purpose of a verify callback is to find the user that possesses a set of credentials. +> When Passport authenticates a request, it parses the credentials contained in the request. It then invokes the verify callback with those credentials as arguments [...]. If the credentials are valid, the verify callback invokes done to supply Passport with the user that authenticated. +> +> If the credentials are not valid (for example, if the password is incorrect), done should be invoked with false instead of a user to indicate an authentication failure. +> +> http://www.passportjs.org/docs/configure/ + +**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply re-exporting the create method to be used for hooking the provider up to the backend. + +```ts +export { createProviderAProvider } from './provider'; +``` + +### Hook it up to the backend + +**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be configured properly so you need to add it to the list of configured providers, all of which implement [AuthProviderConfig](#AuthProviderConfig): + +```ts +export const providers = [ + { + provider: 'providerA', # used as an identifier + options: { ... }, # consult the provider documentation for which options you should provide + disableRefresh: true # if the provider lacks refresh tokens + }, +``` + +**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling `createAuthProviderRouter` on each provider. You need to import the create method from the provider and add it to the factory: + +```ts +import { createProviderAProvider } from './providerA'; +const factories: { [providerId: string]: AuthProviderFactory } = { + providerA: createProviderAProvider, +}; +``` + +By doing this `auth-backend` automatically adds these endpoints: + +```ts +router.get('/auth/providerA/start'); +router.get('/auth/providerA/handler/frame'); +router.post('/auth/providerA/handler/frame'); +router.post('/auth/providerA/logout'); +router.get('/auth/providerA/refresh'); // if supported +``` + +As you can see each endpoint is prefixed with both `/auth` and its provider name. + +### Test the new provider + +You can `curl -i localhost:7000/auth/providerA/start` and which should provide a `302` redirect with a `Location` header. Paste the url from that header into a web browser and you should be able to trigger the authorization flow. + +--- + +##### OAuthProviderHandlers + +```ts +export interface OAuthProviderHandlers { + start(req: express.Request, options: any): Promise; + handler(req: express.Request): Promise; + refresh?(refreshToken: string, scope: string): Promise; + logout?(): Promise; +} +``` + +##### AuthProviderRouteHandlers + +```ts +export interface AuthProviderRouteHandlers { + start(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; + logout(req: express.Request, res: express.Response): Promise; +} +``` + +##### AuthProviderConfig + +```ts +export type AuthProviderConfig = { + provider: string; + options: any; + disableRefresh?: boolean; +}; +``` diff --git a/packages/backend/README.md b/packages/backend/README.md index fc712d4717..48dd7f09fe 100644 --- a/packages/backend/README.md +++ b/packages/backend/README.md @@ -56,6 +56,12 @@ to the absolute path of a YAML file on disk, you could consume your own experime The catalog currently runs in-memory only, so feel free to try it out, but it will need to be re-populated on next startup. +## Authentication + +We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). + +Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md) + ## Documentation - [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index ae17a556c9..147853f06f 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -27,6 +27,10 @@ To try out SAML, you can use the mock identity provider: ./scripts/start-saml-idp.sh ``` +## Authentication providers + +[How to add an auth provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md) + ## Links - (The Backstage homepage)[https://backstage.io]