Skip to content

Commit

Permalink
Merge pull request #318 from lelylan/feature/new-module-api
Browse files Browse the repository at this point in the history
[new module api] New module public API
  • Loading branch information
jonathansamines authored May 30, 2020
2 parents 183142f + e66b571 commit 87a1d3a
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 325 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"ignoreComments": true
}],
"strict": ["error", "safe"],
"max-classes-per-file": ["off"],
"lines-between-class-members": ["off"]
}
}
26 changes: 10 additions & 16 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Node.js client library for [OAuth2](http://oauth.net/2/). OAuth2 allows users to grant access to restricted resources by third party applications, giving them the possibility to enable and disable those accesses whenever they want.

## .create(options) => Module
## Options

Simple OAuth2 accepts an object with the following params.
Simple OAuth2 grant classes accept an object with the following params.

* `client` - required object with the following properties:
* `id` - Service registered client id. When required by the [spec](https://tools.ietf.org/html/rfc6749#appendix-B) this value will be automatically encoded. Required
Expand Down Expand Up @@ -35,8 +35,8 @@ Simple OAuth2 accepts an object with the following params.
### URL resolution
URL paths are relatively resolved to their corresponding host property using the [Node WHATWG URL](https://nodejs.org/dist/latest-v12.x/docs/api/url.html#url_constructor_new_url_input_base) resolution algorithm.

## Module
### .authorizationCode
## Grants
### new AuthorizationCode(options)
This submodule provides supports for the OAuth2 [Authorization Code Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1) to support applications asking for user's resources without handling the user credentials.

#### .authorizeURL([authorizeOptions]) => String
Expand All @@ -48,7 +48,7 @@ Creates the authorization URL from the *client configuration* and the *authorize

Additional options will be automatically serialized as query params in the resulting URL.

#### .getToken(params, [httpOptions]) => Promise<token>
#### .getToken(params, [httpOptions]) => Promise<AccessToken>
Get a new access token using the current grant type.

* `params`
Expand All @@ -60,10 +60,10 @@ Additional options will be automatically serialized as params for the token requ

* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options.

### .ownerPassword
### new PasswordOwner(options)
This submodule provides support for the OAuth2 [Password Owner](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.3) to support applications handling the user credentials.

#### .getToken(params, [httpOptions]) => Promise<token>
#### .getToken(params, [httpOptions]) => Promise<AccessToken>
Get a new access token using the current grant type.

* `params`
Expand All @@ -75,10 +75,10 @@ Additional options will be automatically serialized as params for the token requ

* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options.

### .clientCredentials
### new ClientCredentials(options)
This submodule provides support for the OAuth2 [Client Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4) to support clients that can request access tokens using only its client credentials.

#### .getToken(params, [httpOptions]) => Promise<token>
#### .getToken(params, [httpOptions]) => Promise<AccessToken>
Get a new access token using the current grant type.

* `params`
Expand All @@ -88,19 +88,13 @@ Additional options will be automatically serialized as params for the token requ

* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options.

### .accessToken
This submodule allows for the token level operations.

#### .create(token) => AccessToken
An access token (plain object) can be used to create a new token object with the following methods

### AccessToken
#### .expired([expirationWindowSeconds]) => Boolean
Determines if the current access token is definitely expired or not

* `expirationWindowSeconds` Window of time before the actual expiration to refresh the token. Defaults to **0**.

#### .refresh(params) => Promise<ResponsePayload>
#### .refresh(params) => Promise<AccessToken>
Refreshes the current access token. The following params are allowed:

* `params`
Expand Down
33 changes: 15 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Node.js client library for [OAuth2](http://oauth.net/2/). OAuth2 allows users to
- [Authorization Code](#authorization-code)
- [Password Credentials Flow](#password-credentials-flow)
- [Client Credentials Flow](#client-credentials-flow)
- [Access Token object](#access-token-object)
- [Access Token](#access-token)
- [Errors](#errors)
- [Debugging the module](#debugging-the-module)
- [API](#api)
Expand Down Expand Up @@ -50,7 +50,7 @@ npm install --save simple-oauth2
Create a new instance by specifying the minimal configuration

```javascript
const credentials = {
const config = {
client: {
id: '<client-id>',
secret: '<client-secret>'
Expand All @@ -60,7 +60,7 @@ const credentials = {
}
};

const oauth2 = require('simple-oauth2').create(credentials);
const { ClientCredentials, PasswordOwner, AuthorizationCode } = require('simple-oauth2');
```
For more detailed configuration information see [API Documentation](./API.md)

Expand All @@ -74,9 +74,9 @@ The [Authorization Code](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#secti

```javascript
async function run() {
const oauth2 = require('simple-oauth2').create(credentials);
const authorizationCode = new AuthorizationCode(config);

const authorizationUri = oauth2.authorizationCode.authorizeURL({
const authorizationUri = authorizationCode.authorizeURL({
redirect_uri: 'http://localhost:3000/callback',
scope: '<scope>',
state: '<state>'
Expand All @@ -92,8 +92,7 @@ async function run() {
};

try {
const result = await oauth2.authorizationCode.getToken(tokenConfig);
const accessToken = oauth2.accessToken.create(result);
const accessToken = await authorizationCode.getToken(tokenConfig);
} catch (error) {
console.log('Access Token Error', error.message);
}
Expand All @@ -108,7 +107,7 @@ The [Password Owner](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4

```javascript
async function run() {
const oauth2 = require('simple-oauth2').create(credentials);
const passwordOwner = new PasswordOwner(config);

const tokenConfig = {
username: 'username',
Expand All @@ -117,8 +116,7 @@ async function run() {
};

try {
const result = await oauth2.ownerPassword.getToken(tokenConfig);
const accessToken = oauth2.accessToken.create(result);
const accessToken = await passwordOwner.getToken(tokenConfig);
} catch (error) {
console.log('Access Token Error', error.message);
}
Expand All @@ -133,15 +131,14 @@ The [Client Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#secti

```javascript
async function run() {
const oauth2 = require('simple-oauth2').create(credentials);
const clientCredentials = new ClientCredentials(config);

const tokenConfig = {
scope: '<scope>',
};

try {
const result = await oauth2.clientCredentials.getToken(tokenConfig);
const accessToken = oauth2.accessToken.create(result);
const accessToken = await clientCredentials.getToken(tokenConfig);
} catch (error) {
console.log('Access Token error', error.message);
}
Expand All @@ -150,7 +147,7 @@ async function run() {
run();
```

### Access Token object
### Access Token

When a token expires we need to refresh it. Simple OAuth2 offers the AccessToken class that add a couple of useful methods to refresh the access token when it is expired.

Expand All @@ -162,8 +159,6 @@ async function run() {
'expires_in': '7200'
};

let accessToken = oauth2.accessToken.create(tokenObject);

if (accessToken.expired()) {
try {
const params = {
Expand All @@ -188,7 +183,7 @@ These come down to factors such as network and processing latency and can be wor
async function run() {
const EXPIRATION_WINDOW_IN_SECONDS = 300; // Window of time before the actual expiration to refresh the token

if (token.expired(EXPIRATION_WINDOW_IN_SECONDS)) {
if (accessToken.expired(EXPIRATION_WINDOW_IN_SECONDS)) {
try {
accessToken = await accessToken.refresh();
} catch (error) {
Expand Down Expand Up @@ -241,8 +236,10 @@ As a standard [boom](https://github.com/hapijs/boom) error you can access any of

```javascript
async function run() {
const clientCredentials = new ClientCredentials(config);

try {
await oauth2.authorizationCode.getToken();
await clientCredentials.getToken();
} catch(error) {
console.log(error.output);
}
Expand Down
100 changes: 34 additions & 66 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,40 @@
'use strict';

const Joi = require('@hapi/joi');
const Client = require('./lib/client');
const AuthorizationCode = require('./lib/grants/authorization-code');
const PasswordOwner = require('./lib/grants/password-owner');
const ClientCredentials = require('./lib/grants/client-credentials');
const AccessToken = require('./lib/access-token');
const { authorizationMethodEnum, bodyFormatEnum, encodingModeEnum } = require('./lib/request-options');

// https://tools.ietf.org/html/draft-ietf-oauth-v2-31#appendix-A.1
const vsCharRegEx = /^[\x20-\x7E]*$/;

const clientSchema = Joi.object().keys({
id: Joi.string().pattern(vsCharRegEx).allow(''),
secret: Joi.string().pattern(vsCharRegEx).allow(''),
secretParamName: Joi.string().default('client_secret'),
idParamName: Joi.string().default('client_id'),
}).required();

const authSchema = Joi.object().keys({
tokenHost: Joi.string().required().uri({ scheme: ['http', 'https'] }),
tokenPath: Joi.string().default('/oauth/token'),
revokePath: Joi.string().default('/oauth/revoke'),
authorizeHost: Joi.string().uri({ scheme: ['http', 'https'] }).default(Joi.ref('tokenHost')),
authorizePath: Joi.string().default('/oauth/authorize'),
}).required();

const optionsSchema = Joi.object().keys({
scopeSeparator: Joi.string().default(' '),
credentialsEncodingMode: Joi
.string()
.valid(...Object.values(encodingModeEnum))
.default(encodingModeEnum.STRICT),
bodyFormat: Joi
.string()
.valid(...Object.values(bodyFormatEnum))
.default(bodyFormatEnum.FORM),
authorizationMethod: Joi
.string()
.valid(...Object.values(authorizationMethodEnum))
.default(authorizationMethodEnum.HEADER),
}).default();

const moduleOptionsSchema = Joi.object().keys({
client: clientSchema,
auth: authSchema,
http: Joi.object().unknown(true),
options: optionsSchema,
});
const Config = require('./lib/config');
const AuthorizationCodeGrant = require('./lib/grants/authorization-code');
const PasswordOwnerGrant = require('./lib/grants/password-owner');
const ClientCredentialsGrant = require('./lib/grants/client-credentials');

class AuthorizationCode extends AuthorizationCodeGrant {
constructor(options) {
const config = Config.apply(options);
const client = new Client(config);

super(config, client);
}
}

class ClientCredentials extends ClientCredentialsGrant {
constructor(options) {
const config = Config.apply(options);
const client = new Client(config);

super(config, client);
}
}

class PasswordOwner extends PasswordOwnerGrant {
constructor(options) {
const config = Config.apply(options);
const client = new Client(config);

super(config, client);
}
}

module.exports = {

/**
* Creates a new simple-oauth2 client with the provided configuration
* @param {Object} opts Module options as defined in schema
* @returns {Object} The simple-oauth2 client
*/
create(opts = {}) {
const options = Joi.attempt(opts, moduleOptionsSchema, 'Invalid options provided to simple-oauth2');
const client = new Client(options);

return Object.freeze({
accessToken: {
create: AccessToken.factory(options, client),
},
ownerPassword: new PasswordOwner(options, client),
authorizationCode: new AuthorizationCode(options, client),
clientCredentials: new ClientCredentials(options, client),
});
},
PasswordOwner,
ClientCredentials,
AuthorizationCode,
};
4 changes: 0 additions & 4 deletions lib/access-token/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ module.exports = class AccessToken {
#config = null;
#client = null;

static factory(config, client) {
return (token) => new AccessToken(config, client, token);
}

constructor(config, client, token) {
Hoek.assert(config, 'Cannot create access token without client configuration');
Hoek.assert(client, 'Cannot create access token without client instance');
Expand Down
53 changes: 53 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

const Joi = require('@hapi/joi');
const { authorizationMethodEnum, bodyFormatEnum, encodingModeEnum } = require('./request-options');

// https://tools.ietf.org/html/draft-ietf-oauth-v2-31#appendix-A.1
const vsCharRegEx = /^[\x20-\x7E]*$/;

const clientSchema = Joi.object().keys({
id: Joi.string().pattern(vsCharRegEx).allow(''),
secret: Joi.string().pattern(vsCharRegEx).allow(''),
secretParamName: Joi.string().default('client_secret'),
idParamName: Joi.string().default('client_id'),
}).required();

const authSchema = Joi.object().keys({
tokenHost: Joi.string().required().uri({ scheme: ['http', 'https'] }),
tokenPath: Joi.string().default('/oauth/token'),
revokePath: Joi.string().default('/oauth/revoke'),
authorizeHost: Joi.string().uri({ scheme: ['http', 'https'] }).default(Joi.ref('tokenHost')),
authorizePath: Joi.string().default('/oauth/authorize'),
}).required();

const optionsSchema = Joi.object().keys({
scopeSeparator: Joi.string().default(' '),
credentialsEncodingMode: Joi
.string()
.valid(...Object.values(encodingModeEnum))
.default(encodingModeEnum.STRICT),
bodyFormat: Joi
.string()
.valid(...Object.values(bodyFormatEnum))
.default(bodyFormatEnum.FORM),
authorizationMethod: Joi
.string()
.valid(...Object.values(authorizationMethodEnum))
.default(authorizationMethodEnum.HEADER),
}).default();

const ModuleSchema = Joi.object().keys({
client: clientSchema,
auth: authSchema,
http: Joi.object().unknown(true),
options: optionsSchema,
});

const Config = {
apply(options) {
return Joi.attempt(options, ModuleSchema, 'Invalid options provided to simple-oauth2');
},
};

module.exports = Config;
Loading

0 comments on commit 87a1d3a

Please sign in to comment.