UNPKG

@loopback/authentication-passport

Version:

A package creating adapters between the passport module and @loopback/authentication

404 lines (316 loc) 13.8 kB
# 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 `@loopback/authentication@3.x`. If you would like to try with an example, [`@loopback/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 @loopback/authentication-passport --save ``` ## Background `@loopback/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 @types/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 `@loopback/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 '@loopback/authentication'; import {securityId, UserProfile} from '@loopback/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 '@loopback/authentication'; import {securityId, UserProfile} from '@loopback/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 '@loopback/core'; import {AuthenticationBindings} from '@loopback/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 '@loopback/security'; import {authenticate} from '@loopback/authentication'; class MyController { constructor( @inject(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 @authenticate(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 '@loopback/authentication'; import {Provider} from '@loopback/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 `@loopback/passport-adapter`; import { AuthenticationStrategy, AuthenticationBindings, } from '@loopback/authentication'; import {Provider, inject} from '@loopback/core'; export class PassportBasicAuthProvider<MyUser> implements Provider<AuthenticationStrategy> { constructor( @inject('authentication.basic.verify') private verifyFn: BasicVerifyFunction, @inject(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 '@loopback/core'; import {repository} from '@loopback/repository'; import {BasicVerifyFunction} from 'passport-http'; import {INVALID_USER_CREDENTIALS_MESSAGE} from '../keys'; export class VerifyFunctionProvider implements Provider<BasicVerifyFunction> { constructor(@repository('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 '@loopback/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 '@loopback/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 '@loopback/authentication'; class MyController { constructor(@inject(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 @authenticate(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).