-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Draft/passport strategy #2822
Draft/passport strategy #2822
Conversation
Resolve authentication strategies registered via extension point BREAKING CHANGE: the new interface and authentication action in 2.0 will require users to adjust existing code
// instantiate the passport strategy, we cannot add the imported `BasicStrategy` | ||
// class as extension directly, we need to wrap it as a strategy provider, | ||
// then add the provider class as the extension. | ||
// See Line 89 in the function `givenAServer` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are not able to register a Passport strategy class directly and have to create a custom provider class, I would like to propose a slightly different user experience that does not require any new extension points to be created.
Usage:
import {Strategy, PassportStrategyAdapter} from '@loopback/authentication';
class PassportBasicAuthProvider implements Provider<Strategy> {
value(): Strategy {
// create an instance of the Passport Strategy
// configure it with "verify" and any other options/behavior as needed
const passportStrategy = new BasicStrategy(this.verify.bind(this));
// wrap the Passport Strategy class instance and
// adapt it to match LB Strategy interface contract
return new PassportStrategyAdapter(passportStrategy);
}
verify(username: string, password: string, cb: Function) {
process.nextTick(() => {
users.find(username, password, cb);
});
}
}
// register PassportBasicAuthProvider as an extension
// for AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME
// (a regular LB4 Authentication strategy)
Implementation wise, I think we just need to export existing StrategyAdapter
class for public consumption, preferably under a more descriptive name. In my example, I am using the name PassportStrategyAdapter
. In the future, we can implement additional adapters, e.g. Auth0StrategyAdapter
(see https://auth0.com, I am totally making this example up.)
An important feature of this approach is extensibility. When each strategy contract (LB4, Passport, etc.) requires a new extension point, then it's difficult to implement support for new strategy contracts in 3rd-party modules - each new contract requires changes in authentication core to recognize the new extension point. With the proposed Adapter approach, the core authentication framework does not need to deal with different strategy contracts and thus adding a new strategy contract does not require any changes in the core.
In fact, I think we should move PassportStrategyAdapter
into a standalone package:
- It will validate extensibility of our design and enforce proper separation of concerns.
- People using only LB strategies should not have to install
passport
dependency. By moving the passport adapter to a new package, only consumers of this new package will depend onpassport
. - As we learn more about Passport, we may need to introduce breaking change in the passport adapter. Ideally, these breaking changes should affect only Passport users, they should not trigger a semver-major release of the entire authentication framework.
/cc @raymondfeng
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are not able to register a Passport strategy class directly and have to create a custom provider class, I would like to propose a slightly different user experience that does not require any new extension points to be created.
Neat 👍 yeah since the adapter implements AuthenticationStrategy
it makes sense to only have one extension. And this could also be a good practice for other adapters like you said in
we can implement additional adapters, e.g. Auth0StrategyAdapter (see https://auth0.com, I am totally making this example up.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the proposed Adapter approach, the core authentication framework does not need to deal with different strategy contracts and thus adding a new strategy contract does not require any changes in the core.
+1 Fair enough.
7edb7ee
to
146d9ca
Compare
There are two possible options to adapt Passport strategies:
|
881fe37
to
dbc51ce
Compare
@raymondfeng Thanks for the suggestion
I believe this is the approach that this PR is implementing.
👍 I am also interested to explorer it, while it looks more like documenting the best practice of applying an express middleware |
Tried wrapping Here is a prototype of the passport adapter provider:
export class PassportAdapterProvider implements Provider<AuthenticateFn> {
constructor(
// `Passport` is a function and cannot be used as a type here
// need to build a type to describe the instance of Passport
@inject(AuthenticationBindings.PASSPORT)
readonly passport: Passport,
@inject(AuthenticationBindings.METADATA)
private metadata?: AuthenticationMetadata,
) { }
/**
* @returns authenticateFn
*/
value(): AuthenticateFn {
return request => this.action(request);
}
/**
* The implementation of authenticate() sequence action.
* @param request The incoming request provided by the REST layer
*/
async action(request: Request): Promise<UserProfile | undefined> {
// we can infer the strategy name from `metadata.name` as an alternate.
const strategyName = metadata.options.strategyName;
const asyncAuthenticate = promisify(passport.authenticate);
const user = await asyncAuthenticate(strategyName);
// (Blocker) How to authenticate a request using `passport.authenticate` in the action?
// We need a function to convert the `user` to `userProfile`
const userProfile = user;
return userProfile;
}
} Since the existing adapter wraps the passport strategy instead of I am afraid I need more time to flush out the uncertainties of wrapping
in story #2467. Then explorer wrapping |
// See Line 89 in the function `givenAServer` | ||
class PassportBasicAuthProvider implements Provider<AuthenticationStrategy> { | ||
value(): AuthenticationStrategy { | ||
const basicStrategy = this.configuratedBasicStrategy(verify); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jannyHou , use configured
instead of configurated
.
} | ||
|
||
convertToAuthStrategy(basic: BasicStrategy): AuthenticationStrategy { | ||
return new StrategyAdapter(basic, 'basic'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jannyHou , let's store the 'basic' identifier string for the authentication strategy in a constant somewhere else.
@@ -16,33 +16,37 @@ describe('Strategy Adapter', () => { | |||
it('calls the authenticate method of the strategy', async () => { | |||
let calledFlag = false; | |||
// TODO: (as suggested by @bajtos) use sinon spy | |||
class Strategy extends MockStrategy { | |||
class Strategy extends MockPassportStrategy { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jannyHou , Please use a custom strategy class other than Strategy
so it is not confused by the Strategy
class used by passport-strategy. thx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jannyHou ^^^ Please see my comments. thx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@emonddr Thanks good catch. This code change is merged in PR #2859. I can address it in the coming doc PR that moves https://github.com/strongloop/loopback-next/blob/master/packages/authentication/docs/authentication-system.md to the root level README.md file.
@jannyHou , it is possible for you to rebase off of loopback-next master? my changes from my feature branch (from which you based your feature branch) are in master branch now. For basic-auth-acceptance.ts, please use this sequence : https://github.com/strongloop/loopback-next/tree/master/packages/authentication/src/__tests__/fixtures/sequences . Also please use : https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/__tests__/fixtures/users/user.repository.ts and https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/__tests__/fixtures/users/user.ts. |
In basic-auth-acceptance.ts, it('throws an error if the injected passport strategy is not valid', async () => {
const context: Context = new Context();
context
.bind(AuthenticationBindings.STRATEGY)
.to({} as AuthenticationStrategy);
context
.bind(AuthenticationBindings.AUTH_ACTION)
.toProvider(AuthenticateActionProvider);
const authenticate = await context.get<AuthenticateFn>(
AuthenticationBindings.AUTH_ACTION,
);
const request = <Request>{};
let error;
try {
await authenticate(request);
} catch (exception) {
error = exception;
}
expect(error).to.have.property('message', 'strategy.authenticate is not a function');
}); and use the error message listed. To catch https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/providers/auth-strategy.provider.ts#L45, perhaps add a test like: https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/__tests__/acceptance/basic-auth-extension.acceptance.ts#L145 and https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.acceptance.ts#L390. |
@emonddr Thank you for the comments. This draft PR is a PoC of the new strategy adapter, so some implementation details are not refined yet and will be rewrote/improved in the official feature PR. The plan of moving forward this PR is:
I removed the test file |
I am closing the draft to keep open PRs less. |
The 2nd commit is the real change
connect to #2467
and also #2311
A draft PR based on #2763.
Discussion see 53cfeeb#r33364760 and 53cfeeb#r33347455
This PR adds another extension point for passport based strategies:
Passport strategy provider as an extension point: link
In the strategy provider, it searches for a passport strategy if no result in the non-passport ones: link
The passport strategy is registered as a provider, reason and details please check the acceptance test
One skipped test to be recovered (need sometime to check the error message, not a big deal), more tests to be added for the new extension point.
Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈