Skip to content

Commit

Permalink
feat: spike for user profile converter
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Sep 19, 2019
1 parent a392251 commit d044af3
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 15 deletions.
22 changes: 22 additions & 0 deletions extensions/authentication-passport/spike-user-profile.md
@@ -0,0 +1,22 @@
# Spike for More Flexible UserProfile

connect to story https://github.com/strongloop/loopback-next/issues/2246

I picked the `authentication-passport` module to start the spike for more flexible user profile because compared with the custom authentication strategies, users have less control to the returned user when using the passport adapter. I believe if we could find a solution for the passport based strategies, applying similar approach to a custom strategy would be easy.

# Solution

A converter function is introduced to be passed into the `StrategyAdapter`'s constructor. It takes in a custom user, converts it to a user profile described by `UserProfile` then returns it.

# Example

See the corresponding change made in file 'authentication-with-passport-strategy-adapter.acceptance.ts':

- Type `UserProfileInDB` is defined to describe the custom user. In a real application it should be a custom User model.
- Define a converter function `converter` that turns an `UserProfileInDb` instance into a user profile. It's provided in the constructor when create the adapter.
- The converter is invoked in the strategy's `authentication()` function to make sure it returns a user profile in type `UserProfile`
- If the strategy is returned in a provider, you can inject the converter.

# To Be Done

Add a field `additionalProperties` in type `AnyObject` in the `UserProfile` interface to allow people add additional fields as the minimum set of identification info.
Expand Up @@ -47,17 +47,17 @@ describe('Basic Authentication', () => {
it('authenticates successfully for correct credentials', async () => {
const client = whenIMakeRequestTo(server);
const credential =
users.list.joe.profile[securityId] + ':' + users.list.joe.password;
users.list.joe.profile.id + ':' + users.list.joe.password;
const hash = Buffer.from(credential).toString('base64');
await client
.get('/whoAmI')
.set('Authorization', 'Basic ' + hash)
.expect(users.list.joe.profile[securityId]);
.expect(users.list.joe.profile.id);
});

it('returns error for invalid credentials', async () => {
const client = whenIMakeRequestTo(server);
const credential = users.list.Simpson.profile[securityId] + ':' + 'invalid';
const credential = users.list.Simpson.profile.id + ':' + 'invalid';
const hash = Buffer.from(credential).toString('base64');
await client
.get('/whoAmI')
Expand All @@ -79,13 +79,12 @@ describe('Basic Authentication', () => {
.expect(200, {running: true});
});

// FIXME: In a real database the column/field won't be a symbol
function givenUserRepository() {
users = new UserRepository({
joe: {profile: {[securityId]: 'joe'}, password: '12345'},
Simpson: {profile: {[securityId]: 'sim123'}, password: 'alpha'},
Flinstone: {profile: {[securityId]: 'Flint'}, password: 'beta'},
George: {profile: {[securityId]: 'Curious'}, password: 'gamma'},
joe: {profile: {id: 'joe'}, password: '12345'},
Simpson: {profile: {id: 'sim123'}, password: 'alpha'},
Flinstone: {profile: {id: 'Flint'}, password: 'beta'},
George: {profile: {id: 'Curious'}, password: 'gamma'},
});
}

Expand All @@ -99,10 +98,18 @@ describe('Basic Authentication', () => {
function verify(username: string, password: string, cb: Function) {
users.find(username, password, cb);
}

function converter(user: UserProfileInDB): UserProfile {
let userProfile = Object.assign({}, user, {[securityId]: user.id});
delete userProfile.id;
return userProfile;
}

const basicStrategy = new BasicStrategy(verify);
const basicAuthStrategy = new StrategyAdapter(
basicStrategy,
AUTH_STRATEGY_NAME,
converter
);

async function givenAServer() {
Expand Down Expand Up @@ -217,14 +224,15 @@ describe('Basic Authentication', () => {
}
});

type UserProfileInDB = Omit<UserProfile, typeof securityId> & {id: string};
class UserRepository {
constructor(
readonly list: {[key: string]: {profile: UserProfile; password: string}},
readonly list: {[key: string]: {profile: UserProfileInDB; password: string}},
) {}
find(username: string, password: string, cb: Function): void {
const userList = this.list;
function search(key: string) {
return userList[key].profile[securityId] === username;
return userList[key].profile.id === username;
}
const found = Object.keys(userList).find(search);
if (!found) return cb(null, false);
Expand Down
24 changes: 19 additions & 5 deletions extensions/authentication-passport/src/strategy-adapter.ts
Expand Up @@ -3,10 +3,11 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {AuthenticationStrategy} from '@loopback/authentication';
import {AuthenticationStrategy, convertUserToUserProfileFn} from '@loopback/authentication';
import {UserProfile} from '@loopback/security';
import {HttpErrors, Request} from '@loopback/rest';
import {Strategy} from 'passport';
import {inspect} from 'util';

const passportRequestMixin = require('passport/lib/http/request');

Expand All @@ -18,12 +19,16 @@ const passportRequestMixin = require('passport/lib/http/request');
* 4. provides state methods to the strategy instance
* see: https://github.com/jaredhanson/passport
*/
export class StrategyAdapter implements AuthenticationStrategy {
export class StrategyAdapter<U> implements AuthenticationStrategy {
/**
* @param strategy instance of a class which implements a passport-strategy;
* @description http://passportjs.org/
*/
constructor(private readonly strategy: Strategy, readonly name: string) {}
constructor(
private readonly strategy: Strategy,
readonly name: string,
private userConverter?: convertUserToUserProfileFn<U>,
) {}

/**
* The function to invoke the contained passport strategy.
Expand All @@ -33,6 +38,7 @@ export class StrategyAdapter implements AuthenticationStrategy {
* @param request The incoming request.
*/
authenticate(request: Request): Promise<UserProfile> {
const self = this;
return new Promise<UserProfile>((resolve, reject) => {
// mix-in passport additions like req.logIn and req.logOut
for (const key in passportRequestMixin) {
Expand All @@ -44,8 +50,16 @@ export class StrategyAdapter implements AuthenticationStrategy {
const strategy = Object.create(this.strategy);

// add success state handler to strategy instance
strategy.success = function(user: UserProfile) {
resolve(user);
// as a generic adapter, it is agnostic of the type of
// the custom user, so loose the type restriction here
// to be `any`
strategy.success = function(user: any) {
if (self.userConverter) {
const userProfile = self.userConverter(user);
resolve(userProfile);
} else {
resolve(user);
}
};

// add failure state handler to strategy instance
Expand Down
4 changes: 4 additions & 0 deletions packages/authentication/src/types.ts
Expand Up @@ -16,6 +16,10 @@ export interface AuthenticateFn {
(request: Request): Promise<UserProfile | undefined>;
}

export interface convertUserToUserProfileFn<U> {
(user: U): UserProfile;
}

/**
* An interface that describes the common authentication strategy.
*
Expand Down

0 comments on commit d044af3

Please sign in to comment.