From b3545f427965ef343b73466390fbabbd1d5ea1ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 10 Jun 2022 16:42:06 +0200 Subject: [PATCH] docs: merge and clean up auth contribution docs Signed-off-by: Patrik Oldsberg --- docs/auth/add-auth-provider.md | 277 +++++++++++++++++++----------- docs/auth/auth-backend-classes.md | 206 ---------------------- microsite/sidebars.json | 3 +- mkdocs.yml | 3 +- 4 files changed, 181 insertions(+), 308 deletions(-) delete mode 100644 docs/auth/auth-backend-classes.md diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index bd2bcd1694..eb8973bec1 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -1,9 +1,113 @@ --- id: add-auth-provider -title: Adding authentication providers -description: Documentation on Adding authentication providers +title: Contributing New Providers +description: Documentation on adding new authentication providers --- +> NOTE: The primary audience for this documentation are contributors to the main +> Backstage project that want to add support for new authentication providers. +> While you can follow it to implement your own custom providers it is much +> more advanced than using our built-in providers. + +## How Does Authentication Work? + +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 -> Initiate a login from the web page + /auth/[provider]/handler/frame -> Handle a finished authentication operation + /auth/[provider]/refresh -> Refresh the validity of a login + /auth/[provider]/logout -> Log out a logged-in user +``` + +The flow is as follows: + +1. A user attempts to sign in. +2. A popup window is opened, pointing to the `auth` endpoint. That endpoint does + initial preparations and then re-directs the user to an external + authenticator, still inside the popup. +3. The authenticator validates the user and returns the result of the validation + (success OR failure), to the wrapper's endpoint (`handler/frame`). +4. The `handler/frame` rendered webpage will issue the appropriate response to + the webpage that opened the popup window, and the popup is closed. +5. The user signs out by clicking on a UI interface and the webpage makes a + request to logout the user. + +## Implementing Your Own Auth Wrapper + +The core interface of any auth wrapper is the `AuthProviderRouteHandlers` +interface. This interface has four methods corresponding to the API described in +the initial section. Any auth wrapper will have to implement this interface. + +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 an HTML response, containing a script that does a +`postMessage` to the frontend 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 an HTML page with the script and +the message. + +There is a helper class for [OAuth2](https://oauth.net/2/) based authentication providers, [OAuthAdapter](../reference/plugin-auth-backend.oauthadapter.md). This class implements the `AuthProviderRouteHandlers` interface +for you, and instead requires you to implement [OAuthHandlers](../reference/plugin-auth-backend.oauthhandlers.md), which +is significantly easier. + +### Auth Environment Separation + +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 +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. + +`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 environments to configurations. See one of +the existing OAuth providers for an example of how it is used. + +Given the following configuration: + +```yaml +development: + clientId: abc + clientSecret: secret +production: + clientId: xyz + clientSecret: supersecret +``` + +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 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 an `AuthProviderFactory` that returns an +`OAuthEnvironmentHandler`, capable of handling authentication for multiple +environments. + ## Passport We chose [Passport](http://www.passportjs.org/) as our authentication platform @@ -46,13 +150,13 @@ plugins/auth-backend/src/providers/providerA **`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 +### Adding an OAuth based provider If we're adding an `OAuth` based provider we would implement the -[OAuthProviderHandlers](#OAuthProviderHandlers) interface. By implementing this +`OAuthHandlers` 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](#AuthProviderRouteHandlers) interface that providers +`AuthProviderRouteHandlers` interface that providers otherwise need to implement. The provider class takes the provider's options as a class parameter. It also @@ -65,7 +169,7 @@ export type ProviderAProviderOptions = OAuthProviderOptions & { // extra options here } -export class ProviderAAuthProvider implements OAuthProviderHandlers { +export class ProviderAAuthProvider implements OAuthHandlers { private readonly _strategy: ProviderAStrategy; constructor(options: ProviderAProviderOptions) { @@ -87,13 +191,10 @@ export class ProviderAAuthProvider implements OAuthProviderHandlers { } ``` -#### Adding an non-OAuth based provider - -_**Note**: We have prioritized OAuth-based providers and non-OAuth providers -should be considered experimental._ +### Adding an non-OAuth based provider An non-`OAuth` based provider could implement -[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. +`AuthProviderRouteHandlers` instead. ```ts type ProviderAOptions = { @@ -119,13 +220,19 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { } ``` -#### Factory function +#### Integration Wrapper -Each provider exports a factory function that instantiates the provider. The -factory should implement [AuthProviderFactory](#AuthProviderFactory), which +Each provider exports an object that provides a way to create new instances +of the provider, along with related utilities like predefined sign-in resolvers. + +The object is created using `createAuthProviderIntegration`, with the most +important part being the `create` method that acts as the factory function +for our provider. + +The factory should return an implementation of `AuthProviderFactory`, which passes in a object with utilities for configuration, logging, token issuing, etc. The factory should return an implementation of -[AuthProviderRouteHandlers](#AuthProviderRouteHandlers). +`AuthProviderRouteHandlers`. The factory is what decides the mapping from [static configuration](../conf/index.md) to the creation of auth providers. For @@ -133,48 +240,70 @@ example, OAuth providers use `OAuthEnvironmentHandler` to allow for multiple different configurations, one for each environment, which looks like this; ```ts -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'); +export const okta = 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; - // instantiate our OAuthProviderHandlers implementation - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - }); + /** + * 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 ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + // read options from config + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); - // Wrap the OAuthProviderHandlers with OAuthProvider, which implements AuthProviderRouteHandlers - return OAuthProvider.fromConfig(globalConfig, provider, { - providerId, - tokenIssuer, - }); - }); + // Use provided auth handler, or create a default one + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + // instantiate our OAuthHandlers implementation + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + // Wrap the OAuthHandlers with OAuthProvider, which implements AuthProviderRouteHandlers + return OAuthProvider.fromConfig(globalConfig, provider, { + providerId, + tokenIssuer, + }); + }); + }, + resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, + + // ... additional predefined resolvers + }, +}); ``` 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: - -```ts -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 @@ -202,7 +331,7 @@ export { createProviderAProvider } from './provider'; **`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 +the factory function of each provider. You need to import the factory function from the provider and add it to the factory: ```ts @@ -232,51 +361,3 @@ You can `curl -i localhost:7007/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 - -```ts -export interface OAuthProviderHandlers { - start( - req: express.Request, - options: Record, - ): Promise; - handler(req: express.Request): Promise<{ - response: AuthResponse; - refreshToken?: string; - }>; - 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; -} -``` - -##### AuthProviderFactory - -```ts -export type AuthProviderFactoryOptions = { - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - tokenIssuer: TokenIssuer; -}; - -export type AuthProviderFactory = ( - options: AuthProviderFactoryOptions, -) => AuthProviderRouteHandlers; -``` diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md deleted file mode 100644 index 5b5950f6c3..0000000000 --- a/docs/auth/auth-backend-classes.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -id: auth-backend-classes -title: Auth backend classes -description: Documentation on Auth backend classes ---- - -## How Does Authentication Work? - -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 -> Initiate a login from the web page - /auth/[provider]/handler/frame -> Handle a finished authentication operation - /auth/[provider]/refresh -> Refresh the validity of a login - /auth/[provider]/logout -> Log out a logged-in user -``` - -The flow is as follows: - -1. A user attempts to sign in. -2. A popup window is opened, pointing to the `auth` endpoint. That endpoint does - initial preparations and then re-directs the user to an external - authenticator, still inside the popup. -3. The authenticator validates the user and returns the result of the validation - (success OR failure), to the wrapper's endpoint (`handler/frame`). -4. The `handler/frame` rendered b´webpage will issue the appropriate response to - the webpage that opened the popup window, and the popup is closed. -5. The user signs out by clicking on a 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 an `OAuth2` or `SAML` based authentication provider, look in -the section [below](#implementing-your-own-auth-wrapper). - -### 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. 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. - -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. - -### SAML - -The SAML Provider is currently under development. Additional validation and -profile handling is still required before use in production. - -To configure the SAML Auth provider, look at the configuration parameters -supported by -[Passport-SAML](https://github.com/node-saml/passport-saml#config-parameter-details) -under the `auth.providers.saml` key - -For security reasons, validate that the response from the IdP is indeed signed -by also providing the `cert` configuration. - -### Configuration - -Each authentication provider (except SAML) needs six parameters: an OAuth client -ID, a client secret, an authorization endpoint, a token endpoint, an optional -list of scopes (as a string separated by spaces) that may be required by the -OAuth2 Server to enable end-user sign-on, and an app origin. The app origin is -the URL at which the frontend of the application is hosted, and it is read from -the `app.baseUrl` config. This is required because the application opens a popup -window to perform the authentication, and once the flow is completed, the popup -window sends a `postMessage` to the frontend application to indicate the result -of the operation. Also this URL is used to verify that authentication requests -are coming from only this endpoint. - -These values are configured via the `app-config.yaml` present in the root of -your app folder. - -``` -auth: - providers: - google: - development: - clientId: ${AUTH_GOOGLE_CLIENT_ID} - clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET} - github: - development: - clientId: ${AUTH_GITHUB_CLIENT_ID} - clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} - enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} - gitlab: - development: - clientId: ${AUTH_GITLAB_CLIENT_ID} - oauth2: - development: - clientId: ${AUTH_OAUTH2_CLIENT_ID} - clientSecret: ${AUTH_OAUTH2_CLIENT_SECRET} - authorizationUrl: ${AUTH_OAUTH2_AUTH_URL} - tokenUrl: ${AUTH_OAUTH2_TOKEN_URL} - scope: ${AUTH_OAUTH2_SCOPE} - saml: - entryPoint: ${AUTH_SAML_ENTRY_POINT} - issuer: ${AUTH_SAML_ISSUER} - ... -``` - -## Implementing Your Own Auth Wrapper - -The core interface of any auth wrapper is the `AuthProviderRouteHandlers` -interface. This interface has four methods corresponding to the API described in -the initial section. Any auth wrapper will have to implement this interface. - -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 an 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 an HTML page with the script and -the message. - -### OAuth Wrapping Interfaces. - -Each OAuth external provider is supported by a corresponding -[Passport](https://github.com/jaredhanson/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 an `OAuthHandlers` interface. - -This interface cannot be directly used as an Express HTTP request handler. To do -so, `OAuthHandlers` are wrapped in an `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 -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. - -`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 environments to configurations. See one of -the existing OAuth providers for an example of how it is used. - -Given the following configuration: - -```yaml -development: - clientId: abc - clientSecret: secret -production: - clientId: xyz - clientSecret: supersecret -``` - -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 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 an `AuthProviderFactory` that returns an -`OAuthEnvironmentHandler`, capable of handling authentication for multiple -environments. - -### OAuth2 Provider - -The `oauth2` provider abstracts a generic **OAuth2 + OIDC** based authentication -provider. What this means is that after the application has been given -permission by the user, the `authorization code` will be exchanged for an -`access_token`, a `refresh_token` and an `id_token`. This `id_token` is used to -obtain an email id of the user, which is then used for creating the session. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 6cbe079def..8c460c7d3d 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -259,11 +259,10 @@ "auth/oauth2-proxy/provider" ] }, - "auth/add-auth-provider", "auth/identity-resolver", "auth/auth-backend", "auth/oauth", - "auth/auth-backend-classes", + "auth/add-auth-provider", "auth/troubleshooting", "auth/glossary" ], diff --git a/mkdocs.yml b/mkdocs.yml index 3bd83832f7..04d62d32d4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -156,11 +156,10 @@ nav: - OneLogin: 'auth/onelogin/provider.md' - OAuth2Proxy: 'auth/oauth2-proxy/provider.md' - Bitbucket: 'auth/bitbucket/provider.md' - - Adding authentication providers: 'auth/add-auth-provider.md' - Sign in resolvers: 'auth/identity-resolver.md' - Auth backend: 'auth/auth-backend.md' - OAuth and OpenID Connect: 'auth/oauth.md' - - Auth backend classes: 'auth/auth-backend-classes.md' + - Contributing New Providers: 'auth/add-auth-provider.md' - Troubleshooting Auth: 'auth/troubleshooting.md' - Glossary: 'auth/glossary.md' - Deployment: