Skip to content

Commit

Permalink
authentication: make it a proper component
Browse files Browse the repository at this point in the history
Export `AuthenticationComponent` that registers providers for
`authenticate` action and current-route metadata.

Simplify usage of `authenticate` action by shifting the complexity
of deferring metadata resolution into the action itself (using the
new `@inject.getter` decorator), so that custom Sequence implementations
can inject this action the same way as other actions are injected.

Improve README - fix markdown formatting, reword introductory texts,
improve the usage example.

Clean up the code in general - fix spelling mistakes, organize
files into `decorators` and `providers` folders.
  • Loading branch information
bajtos committed Aug 4, 2017
1 parent 8b56d16 commit ac3fd77
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 105 deletions.
152 changes: 137 additions & 15 deletions packages/authentication/README.md
Expand Up @@ -2,45 +2,167 @@

A LoopBack component for authentication support.

# Overview
It demonstrates how to use LoopBack's user models and passport to interact with other authentication providers.
User can login using a passport.js strategy, which could include a third party provider.
**This is a reference implementation showing how to implement an authentication component, it is not production ready.*

## Overview

# Installation
The component demonstrates how to leverage Passport module and extension points
provided by LoopBack Next to implement authentication layer.

## Installation

```shell
npm install --save @loopback/authentication
```

# Basic use
## Basic use

Start by decorating your controller methods with `@authenticate` to require
the request to be authenticated.

```ts
// controllers/my-controller.ts
import {UserProfile, authenticate} from '@loopback/authentication';

class MyController {
constructor(@inject('authentication.user') private user: UserProfile) {}

@authenticate('BasicStrategy')
whoAmI() {
return this.user.id;
}
}
```

Next, implement a Strategy provider to map strategy names specified
in `@authenticate` decorators into Passport Strategy instances.

```ts
// providers/auth-strategy.ts
import {
inject,
Provider,
ValueOrPromise,
} from '@loopback/context';
import {
BindingKeys,
AuthenticationMetadata,
} from '@loopback/authentication';

import {Strategy} from 'passport';
import {BasicStrategy} from 'passport-http';

export class MyAuthStrategyProvider implements Provider<Strategy> {
constructor(
@inject(BindingKeys.Authentication.METADATA)
private metadata: AuthenticationMetadata,
) {}

value() : ValueOrPromise<Strategy> {
const name = this.metadata.strategy;
if (name === 'BasicStrategy') {
return new BasicStrategy(this.verify);
} else {
return Promise.reject(`The strategy ${name} is not available.`);
}
}

verify(username: string, password: string, cb: Function) {
// find user by name & password
// call cb(null, false) when user not found
// call cb(null, userProfile) when user is authenticated
}
}
```

```ts
const strategy = new BasicStrategy(async (username, password) => {
return await findUser(username, password);
};
getAuthenticatedUser(strategy, ParsedRequest);
In order to perform authentication, we need to implement a custom Sequence
invoking the authentication at the right time during the request handling.

```ts
// sequence.ts
import {
FindRoute,
inject,
InvokeMethod,
ParsedRequest,
Reject
Send,
ServerResponse,
SequenceHandler,
} from '@loopback/core';

import {
AuthenticateFn,
} from '@loopback/authentication';

class MySequence implements SequenceHandler {
constructor(
@inject('sequence.actions.findRoute') protected findRoute: FindRoute,
@inject('sequence.actions.invokeMethod') protected invoke: InvokeMethod,
@inject('sequence.actions.send') protected send: Send,
@inject('sequence.actions.reject') protected reject: Reject,
@inject('authentication.actions.authenticate')
protected authenticateRequest: AuthenticateFn,
) {}

async handle(req: ParsedRequest, res: ServerResponse) {
try {
const route = this.findRoute(req);

// This is the important line added to the default sequence implementation
const user = await this.authenticateRequest(req);

const args = await parseOperationArgs(req, route);
const result = await this.invoke(route, args);
this.send(res, result);
} catch (err) {
this.reject(res, req, err);
}
}
}
```

Finally, put it all together in your application object:

```ts
import {Application} from '@loopback/core';
import {AuthenticationComponent, BindingKeys} from '@loopback/authentication';
import {MyAuthStrategyProvider} from './providers/auth-strategy';
import {MyController} from './controllers/my-controller';
import {MySequence} from './my-sequence';

class MyApp extends Application {
constructor() {
super({
components: [AuthenticationComponent],
});

this.bind(BindingKeys.Authentication.STRATEGY)
.toProvider(MyPassportStrategyProvider);
this.sequence(MySequence);

this.controller(MyController);
}
}
```

# Related resources
## Related resources

For more info about passport, see [passport.js](http://passportjs.org/).

# Contributions
## Contributions

- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing#guidelines)
- [Join the team](https://github.com/strongloop/loopback-next/issues/110)

# Tests
## Tests

run `npm test` from the root folder.

# Contributors
## Contributors

See [all contributors](https://github.com/strongloop/loopback-next/graphs/contributors).

# License
## License

MIT
1 change: 1 addition & 0 deletions packages/authentication/package.json
Expand Up @@ -23,6 +23,7 @@
"@loopback/context": "^4.0.0-alpha.9",
"@loopback/core": "^4.0.0-alpha.9",
"@types/passport": "^0.3.3",
"@types/passport-http": "^0.3.2",
"passport": "^0.3.2",
"passport-strategy": "^1.0.0"
},
Expand Down
22 changes: 22 additions & 0 deletions packages/authentication/src/auth-component.ts
@@ -0,0 +1,22 @@
// Copyright IBM Corp. 2013,2017. All Rights Reserved.
// Node module: @loopback/authentication
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {BindingKeys} from './keys';
import {Constructor} from '@loopback/context';
import {Component, ProviderMap} from '@loopback/core';
import {AuthenticationProvider} from './providers/authenticate';
import {AuthMetadataProvider} from './providers/auth-metadata';

export class AuthenticationComponent implements Component {
providers?: ProviderMap;

// TODO(bajtos) inject configuration
constructor() {
this.providers = {
[BindingKeys.Authentication.AUTH_ACTION]: AuthenticationProvider,
[BindingKeys.Authentication.METADATA]: AuthMetadataProvider,
};
}
}
Expand Up @@ -4,7 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {Reflector, Constructor} from '@loopback/context';
import {BindingKeys} from './keys';
import {BindingKeys} from '../keys';

/**
* Authentication metadata stored via Reflection API
Expand Down
11 changes: 7 additions & 4 deletions packages/authentication/src/index.ts
Expand Up @@ -3,8 +3,11 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './decorator';
export * from './strategy-adapter';
export * from './metadata-provider';
export * from './provider';
export * from './auth-component';
export * from './decorators/authenticate';
export * from './keys';
export * from './strategy-adapter';

// internals for tests
export * from './providers/auth-metadata';
export * from './providers/authenticate';
4 changes: 2 additions & 2 deletions packages/authentication/src/keys.ts
Expand Up @@ -9,7 +9,7 @@
export namespace BindingKeys {
export namespace Authentication {
export const STRATEGY = 'authentication.strategy';
export const PROVIDER = 'authentication.provider';
export const METADATA = 'authenticate';
export const AUTH_ACTION = 'authentication.actions.authenticate';
export const METADATA = 'authentication.operation-metadata';
}
}
Expand Up @@ -5,7 +5,10 @@

import {BindingKeys} from '@loopback/core';
import {Constructor, Provider, inject} from '@loopback/context';
import {AuthenticationMetadata, getAuthenticateMetadata} from './decorator';
import {
AuthenticationMetadata,
getAuthenticateMetadata,
} from '../decorators/authenticate';

/**
* @description Provides authentication metadata of a controller method
Expand Down
Expand Up @@ -7,8 +7,8 @@ import * as http from 'http';
import {HttpErrors, inject, ParsedRequest} from '@loopback/core';
import {Provider} from '@loopback/context';
import {Strategy} from 'passport';
import {StrategyAdapter} from './strategy-adapter';
import {BindingKeys} from './keys';
import {StrategyAdapter} from '../strategy-adapter';
import {BindingKeys} from '../keys';

/**
* interface definition of a function which accepts a request
Expand All @@ -35,15 +35,25 @@ export interface UserProfile {
*/
export class AuthenticationProvider implements Provider<AuthenticateFn> {
constructor(
@inject(BindingKeys.Authentication.STRATEGY) readonly strategy: Strategy,
// The provider is instantiated for Sequence constructor,
// at which time we don't have information about the current
// route yet. This information is needed to determine
// what auth strategy should be used.
// To solve this, we are injecting a getter function that will
// defer resolution of the strategy until authenticate() action
// is executed.
@inject.getter(BindingKeys.Authentication.STRATEGY)
readonly getStrategy: () => Promise<Strategy>,
) {}

/**
* @returns authenticateFn
*/
value(): AuthenticateFn {
return async (request: ParsedRequest) =>
await getAuthenticatedUser(this.strategy, request);
return async (request: ParsedRequest) => {
const strategy = await this.getStrategy();
return await getAuthenticatedUser(strategy, request);
};
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/authentication/src/strategy-adapter.ts
Expand Up @@ -5,7 +5,7 @@
import * as http from 'http';
import {HttpErrors, ParsedRequest} from '@loopback/core';
import {Strategy} from 'passport';
import {UserProfile} from './provider';
import {UserProfile} from './providers/authenticate';

/**
* Shimmed Request to satisfy express requirements of passport strategies.
Expand Down

0 comments on commit ac3fd77

Please sign in to comment.