Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PoC][WIP, not ready for review]feat: add abstractions in authentication module #2445

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@loopback/metadata": "^1.0.6",
"@loopback/openapi-v3": "^1.2.1",
"@loopback/rest": "^1.5.5",
"lodash": "^4.17.11",
"passport": "^0.4.0",
"passport-strategy": "^1.0.0"
},
Expand Down
8 changes: 6 additions & 2 deletions packages/authentication/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import {AuthenticateFn, UserProfile} from './types';
import {AuthenticationMetadata} from './decorators';
import {BindingKey} from '@loopback/context';
import {MetadataAccessor} from '@loopback/metadata';
import {AuthenticationServices} from './services/authentication.service';

/**
* Binding keys used by this component.
*/
export namespace AuthenticationBindings {
export const SERVICES = BindingKey.create<AuthenticationServices>(
'authentication.services',
);
/**
* Key used to bind an authentication strategy to the context for the
* authentication function to use.
Expand All @@ -23,8 +27,8 @@ export namespace AuthenticationBindings {
* .toProvider(MyPassportStrategyProvider);
* ```
*/
export const STRATEGY = BindingKey.create<Strategy | undefined>(
'authentication.strategy',
export const STRATEGY_RESOLVER = BindingKey.create<Strategy | undefined>(
'authentication.strategy.resolver',
);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import {Getter, Provider, Setter, inject} from '@loopback/context';
import {Request} from '@loopback/rest';
import {Strategy} from 'passport';
import {AuthenticationBindings} from '../keys';
import {StrategyAdapter} from '../strategy-adapter';
import {AuthenticateFn, UserProfile} from '../types';
import {AuthenticationBindings} from '../../../keys';
import {StrategyAdapter} from '../../../strategies/passport/passport-strategy-adapter';
import {AuthenticateFn, UserProfile} from '../../../types';

/**
* @description Provider of a function which authenticates
Expand Down
2 changes: 1 addition & 1 deletion packages/authentication/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
// License text available at https://opensource.org/licenses/MIT

export * from './auth-metadata.provider';
export * from './authentication.provider';
export * from './auth-action/passport/auth-action.provider';
10 changes: 10 additions & 0 deletions packages/authentication/src/services/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {UserProfile, Credentials, AuthenticatedUser} from '../types';
import {Entity} from '@loopback/repository';
export interface AuthenticationServices {
authenticateUser<U extends Entity>(
credentials: Credentials,
): Promise<AuthenticatedUser<U>>;
comparePassword<T = string>(credentialPass: T, userPass: T): Promise<boolean>;
generateAccessToken(user: UserProfile): Promise<string>;
decodeAccessToken(token: string): Promise<UserProfile | undefined>;
}
1 change: 1 addition & 0 deletions packages/authentication/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authentication.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {inject} from '@loopback/core';
import {AuthenticationBindings} from '../keys';
import {AuthenticationServices} from '../services';
import {UserProfile, AuthenticatedUser, Credentials} from '../types';
import {Entity} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {toJSON} from '@loopback/testlab';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought @loopback/testlab is supposed to be used in tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @shazha thank you for catching it. This PR is still in progress, code changes everyday :( I will let you know when it's ready for review. Thanks for understanding.

import * as _ from 'lodash';

/**
* An interface describes the common authentication strategy.
*
* An authentication strategy is usually a class with an
* authenticate method that verifies a user's identity and
* returns the corresponding user profile.
*
* Please note this file should be moved to @loopback/authentication
*/
export abstract class AuthenticationStrategy {
constructor(
@inject(AuthenticationBindings.SERVICES)
private services: AuthenticationServices,
) {}
abstract async authenticateRequest(
request: Request,
): Promise<UserProfile | undefined>;

async authenticateUser<U extends Entity>(
credentials: Credentials,
): Promise<AuthenticatedUser<U>> {
return this.services.authenticateUser(credentials);
}

async comparePassword<T = string>(
credentialPass: T,
userPass: T,
): Promise<boolean> {
return this.services.comparePassword(credentialPass, userPass);
}

async generateAccessToken(user: UserProfile): Promise<string> {
return this.services.generateAccessToken(user);
}

async decodeAccessToken(token: string): Promise<UserProfile | undefined> {
return this.services.decodeAccessToken(token);
}

async getAccessTokenForUser(credentials: Credentials): Promise<string> {
const user = await this.authenticateUser(credentials);
// There is no guarantee that an Entity contains field `password`
const userWithPassword = Object.assign({password: ''}, user);
const passwordMatched = await this.comparePassword(
credentials.password,
userWithPassword.password,
);
if (!passwordMatched) {
throw new HttpErrors.Unauthorized('The credentials are not correct.');
}

const userProfile = _.pick(toJSON(user), ['id', 'email', 'firstName']);
const token = await this.generateAccessToken(userProfile);
return token;
}
}
2 changes: 2 additions & 0 deletions packages/authentication/src/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './authentication-strategy';
export * from './passport/passport-strategy-adapter';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {HttpErrors, Request} from '@loopback/rest';
import {Strategy} from 'passport';
import {UserProfile} from './types';
import {UserProfile} from '../../types';

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

Expand Down
11 changes: 11 additions & 0 deletions packages/authentication/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {Request} from '@loopback/rest';
import {Entity} from '@loopback/repository';

/**
* interface definition of a function which accepts a request
Expand All @@ -22,3 +23,13 @@ export interface UserProfile {
name?: string;
email?: string;
}

export type Credentials = {
email: string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the credentials include telephone number or username?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the credentials include telephone number or username?

#2246 - to be addressed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dreamdevil00 Good point. And #2246 is supposed to provide a solution for customizing the fields. (Thanks @dougal83 )

password: string;
};

export type AuthenticatedUser<U extends Entity> = {
authenticated: boolean;
userInfo?: U;
};