Merge pull request #2268 from spotify/rugvip/authref
auth-backend: refactor to split lib, move types closer to home, update docs, renaming things
This commit is contained in:
+104
-53
@@ -48,22 +48,35 @@ 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.
|
||||
[OAuthProviderHandlers](#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](#AuthProviderRouteHandlers) interface that providers
|
||||
otherwise need to implement.
|
||||
|
||||
The provider class takes the provider's configuration as a class parameter. It
|
||||
also imports the `Strategy` from the passport package.
|
||||
The provider class takes the provider's options as a class parameter. It also
|
||||
imports the `Strategy` from the passport package.
|
||||
|
||||
```ts
|
||||
import { Strategy as ProviderAStrategy } from 'passport-provider-a';
|
||||
|
||||
export type ProviderAProviderOptions = OAuthProviderOptions & {
|
||||
// extra options here
|
||||
}
|
||||
|
||||
export class ProviderAAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
private readonly _strategy: ProviderAStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
constructor(options: ProviderAProviderOptions) {
|
||||
this._strategy = new ProviderAStrategy(
|
||||
{ ...providerConfig.options },
|
||||
{
|
||||
clientID: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
callbackURL: options.callbackUrl,
|
||||
passReqToCallback: false as true,
|
||||
response_type: 'code',
|
||||
/// ... etc
|
||||
}
|
||||
verifyFunction, // See the "Verify Callback" section
|
||||
);
|
||||
}
|
||||
@@ -82,14 +95,18 @@ An non-`OAuth` based provider could implement
|
||||
[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead.
|
||||
|
||||
```ts
|
||||
type ProviderAOptions = {
|
||||
// ...
|
||||
};
|
||||
|
||||
export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
private readonly _strategy: ProviderAStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
constructor(options: ProviderAOptions) {
|
||||
this._strategy = new ProviderAStrategy(
|
||||
{ ...providerConfig.options },
|
||||
{
|
||||
// ...
|
||||
},
|
||||
verifyFunction, // See the "Verify Callback" section
|
||||
);
|
||||
}
|
||||
@@ -101,31 +118,61 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
```
|
||||
|
||||
#### Create method
|
||||
#### Factory function
|
||||
|
||||
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.
|
||||
Each provider exports a factory function that instantiates the provider. The
|
||||
factory should implement [AuthProviderFactory](#AuthProviderFactory), which
|
||||
passes in a object with utilities for configuration, logging, token issuing,
|
||||
etc. The factory should return an implementation of
|
||||
[AuthProviderRouteHandlers](#AuthProviderRouteHandlers).
|
||||
|
||||
Implementing OAuth by returning an instance of `OAuthProvider` based of the
|
||||
provider's class:
|
||||
The factory is what decides the mapping from
|
||||
[static configuration](../conf/index.md) 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;
|
||||
|
||||
```ts
|
||||
export function createProviderAProvider(config: AuthProviderConfig) {
|
||||
const provider = new ProviderAAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider, true);
|
||||
return oauthProvider;
|
||||
}
|
||||
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,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
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 function createProviderAProvider(config: AuthProviderConfig) {
|
||||
return new ProviderAAuthProvider(config);
|
||||
}
|
||||
export const createProviderAProvider: AuthProviderFactory = ({ config }) => {
|
||||
const a = config.getString('a');
|
||||
const b = config.getString('b');
|
||||
|
||||
return new ProviderAAuthProvider({ a, b });
|
||||
};
|
||||
```
|
||||
|
||||
#### Verify Callback
|
||||
@@ -144,7 +191,7 @@ export function createProviderAProvider(config: AuthProviderConfig) {
|
||||
> 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
|
||||
re-exporting the factory function to be used for hooking the provider up to the
|
||||
backend.
|
||||
|
||||
```ts
|
||||
@@ -153,26 +200,14 @@ 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:
|
||||
`createAuthProviderRouter` on each provider. You need to import the factory
|
||||
function from the provider and add it to the factory:
|
||||
|
||||
```ts
|
||||
import { createProviderAProvider } from './providerA';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
providerA: createProviderAProvider,
|
||||
};
|
||||
@@ -203,10 +238,21 @@ web browser and you should be able to trigger the authorization flow.
|
||||
|
||||
```ts
|
||||
export interface OAuthProviderHandlers {
|
||||
start(req: express.Request, options: any): Promise<any>;
|
||||
handler(req: express.Request): Promise<any>;
|
||||
refresh?(refreshToken: string, scope: string): Promise<any>;
|
||||
logout?(): Promise<any>;
|
||||
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>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -221,12 +267,17 @@ export interface AuthProviderRouteHandlers {
|
||||
}
|
||||
```
|
||||
|
||||
##### AuthProviderConfig
|
||||
##### AuthProviderFactory
|
||||
|
||||
```ts
|
||||
export type AuthProviderConfig = {
|
||||
provider: string;
|
||||
options: any;
|
||||
disableRefresh?: boolean;
|
||||
export type AuthProviderFactoryOptions = {
|
||||
globalConfig: AuthProviderConfig;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
options: AuthProviderFactoryOptions,
|
||||
) => AuthProviderRouteHandlers;
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ refer to the type documentation under
|
||||
`plugins/auth-backend/src/providers/types.ts`.
|
||||
|
||||
There are currently two different classes for two authentication mechanisms that
|
||||
implement this interface: an `OAuthProvider` for [OAuth](https://oauth.net/2/)
|
||||
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)
|
||||
based mechanisms.
|
||||
@@ -50,11 +50,12 @@ OAuth2) providers, you can configure them by setting the right variables in
|
||||
Each authentication provider (except SAML) needs five parameters: an OAuth
|
||||
client ID, a client secret, an authorization endpoint and a token endpoint, and
|
||||
an app origin. The app origin is the URL at which the frontend of the
|
||||
application is hosted. 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.
|
||||
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.
|
||||
@@ -64,8 +65,6 @@ auth:
|
||||
providers:
|
||||
google:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GOOGLE_CLIENT_ID
|
||||
@@ -74,8 +73,6 @@ auth:
|
||||
env: AUTH_GOOGLE_CLIENT_SECRET
|
||||
github:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GITHUB_CLIENT_ID
|
||||
@@ -87,8 +84,6 @@ auth:
|
||||
env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
|
||||
gitlab:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
...
|
||||
@@ -96,24 +91,34 @@ auth:
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### EnvironmentHandler
|
||||
### OAuthEnvironmentHandler
|
||||
|
||||
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 `EnvironmentHandler`.
|
||||
`AuthProviderRouteHandlers` are wrapped within an `OAuthEnvironmentHandler`.
|
||||
|
||||
An `EnvironmentHandler` takes an ID for each provider that it wraps, the
|
||||
handlers for each env the provider is supported in, and a function that, given a
|
||||
`Request` as argument, can extract the information about the env under which it
|
||||
should be processed.
|
||||
To instantiate multiple OAuth providers 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.
|
||||
|
||||
Each provider exposes a factory function `createXProvider` (where X is the name
|
||||
of the provider) that takes the global config, env and other parameters and
|
||||
returns an `AuthProviderRouteHandlers` for each env, and an `envIdentifier`
|
||||
function to identify the env of a request.
|
||||
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 a list of currently available providers, look in the `factories` module
|
||||
located in `plugins/auth-backend/src/providers/factories.ts`
|
||||
|
||||
Reference in New Issue
Block a user