Skip to content

Commit f2f6c21

Browse files
authored
Merge branch 'master' into development
2 parents bb96022 + 4494564 commit f2f6c21

18 files changed

+744
-63
lines changed

.github/FUNDING.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# These are supported funding model platforms
2+
3+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4+
- jankapunkt
5+
patreon: # Replace with a single Patreon username
6+
open_collective: # Replace with a single Open Collective username
7+
ko_fi: # Replace with a single Ko-fi username
8+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
9+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
10+
liberapay: # Replace with a single Liberapay username
11+
issuehunt: # Replace with a single IssueHunt username
12+
otechie: # Replace with a single Otechie username
13+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
14+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
15+
- https://paypal.me/kuesterjan

.github/workflows/tests-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- uses: actions/checkout@v3
2727
- uses: actions/setup-node@v3
2828
with:
29-
node-version: 16
29+
node-version: 20
3030
# install to create local package-lock.json but don't cache the files
3131
# also: no audit for dev dependencies
3232
- run: npm i --package-lock-only && npm audit --production
@@ -57,7 +57,6 @@ jobs:
5757
key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
5858
restore-keys: |
5959
${{ runner.os }}-node-${{ matrix.node }}
60-
6160
# for this workflow we also require npm audit to pass
6261
- run: npm i
6362
- run: npm run test:coverage
@@ -110,6 +109,7 @@ jobs:
110109
# in order to test the adapter we need to use the current checkout
111110
# and install it as local dependency
112111
# we just cloned and install it as local dependency
112+
# xxx: added bluebird as explicit dependency
113113
- run: |
114114
cd github/testing/express
115115
npm i

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: setup node
2424
uses: actions/setup-node@v3
2525
with:
26-
node-version: 16
26+
node-version: 20
2727

2828
- name: cache dependencies
2929
uses: actions/cache@v3
@@ -41,7 +41,7 @@ jobs:
4141
needs: [lint]
4242
strategy:
4343
matrix:
44-
node: [14, 16, 18]
44+
node: [16, 18, 20]
4545
steps:
4646
- name: Checkout ${{ matrix.node }}
4747
uses: actions/checkout@v3

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Complete, compliant and well tested module for implementing an OAuth2 server in
66
[![Tests](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml)
77
[![CodeQL Semantic Analysis](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml)
88
[![Tests for Release](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests-release.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests-release.yml)
9+
[![Documentation Status](https://readthedocs.org/projects/node-oauthoauth2-server/badge/?version=latest)](https://node-oauthoauth2-server.readthedocs.io/en/latest/?badge=latest)
910
[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
1011
![npm Version](https://img.shields.io/npm/v/@node-oauth/oauth2-server?label=version)
1112
![npm Downloads/Week](https://img.shields.io/npm/dw/@node-oauth/oauth2-server)
@@ -19,7 +20,8 @@ NOTE: This project has been forked from [oauthjs/node-oauth2-server](https://git
1920
npm install @node-oauth/oauth2-server
2021
```
2122

22-
The *@node-oauth/oauth2-server* module is framework-agnostic but there are several officially supported wrappers available for popular HTTP server frameworks such as [Express](https://npmjs.org/package/express-oauth-server) and [Koa](https://npmjs.org/package/koa-oauth-server). If you're using one of those frameworks it is strongly recommended to use the respective wrapper module instead of rolling your own.
23+
The `@node-oauth/oauth2-server` module is framework-agnostic but there are several officially supported wrappers available for popular HTTP server frameworks such as [Express](https://www.npmjs.com/package/@node-oauth/express-oauth-server) and [Koa (not maintained by us)](https://npmjs.org/package/koa-oauth-server).
24+
If you're using one of those frameworks it is strongly recommended to use the respective wrapper module instead of rolling your own.
2325

2426

2527
## Features
@@ -28,25 +30,28 @@ The *@node-oauth/oauth2-server* module is framework-agnostic but there are sever
2830
- Can be used with *promises*, *Node-style callbacks*, *ES6 generators* and *async*/*await* (using [Babel](https://babeljs.io)).
2931
- Fully [RFC 6749](https://tools.ietf.org/html/rfc6749.html) and [RFC 6750](https://tools.ietf.org/html/rfc6750.html) compliant.
3032
- Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc.
33+
- Support for PKCE
3134
- Complete [test suite](https://github.com/node-oauth/node-oauth2-server/tree/master/test).
3235

33-
3436
## Documentation
3537

36-
[Documentation](https://oauth2-server.readthedocs.io) is hosted on Read the Docs.
37-
38+
[Documentation](https://node-oauthoauth2-server.readthedocs.io/en/latest/) is hosted on Read the Docs.
39+
Please leave an issue if something is confusing or missing in the docs.
3840

3941
## Examples
4042

41-
Most users should refer to our [Express](https://github.com/oauthjs/express-oauth-server/tree/master/examples) or [Koa](https://github.com/oauthjs/koa-oauth-server/tree/master/examples) examples.
43+
Most users should refer to our [Express (active)](https://github.com/node-oauth/express-oauth-server) or
44+
[Koa (not maintained by us)](https://github.com/oauthjs/koa-oauth-server/tree/master/examples) examples.
4245

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

45-
## Upgrading from 2.x
48+
## Migrating from OAuthJs and 3.x
4649

47-
This module has been rewritten using a promise-based approach, introducing changes to the API and model specification. v2.x is no longer supported.
50+
Version 4.x should not be hard-breaking, however, there were many improvements and fixes that may
51+
be incompatible with specific behaviour in <= 3.x
4852

49-
Please refer to our [3.0 migration guide](https://oauth2-server.readthedocs.io/en/latest/misc/migrating-v2-to-v3.html) for more information.
53+
For more info, please read the [changelog](./CHANGELOG.md) or open an issue, if you think something
54+
is unexpectedly not working.
5055

5156
## Supported NodeJs versions
5257

index.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ declare namespace OAuth2Server {
306306
*
307307
*/
308308
saveAuthorizationCode(
309-
code: Pick<AuthorizationCode, 'authorizationCode' | 'expiresAt' | 'redirectUri' | 'scope'>,
309+
code: Pick<AuthorizationCode, 'authorizationCode' | 'expiresAt' | 'redirectUri' | 'scope' | 'codeChallenge' | 'codeChallengeMethod'>,
310310
client: Client,
311311
user: User,
312312
callback?: Callback<AuthorizationCode>): Promise<AuthorizationCode | Falsey>;
@@ -322,6 +322,12 @@ declare namespace OAuth2Server {
322322
*
323323
*/
324324
validateScope?(user: User, client: Client, scope: string | string[], callback?: Callback<string | Falsey>): Promise<string | string[] | Falsey>;
325+
326+
/**
327+
* Invoked to check if the provided `redirectUri` is valid for a particular `client`.
328+
*
329+
*/
330+
validateRedirectUri?(redirect_uri: string, client: Client): Promise<boolean>;
325331
}
326332

327333
interface PasswordModel extends BaseModel, RequestAuthenticationModel {
@@ -410,6 +416,8 @@ declare namespace OAuth2Server {
410416
scope?: string | string[] | undefined;
411417
client: Client;
412418
user: User;
419+
codeChallenge?: string;
420+
codeChallengeMethod?: string;
413421
[key: string]: any;
414422
}
415423

lib/grant-types/authorization-code-grant-type.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const Promise = require('bluebird');
1212
const promisify = require('promisify-any').use(Promise);
1313
const ServerError = require('../errors/server-error');
1414
const isFormat = require('@node-oauth/formats');
15+
const util = require('util');
16+
const pkce = require('../pkce/pkce');
1517

1618
/**
1719
* Constructor.
@@ -164,6 +166,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType {
164166
});
165167
}
166168

169+
167170
/**
168171
* Save token.
169172
*/

lib/handlers/authorize-handler.js

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const UnauthorizedClientError = require('../errors/unauthorized-client-error');
2121
const isFormat = require('@node-oauth/formats');
2222
const tokenUtil = require('../utils/token-util');
2323
const url = require('url');
24+
const pkce = require('../pkce/pkce');
2425

2526
/**
2627
* Response types.
@@ -77,10 +78,6 @@ AuthorizeHandler.prototype.handle = function(request, response) {
7778
throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response');
7879
}
7980

80-
if (request.query.allowed === 'false' || request.body.allowed === 'false') {
81-
return Promise.reject(new AccessDeniedError('Access denied: user denied access to application'));
82-
}
83-
8481
const fns = [
8582
this.getAuthorizationCodeLifetime(),
8683
this.getClient(request),
@@ -98,7 +95,7 @@ AuthorizeHandler.prototype.handle = function(request, response) {
9895
return Promise.bind(this)
9996
.then(function() {
10097
state = this.getState(request);
101-
if(request.query.allowed === 'false') {
98+
if (request.query.allowed === 'false' || request.body.allowed === 'false') {
10299
throw new AccessDeniedError('Access denied: user denied access to application');
103100
}
104101
})
@@ -114,8 +111,10 @@ AuthorizeHandler.prototype.handle = function(request, response) {
114111
})
115112
.then(function(authorizationCode) {
116113
ResponseType = this.getResponseType(request);
114+
const codeChallenge = this.getCodeChallenge(request);
115+
const codeChallengeMethod = this.getCodeChallengeMethod(request);
117116

118-
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user);
117+
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user, codeChallenge, codeChallengeMethod);
119118
})
120119
.then(function(code) {
121120
const responseType = new ResponseType(code.authorizationCode);
@@ -293,13 +292,20 @@ AuthorizeHandler.prototype.getRedirectUri = function(request, client) {
293292
* Save authorization code.
294293
*/
295294

296-
AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) {
297-
const code = {
295+
AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod) {
296+
let code = {
298297
authorizationCode: authorizationCode,
299298
expiresAt: expiresAt,
300299
redirectUri: redirectUri,
301300
scope: scope
302301
};
302+
303+
if(codeChallenge && codeChallengeMethod){
304+
code = Object.assign({
305+
codeChallenge: codeChallenge,
306+
codeChallengeMethod: codeChallengeMethod
307+
}, code);
308+
}
303309
return promisify(this.model.saveAuthorizationCode, 3).call(this.model, code, client, user);
304310
};
305311

@@ -369,6 +375,27 @@ AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, stat
369375
response.redirect(url.format(redirectUri));
370376
};
371377

378+
AuthorizeHandler.prototype.getCodeChallenge = function(request) {
379+
return request.body.code_challenge;
380+
};
381+
382+
/**
383+
* Get code challenge method from request or defaults to plain.
384+
* https://www.rfc-editor.org/rfc/rfc7636#section-4.3
385+
*
386+
* @throws {InvalidRequestError} if request contains unsupported code_challenge_method
387+
* (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4)
388+
*/
389+
AuthorizeHandler.prototype.getCodeChallengeMethod = function(request) {
390+
const algorithm = request.body.code_challenge_method;
391+
392+
if (algorithm && !pkce.isValidMethod(algorithm)) {
393+
throw new InvalidRequestError(`Invalid request: transform algorithm '${algorithm}' not supported`);
394+
}
395+
396+
return algorithm || 'plain';
397+
};
398+
372399
/**
373400
* Export constructor.
374401
*/

lib/handlers/token-handler.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const TokenModel = require('../models/token-model');
1818
const UnauthorizedClientError = require('../errors/unauthorized-client-error');
1919
const UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error');
2020
const auth = require('basic-auth');
21+
const pkce = require('../pkce/pkce');
2122
const isFormat = require('@node-oauth/formats');
2223

2324
/**
@@ -114,12 +115,14 @@ TokenHandler.prototype.handle = function(request, response) {
114115
TokenHandler.prototype.getClient = function(request, response) {
115116
const credentials = this.getClientCredentials(request);
116117
const grantType = request.body.grant_type;
118+
const codeVerifier = request.body.code_verifier;
119+
const isPkce = pkce.isPKCERequest({ grantType, codeVerifier });
117120

118121
if (!credentials.clientId) {
119122
throw new InvalidRequestError('Missing parameter: `client_id`');
120123
}
121124

122-
if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret) {
125+
if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) {
123126
throw new InvalidRequestError('Missing parameter: `client_secret`');
124127
}
125128

@@ -174,6 +177,7 @@ TokenHandler.prototype.getClient = function(request, response) {
174177
TokenHandler.prototype.getClientCredentials = function(request) {
175178
const credentials = auth(request);
176179
const grantType = request.body.grant_type;
180+
const codeVerifier = request.body.code_verifier;
177181

178182
if (credentials) {
179183
return { clientId: credentials.name, clientSecret: credentials.pass };
@@ -183,6 +187,12 @@ TokenHandler.prototype.getClientCredentials = function(request) {
183187
return { clientId: request.body.client_id, clientSecret: request.body.client_secret };
184188
}
185189

190+
if (pkce.isPKCERequest({ grantType, codeVerifier })) {
191+
if(request.body.client_id) {
192+
return { clientId: request.body.client_id };
193+
}
194+
}
195+
186196
if (!this.isClientAuthenticationRequired(grantType)) {
187197
if(request.body.client_id) {
188198
return { clientId: request.body.client_id };

lib/pkce/pkce.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
/**
4+
* Module dependencies.
5+
*/
6+
const { base64URLEncode } = require('../utils/string-util');
7+
const { createHash } = require('../utils/crypto-util');
8+
const codeChallengeRegexp = /^([a-zA-Z0-9.\-_~]){43,128}$/;
9+
/**
10+
* Export `TokenUtil`.
11+
*/
12+
13+
const pkce = {
14+
/**
15+
* Return hash for code-challenge method-type.
16+
*
17+
* @param method {String} the code challenge method
18+
* @param verifier {String} the code_verifier
19+
* @return {String|undefined}
20+
*/
21+
getHashForCodeChallenge: function({ method, verifier }) {
22+
// to prevent undesired side-effects when passing some wird values
23+
// to createHash or base64URLEncode we first check if the values are right
24+
if (pkce.isValidMethod(method) && typeof verifier === 'string' && verifier.length > 0) {
25+
if (method === 'plain') {
26+
return verifier;
27+
}
28+
29+
if (method === 'S256') {
30+
const hash = createHash({ data: verifier });
31+
return base64URLEncode(hash);
32+
}
33+
}
34+
},
35+
36+
/**
37+
* Check if the request is a PCKE request. We assume PKCE if grant type is
38+
* 'authorization_code' and code verifier is present.
39+
*
40+
* @param grantType {String}
41+
* @param codeVerifier {String}
42+
* @return {boolean}
43+
*/
44+
isPKCERequest: function ({ grantType, codeVerifier }) {
45+
return grantType === 'authorization_code' && !!codeVerifier;
46+
},
47+
48+
/**
49+
* Matches a code verifier (or code challenge) against the following criteria:
50+
*
51+
* code-verifier = 43*128unreserved
52+
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
53+
* ALPHA = %x41-5A / %x61-7A
54+
* DIGIT = %x30-39
55+
*
56+
* @see: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
57+
* @param codeChallenge {String}
58+
* @return {Boolean}
59+
*/
60+
codeChallengeMatchesABNF: function (codeChallenge) {
61+
return typeof codeChallenge === 'string' &&
62+
!!codeChallenge.match(codeChallengeRegexp);
63+
},
64+
65+
/**
66+
* Checks if the code challenge method is one of the supported methods
67+
* 'sha256' or 'plain'
68+
*
69+
* @param method {String}
70+
* @return {boolean}
71+
*/
72+
isValidMethod: function (method) {
73+
return method === 'S256' || method === 'plain';
74+
}
75+
};
76+
77+
module.exports = pkce;

lib/utils/crypto-util.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
const crypto = require('crypto');
4+
5+
/**
6+
* Export `StringUtil`.
7+
*/
8+
9+
module.exports = {
10+
/**
11+
*
12+
* @param algorithm {String} the hash algorithm, default is 'sha256'
13+
* @param data {Buffer|String|TypedArray|DataView} the data to hash
14+
* @param encoding {String|undefined} optional, the encoding to calculate the
15+
* digest
16+
* @return {Buffer|String} if {encoding} undefined a {Buffer} is returned, otherwise a {String}
17+
*/
18+
createHash: function({ algorithm = 'sha256', data = undefined, encoding = undefined }) {
19+
return crypto
20+
.createHash(algorithm)
21+
.update(data)
22+
.digest(encoding);
23+
}
24+
};

0 commit comments

Comments
 (0)