Skip to content

Use native promises and async/await, drop bluebird and promisify-any #190

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

Merged
merged 7 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .mocharc.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
recursive: true
reporter: "spec"
retries: 1
retries: 0
slow: 20
timeout: 2000
ui: "bdd"
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## Changelog

## 5.0.0

- removed `bluebird` and `promisify-any`
- uses native Promises and `async/await` everywhere
- drop support for Node 14 (EOL), setting Node 16 as `engine` in `package.json`
- this is a breaking change, because **it removes callback support** for
`OAuthServer` and your model implementation.

## 4.2.0
### Fixed
- fix(core): Bearer regular expression matching in authenticate handler #105
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ Most users should refer to our [Express (active)](https://github.com/node-oauth/

More examples can be found here: https://github.com/14gasher/oauth-example

## Version 5 notes

Beginning with version `5.x` we removed dual support for callbacks and promises.
With this version there is only support for Promises / async/await.

With this version we also bumped the `engine` to Node 16 as 14 is now deprecated.

## Migrating from OAuthJs and 3.x

Version 4.x should not be hard-breaking, however, there were many improvements and fixes that may
Expand Down
11 changes: 6 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
Use this section to tell people about which versions of your project are
currently being supported with security updates.

| Version | Supported |
| ------- | ------------------ |
| 4.x.x | :white_check_mark: |
| 3.x.x | :white_check_mark: but only very critical security issues |
| < 3 | :x: |
| Version | Supported |
|---------|--------------------------------------------------|
| 5.x.x | :white_check_mark: |
| 4.x.x | :white_check_mark: but only high severity issues |
| 3.x.x | :x: |
| < 3 | :x: |

## Reporting a Vulnerability

Expand Down
35 changes: 14 additions & 21 deletions lib/grant-types/abstract-grant-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

const InvalidArgumentError = require('../errors/invalid-argument-error');
const InvalidScopeError = require('../errors/invalid-scope-error');
const Promise = require('bluebird');
const promisify = require('promisify-any').use(Promise);
const isFormat = require('@node-oauth/formats');
const tokenUtil = require('../utils/token-util');

Expand Down Expand Up @@ -36,12 +34,10 @@ function AbstractGrantType(options) {
* Generate access token.
*/

AbstractGrantType.prototype.generateAccessToken = function(client, user, scope) {
AbstractGrantType.prototype.generateAccessToken = async function(client, user, scope) {
if (this.model.generateAccessToken) {
return promisify(this.model.generateAccessToken, 3).call(this.model, client, user, scope)
.then(function(accessToken) {
return accessToken || tokenUtil.generateRandomToken();
});
const accessToken = await this.model.generateAccessToken(client, user, scope);
return accessToken || tokenUtil.generateRandomToken();
}

return tokenUtil.generateRandomToken();
Expand All @@ -51,12 +47,10 @@ AbstractGrantType.prototype.generateAccessToken = function(client, user, scope)
* Generate refresh token.
*/

AbstractGrantType.prototype.generateRefreshToken = function(client, user, scope) {
AbstractGrantType.prototype.generateRefreshToken = async function(client, user, scope) {
if (this.model.generateRefreshToken) {
return promisify(this.model.generateRefreshToken, 3).call(this.model, client, user, scope)
.then(function(refreshToken) {
return refreshToken || tokenUtil.generateRandomToken();
});
const refreshToken = await this.model.generateRefreshToken(client, user, scope);
return refreshToken || tokenUtil.generateRandomToken();
}

return tokenUtil.generateRandomToken();
Expand Down Expand Up @@ -93,16 +87,15 @@ AbstractGrantType.prototype.getScope = function(request) {
/**
* Validate requested scope.
*/
AbstractGrantType.prototype.validateScope = function(user, client, scope) {
AbstractGrantType.prototype.validateScope = async function(user, client, scope) {
if (this.model.validateScope) {
return promisify(this.model.validateScope, 3).call(this.model, user, client, scope)
.then(function (scope) {
if (!scope) {
throw new InvalidScopeError('Invalid scope: Requested scope is invalid');
}

return scope;
});
const validatedScope = await this.model.validateScope(user, client, scope);

if (!validatedScope) {
throw new InvalidScopeError('Invalid scope: Requested scope is invalid');
}

return validatedScope;
} else {
return scope;
}
Expand Down
203 changes: 92 additions & 111 deletions lib/grant-types/authorization-code-grant-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const AbstractGrantType = require('./abstract-grant-type');
const InvalidArgumentError = require('../errors/invalid-argument-error');
const InvalidGrantError = require('../errors/invalid-grant-error');
const InvalidRequestError = require('../errors/invalid-request-error');
const Promise = require('bluebird');
const promisify = require('promisify-any').use(Promise);
const ServerError = require('../errors/server-error');
const isFormat = require('@node-oauth/formats');
const pkce = require('../pkce/pkce');
Expand Down Expand Up @@ -45,7 +43,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType {
* @see https://tools.ietf.org/html/rfc6749#section-4.1.3
*/

handle(request, client) {
async handle(request, client) {
if (!request) {
throw new InvalidArgumentError('Missing parameter: `request`');
}
Expand All @@ -54,96 +52,87 @@ class AuthorizationCodeGrantType extends AbstractGrantType {
throw new InvalidArgumentError('Missing parameter: `client`');
}

return Promise.bind(this)
.then(function () {
return this.getAuthorizationCode(request, client);
})
.tap(function (code) {
return this.validateRedirectUri(request, code);
})
.tap(function (code) {
return this.revokeAuthorizationCode(code);
})
.then(function (code) {
return this.saveToken(code.user, client, code.authorizationCode, code.scope);
});
const code = await this.getAuthorizationCode(request, client);
await this.validateRedirectUri(request, code);
await this.revokeAuthorizationCode(code);

return this.saveToken(code.user, client, code.authorizationCode, code.scope);
}

/**
* Get the authorization code.
*/

getAuthorizationCode(request, client) {
async getAuthorizationCode(request, client) {
if (!request.body.code) {
throw new InvalidRequestError('Missing parameter: `code`');
}

if (!isFormat.vschar(request.body.code)) {
throw new InvalidRequestError('Invalid parameter: `code`');
}
return promisify(this.model.getAuthorizationCode, 1)
.call(this.model, request.body.code)
.then((code) => {
if (!code) {
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
}

if (!code.client) {
throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object');
}

if (!code.user) {
throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object');
}

if (code.client.id !== client.id) {
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
}

if (!(code.expiresAt instanceof Date)) {
throw new ServerError('Server error: `expiresAt` must be a Date instance');
}

if (code.expiresAt < new Date()) {
throw new InvalidGrantError('Invalid grant: authorization code has expired');
}

if (code.redirectUri && !isFormat.uri(code.redirectUri)) {
throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI');
}

// optional: PKCE code challenge

if (code.codeChallenge) {
if (!request.body.code_verifier) {
throw new InvalidGrantError('Missing parameter: `code_verifier`');
}

const hash = pkce.getHashForCodeChallenge({
method: code.codeChallengeMethod,
verifier: request.body.code_verifier
});

if (!hash) {
// notice that we assume that codeChallengeMethod is already
// checked at an earlier stage when being read from
// request.body.code_challenge_method
throw new ServerError('Server error: `getAuthorizationCode()` did not return a valid `codeChallengeMethod` property');
}

if (code.codeChallenge !== hash) {
throw new InvalidGrantError('Invalid grant: code verifier is invalid');
}
}
else {
if (request.body.code_verifier) {
// No code challenge but code_verifier was passed in.
throw new InvalidGrantError('Invalid grant: code verifier is invalid');
}
}

return code;

const code = await this.model.getAuthorizationCode(request.body.code);

if (!code) {
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
}

if (!code.client) {
throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object');
}

if (!code.user) {
throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object');
}

if (code.client.id !== client.id) {
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
}

if (!(code.expiresAt instanceof Date)) {
throw new ServerError('Server error: `expiresAt` must be a Date instance');
}

if (code.expiresAt < new Date()) {
throw new InvalidGrantError('Invalid grant: authorization code has expired');
}

if (code.redirectUri && !isFormat.uri(code.redirectUri)) {
throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI');
}

// optional: PKCE code challenge

if (code.codeChallenge) {
if (!request.body.code_verifier) {
throw new InvalidGrantError('Missing parameter: `code_verifier`');
}

const hash = pkce.getHashForCodeChallenge({
method: code.codeChallengeMethod,
verifier: request.body.code_verifier
});

if (!hash) {
// notice that we assume that codeChallengeMethod is already
// checked at an earlier stage when being read from
// request.body.code_challenge_method
throw new ServerError('Server error: `getAuthorizationCode()` did not return a valid `codeChallengeMethod` property');
}

if (code.codeChallenge !== hash) {
throw new InvalidGrantError('Invalid grant: code verifier is invalid');
}
}
else {
if (request.body.code_verifier) {
// No code challenge but code_verifier was passed in.
throw new InvalidGrantError('Invalid grant: code verifier is invalid');
}
}

return code;
}

/**
Expand Down Expand Up @@ -183,46 +172,38 @@ class AuthorizationCodeGrantType extends AbstractGrantType {
* @see https://tools.ietf.org/html/rfc6749#section-4.1.2
*/

revokeAuthorizationCode(code) {
return promisify(this.model.revokeAuthorizationCode, 1)
.call(this.model, code)
.then((status) => {
if (!status) {
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
}
async revokeAuthorizationCode(code) {
const status = await this.model.revokeAuthorizationCode(code);

return code;
});
if (!status) {
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
}

return code;
}


/**
* Save token.
*/

saveToken(user, client, authorizationCode, scope) {
const fns = [
this.validateScope(user, client, scope),
this.generateAccessToken(client, user, scope),
this.generateRefreshToken(client, user, scope),
this.getAccessTokenExpiresAt(),
this.getRefreshTokenExpiresAt(),
];

return Promise.all(fns)
.bind(this)
.spread(function (scope, accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) {
const token = {
accessToken: accessToken,
authorizationCode: authorizationCode,
accessTokenExpiresAt: accessTokenExpiresAt,
refreshToken: refreshToken,
refreshTokenExpiresAt: refreshTokenExpiresAt,
scope: scope,
};

return promisify(this.model.saveToken, 3).call(this.model, token, client, user);
});
async saveToken(user, client, authorizationCode, scope) {
const validatedScope = await this.validateScope(user, client, scope);
const accessToken = await this.generateAccessToken(client, user, scope);
const refreshToken = await this.generateRefreshToken(client, user, scope);
const accessTokenExpiresAt = await this.getAccessTokenExpiresAt();
const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt();

const token = {
accessToken: accessToken,
authorizationCode: authorizationCode,
accessTokenExpiresAt: accessTokenExpiresAt,
refreshToken: refreshToken,
refreshTokenExpiresAt: refreshTokenExpiresAt,
scope: validatedScope,
};

return this.model.saveToken(token, client, user);
}
}

Expand Down
Loading