Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d40173d

Browse files
committed
PKCE - implementation
1 parent e1f741f commit d40173d

File tree

11 files changed

+426
-25
lines changed

11 files changed

+426
-25
lines changed

‎lib/grant-types/abstract-grant-type.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function AbstractGrantType(options) {
3030
this.model = options.model;
3131
this.refreshTokenLifetime = options.refreshTokenLifetime;
3232
this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken;
33+
this.PKCEEnabled = options.PKCEEnabled;
3334
}
3435

3536
/**

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ var InvalidArgumentError = require('../errors/invalid-argument-error');
99
var InvalidGrantError = require('../errors/invalid-grant-error');
1010
var InvalidRequestError = require('../errors/invalid-request-error');
1111
var Promise = require('bluebird');
12+
var crypto = require('crypto');
1213
var promisify = require('promisify-any').use(Promise);
1314
var ServerError = require('../errors/server-error');
1415
var is = require('../validator/is');
1516
var util = require('util');
17+
var stringUtil = require('../utils/string-util');
1618

1719
/**
1820
* Constructor.
@@ -88,7 +90,9 @@ AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, cl
8890
if (!is.vschar(request.body.code)) {
8991
throw new InvalidRequestError('Invalid parameter: `code`');
9092
}
93+
9194
return promisify(this.model.getAuthorizationCode, 1).call(this.model, request.body.code)
95+
.bind(this)
9296
.then(function(code) {
9397
if (!code) {
9498
throw new InvalidGrantError('Invalid grant: authorization code is invalid');
@@ -118,6 +122,28 @@ AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, cl
118122
throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI');
119123
}
120124

125+
if (this.PKCEEnabled && client.isPublic) {
126+
if (!code.codeChallenge) {
127+
throw new ServerError('Server error: `getAuthorizationCode()` did not return a `codeChallenge` property');
128+
}
129+
130+
if (!code.codeChallengeMethod) {
131+
throw new ServerError('Server error: `getAuthorizationCode()` did not return a `codeChallengeMethod` property');
132+
}
133+
134+
if (code.codeChallengeMethod === 'plain' && code.codeChallenge !== request.body.code_verifier) {
135+
throw new InvalidGrantError('Invalid grant: `code_verifier` is invalid');
136+
}
137+
138+
if (code.codeChallengeMethod === 'S256') {
139+
var hash = stringUtil.base64URLEncode(crypto.createHash('sha256').update(request.body.code_verifier).digest());
140+
141+
if (code.codeChallenge !== hash) {
142+
throw new InvalidGrantError('Invalid grant: `code_verifier` is invalid');
143+
}
144+
}
145+
}
146+
121147
return code;
122148
});
123149
};

‎lib/handlers/authorize-handler.js‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function AuthorizeHandler(options) {
6262
this.allowEmptyState = options.allowEmptyState;
6363
this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options);
6464
this.authorizationCodeLifetime = options.authorizationCodeLifetime;
65+
this.PKCEEnabled = options.PKCEEnabled;
6566
this.model = options.model;
6667
}
6768

@@ -95,18 +96,25 @@ AuthorizeHandler.prototype.handle = function(request, response) {
9596
var scope;
9697
var state;
9798
var ResponseType;
99+
var codeChallenge;
100+
var codeChallengeMethod;
98101

99102
return Promise.bind(this)
100-
.then(function() {
103+
.then(function() {
101104
scope = this.getScope(request);
105+
codeChallenge = this.getCodeChallenge(request, client);
102106

103-
return this.generateAuthorizationCode(client, user, scope);
104-
})
107+
if (codeChallenge) {
108+
codeChallengeMethod = this.getCodeChallengeMethod(request);
109+
}
110+
111+
return this.generateAuthorizationCode(client, user, scope);
112+
})
105113
.then(function(authorizationCode) {
106114
state = this.getState(request);
107115
ResponseType = this.getResponseType(request);
108116

109-
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user);
117+
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user,codeChallenge,codeChallengeMethod);
110118
})
111119
.then(function(code) {
112120
var responseType = new ResponseType(code.authorizationCode);
@@ -135,11 +143,31 @@ AuthorizeHandler.prototype.handle = function(request, response) {
135143

136144
AuthorizeHandler.prototype.generateAuthorizationCode = function(client, user, scope) {
137145
if (this.model.generateAuthorizationCode) {
138-
return promisify(this.model.generateAuthorizationCode).call(this.model, client, user, scope);
146+
return promisify(this.model.generateAuthorizationCode,3).call(this.model, client, user, scope);
139147
}
140148
return tokenUtil.generateRandomToken();
141149
};
142150

151+
AuthorizeHandler.prototype.getCodeChallenge = function(request, client) {
152+
var codeChallenge = request.body.code_challenge || request.query.code_challenge;
153+
154+
if (this.PKCEEnabled && client.isPublic && _.isEmpty(codeChallenge)) {
155+
throw new InvalidRequestError('Missing parameter: `code_challenge`. Public clients must include a code_challenge');
156+
}
157+
158+
return codeChallenge;
159+
};
160+
161+
AuthorizeHandler.prototype.getCodeChallengeMethod = function(request) {
162+
var codeChallengeMethod = request.body.code_challenge_method || request.query.code_challenge_method || 'plain';
163+
164+
if (!_.includes(['S256', 'plain'], codeChallengeMethod)) {
165+
throw new InvalidRequestError('Invalid parameter: `code_challenge_method`');
166+
}
167+
168+
return codeChallengeMethod;
169+
};
170+
143171
/**
144172
* Get authorization code lifetime.
145173
*/
@@ -172,6 +200,7 @@ AuthorizeHandler.prototype.getClient = function(request) {
172200
throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI');
173201
}
174202
return promisify(this.model.getClient, 2).call(this.model, clientId, null)
203+
.bind(this)
175204
.then(function(client) {
176205
if (!client) {
177206
throw new InvalidClientError('Invalid client: client credentials are invalid');
@@ -192,6 +221,16 @@ AuthorizeHandler.prototype.getClient = function(request) {
192221
if (redirectUri && !_.includes(client.redirectUris, redirectUri)) {
193222
throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value');
194223
}
224+
225+
if (this.PKCEEnabled) {
226+
if (client.isPublic === undefined) {
227+
throw new InvalidClientError('Invalid client: missing client `isPublic`');
228+
}
229+
230+
if (typeof client.isPublic !== 'boolean') {
231+
throw new InvalidClientError('Invalid client: `isPublic` must be a boolean');
232+
}
233+
}
195234
return client;
196235
});
197236
};
@@ -257,12 +296,14 @@ AuthorizeHandler.prototype.getRedirectUri = function(request, client) {
257296
* Save authorization code.
258297
*/
259298

260-
AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) {
299+
AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user,codeChallenge,codeChallengeMethod) {
261300
var code = {
262301
authorizationCode: authorizationCode,
263302
expiresAt: expiresAt,
264303
redirectUri: redirectUri,
265-
scope: scope
304+
scope: scope,
305+
codeChallenge: codeChallenge,
306+
codeChallengeMethod: codeChallengeMethod
266307
};
267308
return promisify(this.model.saveAuthorizationCode, 3).call(this.model, code, client, user);
268309
};

‎lib/handlers/token-handler.js‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function TokenHandler(options) {
6262
this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes;
6363
this.requireClientAuthentication = options.requireClientAuthentication || {};
6464
this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false;
65+
this.PKCEEnabled = options.PKCEEnabled;
6566
}
6667

6768
/**
@@ -133,6 +134,7 @@ TokenHandler.prototype.getClient = function(request, response) {
133134
}
134135

135136
return promisify(this.model.getClient, 2).call(this.model, credentials.clientId, credentials.clientSecret)
137+
.bind(this)
136138
.then(function(client) {
137139
if (!client) {
138140
throw new InvalidClientError('Invalid client: client is invalid');
@@ -146,6 +148,16 @@ TokenHandler.prototype.getClient = function(request, response) {
146148
throw new ServerError('Server error: `grants` must be an array');
147149
}
148150

151+
if (this.PKCEEnabled) {
152+
if (client.isPublic === undefined) {
153+
throw new ServerError('Server error: missing client `isPublic`');
154+
}
155+
156+
if (typeof client.isPublic !== 'boolean') {
157+
throw new ServerError('Server error: invalid client, `isPublic` must be a boolean');
158+
}
159+
}
160+
149161
return client;
150162
})
151163
.catch(function(e) {
@@ -224,7 +236,8 @@ TokenHandler.prototype.handleGrantType = function(request, client) {
224236
accessTokenLifetime: accessTokenLifetime,
225237
model: this.model,
226238
refreshTokenLifetime: refreshTokenLifetime,
227-
alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken
239+
alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken,
240+
PKCEenabled: this.PKCEenabled
228241
};
229242

230243
return new Type(options)

‎lib/server.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ OAuth2Server.prototype.authenticate = function(request, response, options, callb
5151
OAuth2Server.prototype.authorize = function(request, response, options, callback) {
5252
options = _.assign({
5353
allowEmptyState: false,
54-
authorizationCodeLifetime: 5 * 60 // 5 minutes.
54+
authorizationCodeLifetime: 5 * 60, // 5 minutes.
55+
PKCEEnabled: false
5556
}, this.options, options);
5657

5758
return new AuthorizeHandler(options)
@@ -68,7 +69,8 @@ OAuth2Server.prototype.token = function(request, response, options, callback) {
6869
accessTokenLifetime: 60 * 60, // 1 hour.
6970
refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks.
7071
allowExtendedTokenAttributes: false,
71-
requireClientAuthentication: {} // defaults to true for all grant types
72+
requireClientAuthentication: {}, // defaults to true for all grant types
73+
PKCEEnabled: false
7274
}, this.options, options);
7375

7476
return new TokenHandler(options)

‎lib/utils/string-util.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
3+
/**
4+
* Export `StringUtil`.
5+
*/
6+
7+
module.exports = {
8+
base64URLEncode: function(str) {
9+
return str.toString('base64')
10+
.replace(/\+/g, '-')
11+
.replace(/\//g, '_')
12+
.replace(/=/g, '');
13+
}
14+
};

‎package.json‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,32 @@
77
"oauth2"
88
],
99
"contributors": [
10-
{ "name": "Thom Seddon", "email": "thom@seddonmedia.co.uk" },
11-
{ "name": "Lars F. Karlström" , "email": "lars@lfk.io" },
12-
{ "name": "Rui Marinho", "email": "ruipmarinho@gmail.com" },
13-
{ "name" : "Tiago Ribeiro", "email": "tiago.ribeiro@gmail.com" },
14-
{ "name": "Michael Salinger", "email": "mjsalinger@gmail.com" },
15-
{ "name": "Nuno Sousa" },
16-
{ "name": "Max Truxa" }
10+
{
11+
"name": "Thom Seddon",
12+
"email": "thom@seddonmedia.co.uk"
13+
},
14+
{
15+
"name": "Lars F. Karlström",
16+
"email": "lars@lfk.io"
17+
},
18+
{
19+
"name": "Rui Marinho",
20+
"email": "ruipmarinho@gmail.com"
21+
},
22+
{
23+
"name": "Tiago Ribeiro",
24+
"email": "tiago.ribeiro@gmail.com"
25+
},
26+
{
27+
"name": "Michael Salinger",
28+
"email": "mjsalinger@gmail.com"
29+
},
30+
{
31+
"name": "Nuno Sousa"
32+
},
33+
{
34+
"name": "Max Truxa"
35+
}
1736
],
1837
"main": "index.js",
1938
"dependencies": {

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /