|
|
|
@@ -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<any>;
|
|
|
|
|
handler(req: express.Request): Promise<any>;
|
|
|
|
|
refresh?(refreshToken: string, scope: string): Promise<any>;
|
|
|
|
|
logout?(): Promise<any>;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
##### AuthProviderRouteHandlers
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
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>;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
##### AuthProviderConfig
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
export type AuthProviderConfig = {
|
|
|
|
|
provider: string;
|
|
|
|
|
options: any;
|
|
|
|
|
disableRefresh?: boolean;
|
|
|
|
|
};
|
|
|
|
|
```
|