Files
backstage/docs/auth/add-auth-provider.md
T

8.2 KiB

id, title, description
id title description
add-auth-provider Adding authentication providers Documentation on Adding authentication providers

Passport

We chose Passport as our authentication platform due to its comprehensive set of supported authentication strategies.

How to add a new strategy provider

Quick guide

1. Install the passport-js based provider package.

2. Create a new folder structure for the provider.

3. Implement the provider, extending the suitable framework if needed.

4. Add the provider to the backend.

Installing the dependencies:

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:

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 interface. By implementing this interface we can use the OAuthProvider class provided by lib/oauth, meaning we don't need to implement the full AuthProviderRouteHandlers interface that providers otherwise need to implement.

The provider class takes the provider's options as a class parameter. It also imports the Strategy from the passport package.

import { Strategy as ProviderAStrategy } from 'passport-provider-a';

export type ProviderAProviderOptions = OAuthProviderOptions & {
  // extra options here
}

export class ProviderAAuthProvider implements OAuthProviderHandlers {
  private readonly _strategy: ProviderAStrategy;

  constructor(options: ProviderAProviderOptions) {
    this._strategy = new ProviderAStrategy(
      {
        clientID: options.clientId,
        clientSecret: options.clientSecret,
        callbackURL: options.callbackUrl,
        passReqToCallback: false as true,
        response_type: 'code',
        /// ... etc
      }
      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 instead.

type ProviderAOptions = {
  // ...
};

export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
  private readonly _strategy: ProviderAStrategy;

  constructor(options: ProviderAOptions) {
    this._strategy = new ProviderAStrategy(
      {
        // ...
      },
      verifyFunction, // See the "Verify Callback" section
    );
  }

  async start() {}
  async frameHandler() {}
  async logout() {}
  async refresh() {} // If supported
}

Factory function

Each provider exports a factory function that instantiates the provider. The factory should implement AuthProviderFactory, which passes in a object with utilities for configuration, logging, token issuing, etc. The factory should return an implementation of AuthProviderRouteHandlers.

The factory is what decides the mapping from static configuration to the creation of auth providers. For example, OAuth providers use OAuthEnvironmentHandler to allow for multiple different configurations, one for each environment, which looks like this;

export const createOktaProvider: AuthProviderFactory = ({
  globalConfig,
  config,
  tokenIssuer,
}) =>
  OAuthEnvironmentHandler.mapConfig(config, envConfig => {
    // read options from config
    const clientId = envConfig.getString('clientId');
    const clientSecret = envConfig.getString('clientSecret');

    // instantiate our OAuthProviderHandlers implementation
    const provider = new OktaAuthProvider({
      audience,
      clientId,
      clientSecret,
      callbackUrl,
    });

    // Wrap the OAuthProviderHandlers with OAuthProvider, which implements AuthProviderRouteHandlers
    return OAuthProvider.fromConfig(globalConfig, provider, {
      disableRefresh: false,
      providerId,
      tokenIssuer,
    });
  });

The purpose of the different environments is to allow for a single auth-backend to serve as the authentication service for multiple different frontend environments, such as local development, staging, and production.

The factory function for other providers can be a lot simpler, as they might not have configuration for each environment. Looking something like this:

export const createProviderAProvider: AuthProviderFactory = ({ config }) => {
  const a = config.getString('a');
  const b = config.getString('b');

  return new ProviderAAuthProvider({ a, b });
};

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 factory function to be used for hooking the provider up to the backend.

export { createProviderAProvider } from './provider';

Hook it up to the backend

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 factory function from the provider and add it to the factory:

import { createProviderAProvider } from './providerA';

const factories: { [providerId: string]: AuthProviderFactory } = {
  providerA: createProviderAProvider,
};

By doing this auth-backend automatically adds these endpoints:

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/api/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
export interface OAuthProviderHandlers {
  start(
    req: express.Request,
    options: Record<string, string>,
  ): Promise<RedirectInfo>;
  handler(
    req: express.Request,
  ): Promise<{
    response: AuthResponse<OAuthProviderInfo>;
    refreshToken?: string;
  }>;
  refresh?(
    refreshToken: string,
    scope: string,
  ): Promise<AuthResponse<OAuthProviderInfo>>;
  logout?(): Promise<void>;
}
AuthProviderRouteHandlers
export interface AuthProviderRouteHandlers {
  start(req: express.Request, res: express.Response): Promise<any>;
  frameHandler(req: express.Request, res: express.Response): Promise<any>;
  refresh?(req: express.Request, res: express.Response): Promise<any>;
  logout(req: express.Request, res: express.Response): Promise<any>;
}
AuthProviderFactory
export type AuthProviderFactoryOptions = {
  globalConfig: AuthProviderConfig;
  config: Config;
  logger: Logger;
  tokenIssuer: TokenIssuer;
};

export type AuthProviderFactory = (
  options: AuthProviderFactoryOptions,
) => AuthProviderRouteHandlers;