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

[new module api] New module public API #318

Merged
merged 5 commits into from
May 30, 2020
Merged
Show file tree
Hide file tree
Changes from all 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 .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