From 202da8ab36ed6942c10db994031329ec0a4f5e77 Mon Sep 17 00:00:00 2001 From: Govind Date: Thu, 26 Nov 2020 11:27:00 +0100 Subject: [PATCH] Allow application to register custom `AuthProviderFactory`. Refs #1984 (#3329) * Allow application to register custom `AuthProviderFactory`. Refs #1984 * PR comments: Explicit exports * Export `OAuth` specific types from the package * Update documentation on how to use the interfaces * Re-export from base `index.ts` as per ADR-004 * pass `providerId` as a parameter to `AuthFactory` instead of hardcoding Co-authored-by: Govindarajan Nagarajan --- docs/auth/auth-backend-classes.md | 118 +++++++++++++----- plugins/auth-backend/src/index.ts | 8 ++ plugins/auth-backend/src/lib/flow/index.ts | 2 + .../src/providers/auth0/provider.ts | 2 +- .../auth-backend/src/providers/factories.ts | 16 +-- .../src/providers/github/provider.ts | 2 +- .../src/providers/gitlab/provider.ts | 2 +- .../src/providers/google/provider.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 14 ++- .../src/providers/microsoft/provider.ts | 3 +- .../src/providers/oauth2/provider.ts | 2 +- .../src/providers/oidc/provider.ts | 2 +- .../src/providers/okta/provider.ts | 2 +- .../src/providers/onelogin/provider.ts | 2 +- .../src/providers/saml/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 1 + plugins/auth-backend/src/service/router.ts | 29 ++++- 17 files changed, 148 insertions(+), 61 deletions(-) diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 442553d4b8..ae6602e62a 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -6,45 +6,59 @@ description: Documentation on Auth backend classes ## How Does Authentication Work? -The Backstage application can use various authentication providers for -authentication. A provider has to implement an `AuthProviderRouteHandlers` -interface for handling authentication. This interface consists of four methods. -Each of these methods is hosted at an endpoint `/auth/[provider]/method`, where -`method` performs a certain operation as follows: +The Backstage application can use various external authentication providers for +authentication. An external provider is wrapped using an +`AuthProviderRouteHandlers` interface for handling authentication. This +interface consists of four methods. Each of these methods is hosted at an +endpoint (by default)`/api/auth/[provider]/method`, where `method` performs a +certain operation as follows: ``` - /auth/[provider]/start -> start - /auth/[provider]/handler/frame -> frameHandler - /auth/[provider]/refresh -> refresh - /auth/[provider]/logout -> logout + /auth/[provider]/start -> Initiate a login from the web page + /auth/[provider]/handler/frame -> Handle a `finished` authentication operation + /auth/[provider]/refresh -> refresh -> Refresh the validity of a login + /auth/[provider]/logout -> logout a logged-in user ``` -For more information on how these methods are used and for which purpose, refer -to the [OAuth documentation](oauth.md). +The flow is as follows: -For details on the parameters, input and output conditions for each method, -refer to the type documentation under -`plugins/auth-backend/src/providers/types.ts`. +1. `User` attempts to sign-in. +2. The `auth` wrapper re-directs the user to an external authenticator. +3. The authenticator validates the user and returns the result of the validation + (success OR failure), to the wrapper's endpoint (`handler/frame`) +4. `handler/frame` will issue the appropriate response to the webpage. +5. User signs out by clicking on an UI interface and the webpage makes a request + to logout the user. There are currently two different classes for two authentication mechanisms that implement this interface: an `OAuthAdapter` for [OAuth](https://oauth.net/2/) based mechanisms and a `SAMLAuthProvider` for [SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) + +If you do not have a `OAuth2` or a `SAML` based authentication provider, look in +the section [below](#Implementing-Your-Own-Auth-Wrapper) + based mechanisms. ### OAuth mechanisms +For more information on how these methods are used and for which purpose, refer +to the [OAuth documentation](oauth.md). + Currently OAuth is assumed to be the de facto authentication mechanism for Backstage based applications. Backstage comes with a "batteries-included" set of supported commonly used OAuth -providers: Okta, GitHub, Google, GitLab, and a generic OAuth2 provider. +providers: Okta, GitHub, Google, GitLab, and a generic OAuth2 provider. For a +list of available providers, look at the available wrappers in +`backstage/plugins/auth-backend/src/providers/` -All of these use the authorization flow of OAuth2 to implement authentication. +All of these use the **authorization flow** of OAuth2 to implement +authentication. -If your authentication provider is any of the above mentioned (except generic -OAuth2) providers, you can configure them by setting the right variables in -`app-config.yaml` under the `auth` section. +If your authentication provider is any of the above mentioned providers, you can +configure them by setting the right variables in `app-config.yaml` under the +`auth` section. ### Configuration @@ -85,18 +99,59 @@ auth: ... ``` -## Technical Notes +## Implementing Your Own Auth Wrapper -### OAuthEnvironmentHandler +The core interface of any Auth wrapper is the `AuthProviderRouteHandlers` +interface. This interface has 4 methods corresponding to API described in the +initial section. Any Auth Wrapper will have to implement this interface. -The concept of an "env" is core to the way the auth backend works. It uses an +When initiating a login, a pop-up window is created by the frontend, to allow +the user to initiate a login. This login request is done to the `/start` +endpoint which is handled by the `start` method. + +The `start` method re-directs to the external auth provider who authenticates +the request and re-directs the request to the `/frame/handler` endpoint, which +is handled by the `frameHandler` method. + +The `frameHandler` returns a `html` response, containing a script that does a +`postMessage` to the frontend's window containing the result of the request. The +`WebMessageResponse` type is the message sent by the `postMessage` to the +frontend. + +A `postMessageResponse` utility function wraps the logic of generating a +`postMessage` response that ensures that `CORS` is successfully handled. This +function takes an `express.Response`, a `WebMessageResponse` and the URL of the +frontend (`appOrigin`) as parameters and return a `HTML` page with the script +and the message + +### OAuth wrapping Interfaces. + +Each OAuth external provider is supported by a corresponding `Passport` +strategy. For a generic OAuth2 provider, passport has a `passport-oauth2` +strategy. The strategy class handles the implementation details of working with +each provider. + +Each strategy is wrapped by a `OAuthHandlers` interface. + +This interface cannot be directly used as an `express` HTTP Request handler. + +To do so, `OAuthHandlers` are wrapped in a `OAuthAdapter`, which implements the +`AuthProviderRouterHandlers` interface. + +#### Env + +The concept of an `env` is core to the way the auth backend works. It uses an `env` query parameter to identify the environment in which the application is -running (`development`, `staging`, `production`, etc). Each runtime can support -multiple environments at the same time and the right handler for each request is -identified and dispatched to based on the `env` parameter. All -`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`. +running (`development`, `staging`, `production`, etc). Each runtime can +simultaneously support multiple environments at the same time and the right +handler for each request is identified and dispatched to, based on the `env` +parameter. -To instantiate multiple OAuth providers for different environments, use +`OAuthEnvironmentHandler` is a utility wrapper for an `OAuthHandlers` that +implements the `AuthProviderRouteHandlers` interface while supporting multiple +`env`s. + +To instantiate OAuth providers (the same but for different environments), use `OAuthEnvironmentHandler.mapConfig`. It's a helper to iterate over a configuration object that is a map of environment to configurations. See one of the existing OAuth providers for an example of how it is used. @@ -116,8 +171,13 @@ The `OAuthEnvironmentHandler.mapConfig(config, envConfig => ...)` call will split the `config` by the top level `development` and `production` keys, and pass on each block as `envConfig`. -For a list of currently available providers, look in the `factories` module -located in `plugins/auth-backend/src/providers/factories.ts` +For convenience, the `AuthProviderFactory` is a factory function that has to be +implemented which can then generate a `AuthProviderRouteHandlers` for a given +`provider`. + +All of the supported providers provide a `AuthProviderFactory` that returns a +`OAuthEnvironmentHandler` ,capable of handling authentication for multiple +`env`s. ### OAuth2 provider diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 7612c392a2..3d3f059dcc 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -15,3 +15,11 @@ */ export * from './service/router'; +export * from './providers'; + +// flow package provides 2 functions +// ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup +export * from './lib/flow'; + +// OAuth wrapper over a passport or a custom `startegy`. +export * from './lib/oauth'; diff --git a/plugins/auth-backend/src/lib/flow/index.ts b/plugins/auth-backend/src/lib/flow/index.ts index a5f2f7a3ac..7f0627994f 100644 --- a/plugins/auth-backend/src/lib/flow/index.ts +++ b/plugins/auth-backend/src/lib/flow/index.ts @@ -15,3 +15,5 @@ */ export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers'; + +export type { WebMessageResponse } from './types'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index f851fb1eb5..8772842e2a 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -149,12 +149,12 @@ export class Auth0AuthProvider implements OAuthHandlers { } export const createAuth0Provider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'auth0'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const domain = envConfig.getString('domain'); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index d4a29c0959..7bad4f4d81 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -24,9 +24,9 @@ import { createSamlProvider } from './saml'; import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; import { createOneLoginProvider } from './onelogin'; -import { AuthProviderFactory, AuthProviderFactoryOptions } from './types'; +import { AuthProviderFactory } from './types'; -const factories: { [providerId: string]: AuthProviderFactory } = { +export const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, github: createGithubProvider, gitlab: createGitlabProvider, @@ -38,15 +38,3 @@ const factories: { [providerId: string]: AuthProviderFactory } = { oidc: createOidcProvider, onelogin: createOneLoginProvider, }; - -export function createAuthProvider( - providerId: string, - options: AuthProviderFactoryOptions, -) { - const factory = factories[providerId]; - if (!factory) { - throw Error(`No auth provider available for '${providerId}'`); - } - - return factory(options); -} diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index bab7b3fc28..6e3f9a6000 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -137,12 +137,12 @@ export class GithubAuthProvider implements OAuthHandlers { } export const createGithubProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'github'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const enterpriseInstanceUrl = envConfig.getOptionalString( diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 4d4ecc3b56..df6d2daeb7 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -140,12 +140,12 @@ export class GitlabAuthProvider implements OAuthHandlers { } export const createGitlabProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'gitlab'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const audience = envConfig.getString('audience'); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index dd5b51eb01..ae4f5fabd0 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -175,6 +175,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } export const createGoogleProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, logger, @@ -182,7 +183,6 @@ export const createGoogleProvider: AuthProviderFactory = ({ catalogApi, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'google'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 99348438be..3f3d16f28f 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -14,4 +14,16 @@ * limitations under the License. */ -export { createAuthProvider } from './factories'; +export { factories as defaultAuthProviderFactories } from './factories'; + +// Export the minimal interface required for implementing a +// custom Authorization Handler +export type { + AuthProviderRouteHandlers, + AuthProviderFactoryOptions, + AuthProviderFactory, +} from './types'; + +// These types are needed for a postMessage from the login pop-up +// to the frontend +export type { AuthResponse, BackstageIdentity, ProfileInfo } from './types'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index a09d5871d2..b0c3635be8 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -206,13 +206,12 @@ export class MicrosoftAuthProvider implements OAuthHandlers { } export const createMicrosoftProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'microsoft'; - const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const tenantID = envConfig.getString('tenantId'); diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 6342d556e1..04cb7670f3 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -157,12 +157,12 @@ export class OAuth2AuthProvider implements OAuthHandlers { } export const createOAuth2Provider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'oauth2'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index fbf38c69c4..9e44a82c37 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -171,12 +171,12 @@ export class OidcAuthProvider implements OAuthHandlers { } export const createOidcProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'oidc'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 09597696ba..84b258e489 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -168,12 +168,12 @@ export class OktaAuthProvider implements OAuthHandlers { } export const createOktaProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'okta'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const audience = envConfig.getString('audience'); diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 97cf182e5a..838b655d59 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -147,12 +147,12 @@ export class OneLoginProvider implements OAuthHandlers { } export const createOneLoginProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const providerId = 'onelogin'; const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const issuer = envConfig.getString('issuer'); diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index a8edc9a714..7b663a5528 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -121,12 +121,12 @@ type SAMLProviderOptions = { }; export const createSamlProvider: AuthProviderFactory = ({ + providerId, globalConfig, config, tokenIssuer, }) => { const url = new URL(globalConfig.baseUrl); - const providerId = 'saml'; const entryPoint = config.getString('entryPoint'); const issuer = config.getString('issuer'); const opts = { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 0e500e027e..a40f2a96ee 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -113,6 +113,7 @@ export interface AuthProviderRouteHandlers { } export type AuthProviderFactoryOptions = { + providerId: string; globalConfig: AuthProviderConfig; config: Config; logger: Logger; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 47e0d14caa..85d60661e9 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -14,6 +14,14 @@ * limitations under the License. */ +import express from 'express'; +import Router from 'express-promise-router'; +import cookieParser from 'cookie-parser'; +import { Logger } from 'winston'; +import { + defaultAuthProviderFactories, + AuthProviderFactory, +} from '../providers'; import { NotFoundError, PluginDatabaseManager, @@ -21,20 +29,18 @@ import { } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import cookieParser from 'cookie-parser'; -import express from 'express'; -import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; -import { createAuthProvider } from '../providers'; import session from 'express-session'; import passport from 'passport'; +type ProviderFactories = { [s: string]: AuthProviderFactory }; + export interface RouterOptions { logger: Logger; database: PluginDatabaseManager; config: Config; discovery: PluginEndpointDiscovery; + providerFactories?: ProviderFactories; } export async function createRouter({ @@ -42,6 +48,7 @@ export async function createRouter({ config, discovery, database, + providerFactories, }: RouterOptions): Promise { const router = Router(); @@ -74,13 +81,23 @@ export async function createRouter({ router.use(express.urlencoded({ extended: false })); router.use(express.json()); + const allProviderFactories = { + ...defaultAuthProviderFactories, + ...providerFactories, + }; const providersConfig = config.getConfig('auth.providers'); const providers = providersConfig.keys(); for (const providerId of providers) { logger.info(`Configuring provider, ${providerId}`); try { - const provider = createAuthProvider(providerId, { + const providerFactory = allProviderFactories[providerId]; + if (!providerFactory) { + throw Error(`No auth provider available for '${providerId}'`); + } + + const provider = providerFactory({ + providerId, globalConfig: { baseUrl: authUrl, appUrl }, config: providersConfig.getConfig(providerId), logger,