@loopback/authentication-passport
Version:
A package creating adapters between the passport module and @loopback/authentication
404 lines (316 loc) • 13.8 kB
Markdown
# Passport Strategy Adapter
_Important: We strongly recommend that users learn LoopBack's
[authentication system](https://loopback.io/doc/en/lb4/Authentication-overview.html)
before using this module._
LoopBack 4 authentication system is highly extensible and give users the
flexibility to provide custom strategies. To be compatible with over 500+
Express [`passport`](https://www.npmjs.com/package/passport) middleware, this
adapter module is created for plugging in
[`passport`](https://www.npmjs.com/package/passport) based strategies to the
authentication system in `/authentication@3.x`.
If you would like to try with an example,
[`/example-passport-login`](https://github.com/loopbackio/loopback-next/tree/master/examples/passport-login)
uses this module to authenticate APIs with several OAuth 2.0 passport strategies
like Facebook, Google.
## Installation
```sh
npm i /authentication-passport --save
```
## Background
`/authentication@3.x` allows users to register authentication
strategies that implement the interface
[`AuthenticationStrategy`](https://loopback.io/doc/en/lb4/apidocs.authentication.authenticationstrategy.html)
Since `AuthenticationStrategy` describes a strategy with different contracts
than the passport
[`Strategy`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/passport/index.d.ts#L79),
and we'd like to support the existing 500+ community passport strategies, an
**adapter class** is created in this package to convert a passport strategy to
the one that LoopBack 4 authentication system wants.
## Usage
For the examples that follow, we will be using `passport-http`, so be sure to
install these modules:
```
npm i passport-http /passport-http --save
```
### Simple Usage
1. Create an instance of the passport strategy
Taking the basic strategy exported from
[`passport-http`](https://github.com/jaredhanson/passport-http) as an
example, first create an instance of the basic strategy with your `verify`
function.
```ts
// Create a file named `my-basic-auth-strategy.ts` to define your strategy below
import {BasicStrategy} from 'passport-http';
function verify(username: string, password: string, cb: Function) {
users.find(username, password, cb);
}
const basicStrategy = new BasicStrategy(verify);
```
It's a similar configuration as you add a strategy to a `passport` by calling
`passport.use()`.
2. Supply a _user profile factory_ which converts a user to a user profile. It
must abide by the `UserProfileFactory` interface supplied by
`/authentication@3.x`.
It is shown below for your convenience.
```ts
export interface UserProfileFactory<U> {
(user: U): UserProfile;
}
```
A default user profile factory is provided for you in the StrategyAdapter
constructor, but it does very little. It simply returns the user model as-is.
```ts
private userProfileFactory: UserProfileFactory<U> = (u: unknown) => {
return u as UserProfile;
},
```
So it is recommended you provide a more meaningful mapping.
An example of a user profile factory converting a specific user type `MyUser`
to type `UserProfile` is shown below.
```ts
//In file 'my.userprofile.factory.ts'
import {UserProfileFactory} from '/authentication';
import {securityId, UserProfile} from '/security';
export const myUserProfileFactory: UserProfileFactory<MyUser> = function (
user: MyUser,
): UserProfile {
const userProfile = {[securityId]: user.id};
return userProfile;
};
```
3. Apply the adapter to the strategy
```ts
// In file 'my-basic-auth-strategy.ts'
import {BasicStrategy} from 'passport-http';
import {UserProfileFactory} from '/authentication';
import {securityId, UserProfile} from '/security';
import {myUserProfileFactory} from '<path to user profile factory>';
function verify(username: string, password: string, cb: Function) {
users.find(username, password, cb);
}
const basicStrategy = new BasicStrategy(verify);
// Apply the adapter
export const AUTH_STRATEGY_NAME = 'basic';
export const basicAuthStrategy = new StrategyAdapter(
// The configured basic strategy instance
basicStrategy,
// Give the strategy a name
// You'd better define your strategy name as a constant, like
// `const AUTH_STRATEGY_NAME = 'basic'`.
// You will need to decorate the APIs later with the same name.
AUTH_STRATEGY_NAME,
// Provide a user profile factory
myUserProfileFactory,
);
```
4. Register(bind) the strategy to app
```ts
import {Application, CoreTags} from '/core';
import {AuthenticationBindings} from '/authentication';
import {basicAuthStrategy} from './my-basic-auth-strategy';
app
.bind('authentication.strategies.basicAuthStrategy')
.to(basicAuthStrategy)
.tag({
[CoreTags.EXTENSION_FOR]:
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
});
```
5. Decorate your endpoint
To authenticate your request with the basic strategy, decorate your
controller function like:
```ts
import {AUTH_STRATEGY_NAME} from './my-basic-auth-strategy';
import {SecurityBindings, UserProfile} from '/security';
import {authenticate} from '/authentication';
class MyController {
constructor(
(SecurityBindings.USER, {optional: true})
private user: UserProfile,
) {}
// Define your strategy name as a constant so that
// it is consistent with the name you provide in the adapter
(AUTH_STRATEGY_NAME)
async whoAmI(): Promise<string> {
return this.user.id;
}
}
```
6. Add the authentication action to your sequence
This part is same as registering a non-passport based strategy. Please make
sure you follow the documentation
[adding-an-authentication-action-to-a-custom-sequence](https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#adding-an-authentication-action-to-a-custom-sequence)
to rewrite your sequence. You can also find a sample implementation in
[this example tutorial](https://loopback.io/doc/en/lb4/Authentication-tutorial.html#creating-a-custom-sequence-and-adding-the-authentication-action).
### With Provider
If you need to inject stuff (e.g. the verify function, user profile factory
function) when configuring the strategy, you may want to provide your strategy
as a provider.
_Note: If you are not familiar with LoopBack providers, check the documentation
in
[Extending LoopBack 4](https://loopback.io/doc/en/lb4/Extending-LoopBack-4.html)_
1. Create a provider for the strategy
Use `passport-http` as the example again:
```ts
// Create a file named `my-basic-auth-strategy.ts` to define your strategy below
import {AuthenticationStrategy} from '/authentication';
import {Provider} from '/core';
class PassportBasicAuthProvider implements Provider<AuthenticationStrategy> {
value(): AuthenticationStrategy {
// The code that returns the converted strategy
}
}
```
The Provider should have two functions:
- A function that takes in the verify callback function and returns a
configured basic strategy. To know more about the configuration, please
check
[the configuration guide in module `passport-http`](https://github.com/jaredhanson/passport-http#usage-of-http-basic).
- A function that applies the `StrategyAdapter` to the configured basic
strategy instance. Then in the `value()` function, you return the converted
strategy.
So a full implementation of the provider is:
```ts
// In file 'providers/my-basic-auth-strategy.ts'
import {BasicStrategy, BasicVerifyFunction} from 'passport-http';
import {StrategyAdapter} from `/passport-adapter`;
import {
AuthenticationStrategy,
AuthenticationBindings,
} from '/authentication';
import {Provider, inject} from '/core';
export class PassportBasicAuthProvider<MyUser>
implements Provider<AuthenticationStrategy>
{
constructor(
('authentication.basic.verify')
private verifyFn: BasicVerifyFunction,
(AuthenticationBindings.USER_PROFILE_FACTORY)
private myUserProfileFactory: UserProfileFactory<MyUser>,
) {}
value(): AuthenticationStrategy {
const basicStrategy = this.configuredBasicStrategy(this.verifyFn);
return this.convertToAuthStrategy(basicStrategy);
}
// Takes in the verify callback function and returns a configured basic strategy.
configuredBasicStrategy(verifyFn: BasicVerifyFunction): BasicStrategy {
return new BasicStrategy(verifyFn);
}
// Applies the `StrategyAdapter` to the configured basic strategy instance.
// You'd better define your strategy name as a constant, like
// `const AUTH_STRATEGY_NAME = 'basic'`
// You will need to decorate the APIs later with the same name
// Pass in the user profile factory
convertToAuthStrategy(basic: BasicStrategy): AuthenticationStrategy {
return new StrategyAdapter(
basic,
AUTH_STRATEGY_NAME,
this.myUserProfileFactory,
);
}
}
```
2. Create a provider for the verify function.
Here is an example provider named VerifyFunctionProvider which has a
`value()` method that returns a function of type BasicVerifyFunction.
```ts
// In file 'providers/verifyfn.provider.ts'
import {Provider} from '/core';
import {repository} from '/repository';
import {BasicVerifyFunction} from 'passport-http';
import {INVALID_USER_CREDENTIALS_MESSAGE} from '../keys';
export class VerifyFunctionProvider
implements Provider<BasicVerifyFunction>
{
constructor( ('users') private userRepo: MyUserRepository) {}
value(): BasicVerifyFunction {
const myThis = this;
return async function (
username: string,
password: string,
cb: Function,
) {
let user: MyUser;
try {
//find user with specific username
const users: MyUser[] = await myThis.userRepo.find({
where: {username: username},
});
// if no user found with this username, throw an error.
if (users.length < 1) {
let error = new Error(INVALID_USER_CREDENTIALS_MESSAGE); //assign 401 in sequence
throw error;
}
//verify given password matches the user's password
user = users[0];
if (user.password !== password) {
let error = new Error(INVALID_USER_CREDENTIALS_MESSAGE); //assign 401 in sequence
throw error;
}
//return null for error, and the valid user
cb(null, user);
} catch (error) {
//return the error, and null for the user
cb(error, null);
}
};
}
}
```
3. Register(bind) the providers
Register **VerifyFunctionProvider** which is required by
**PassportBasicAuthProvider**. Then register **PassportBasicAuthProvider** in
your LoopBack application so that the authentication system can look for your
strategy by name and invoke it.
```ts
// In the main file
import {addExtension} from '/core';
import {MyApplication} from '<path_to_your_app>';
import {PassportBasicAuthProvider} from '<path_to_the_provider>';
import {VerifyFunctionProvider} from '<path_to_the_provider>';
import {
AuthenticationBindings,
AuthenticationComponent,
} from '/authentication';
const app = new MyApplication();
//load the authentication component
app.component(AuthenticationComponent);
// bind the user repo
app.bind('repositories.users').toClass(MyUserRepository);
// bind the authenticated sequence (mentioned later in this document)
app.sequence(MyAuthenticationSequence);
// the verify function for passport-http
app.bind('authentication.basic.verify').toProvider(VerifyFunctionProvider);
// register PassportBasicAuthProvider as a custom authentication strategy
addExtension(
app,
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
PassportBasicAuthProvider,
{
namespace:
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
},
);
```
4. Decorate your endpoint
To authenticate your request with the basic strategy, decorate your
controller function like:
```ts
import {AUTH_STRATEGY_NAME} from './my-basic-auth-strategy';
import {authenticate} from '/authentication';
class MyController {
constructor( (SecurityBindings.USER) private user: UserProfile) {}
// Define your strategy name as a constant so that
// it is consistent with the name you provide in the adapter
(AUTH_STRATEGY_NAME)
async whoAmI(): Promise<string> {
return this.user.id;
}
}
```
5. Add the authentication action to your sequence
This part is same as registering a non-passport based strategy. Please make
sure you follow the documentation
[adding-an-authentication-action-to-a-custom-sequence](https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#adding-an-authentication-action-to-a-custom-sequence)
to rewrite your sequence. You can also find a sample implementation in
[this example tutorial](https://loopback.io/doc/en/lb4/Authentication-tutorial.html#creating-a-custom-sequence-and-adding-the-authentication-action).