Skip to content

PKCE #452

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

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open

PKCE #452

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
25 changes: 25 additions & 0 deletions lib/grant-types/authorization-code-grant-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ var InvalidArgumentError = require('../errors/invalid-argument-error');
var InvalidGrantError = require('../errors/invalid-grant-error');
var InvalidRequestError = require('../errors/invalid-request-error');
var Promise = require('bluebird');
var crypto = require('crypto');
var promisify = require('promisify-any').use(Promise);
var ServerError = require('../errors/server-error');
var is = require('../validator/is');
var util = require('util');
var stringUtil = require('../utils/string-util');

/**
* Constructor.
Expand Down Expand Up @@ -88,6 +90,7 @@ AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, cl
if (!is.vschar(request.body.code)) {
throw new InvalidRequestError('Invalid parameter: `code`');
}

return promisify(this.model.getAuthorizationCode, 1).call(this.model, request.body.code)
.then(function(code) {
if (!code) {
Expand Down Expand Up @@ -118,6 +121,28 @@ AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, cl
throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI');
}

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

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

if (code.codeChallengeMethod === 'plain' && code.codeChallenge !== request.body.code_verifier) {
throw new InvalidGrantError('Invalid grant: code verifier is invalid');
}

if (code.codeChallengeMethod === 'S256') {
var hash = stringUtil.base64URLEncode(crypto.createHash('sha256').update(request.body.code_verifier).digest());

if (code.codeChallenge !== hash) {
throw new InvalidGrantError('Invalid grant: code verifier is invalid');
}
}
}

return code;
});
};
Expand Down
2 changes: 1 addition & 1 deletion lib/handlers/authorize-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ AuthorizeHandler.prototype.getClient = function(request) {
if (redirectUri && !_.includes(client.redirectUris, redirectUri)) {
throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value');
}

return client;
});
};
Expand Down Expand Up @@ -247,7 +248,6 @@ AuthorizeHandler.prototype.getRedirectUri = function(request, client) {
return request.body.redirect_uri || request.query.redirect_uri || client.redirectUris[0];
};


/**
* Get response type.
*/
Expand Down
43 changes: 40 additions & 3 deletions lib/response-types/code-response-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
*/

var InvalidArgumentError = require('../errors/invalid-argument-error');
var InvalidRequestError = require('../errors/invalid-request-error');
var tokenUtil = require('../utils/token-util');
var is = require('../validator/is');
var _ = require('lodash');
var Promise = require('bluebird');

/**
Expand Down Expand Up @@ -53,6 +56,8 @@ CodeResponseType.prototype.handle = function(request, client, user, uri, scope)
throw new InvalidArgumentError('Missing parameter: `uri`');
}

var codeChallenge = this.getCodeChallenge(request);
var codeChallengeMethod = codeChallenge && this.getCodeChallengeMethod(request);
var fns = [
this.generateAuthorizationCode(),
this.getAuthorizationCodeExpiresAt(client)
Expand All @@ -61,7 +66,7 @@ CodeResponseType.prototype.handle = function(request, client, user, uri, scope)
return Promise.all(fns)
.bind(this)
.spread(function(authorizationCode, expiresAt) {
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user);
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user, codeChallenge, codeChallengeMethod);
})
.then(function(code) {
this.code = code.authorizationCode;
Expand Down Expand Up @@ -94,12 +99,14 @@ CodeResponseType.prototype.getAuthorizationCodeLifetime = function(client) {
* Save authorization code.
*/

CodeResponseType.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) {
CodeResponseType.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod) {
var code = {
authorizationCode: authorizationCode,
expiresAt: expiresAt,
redirectUri: redirectUri,
scope: scope
scope: scope,
codeChallenge: codeChallenge,
codeChallengeMethod: codeChallengeMethod
};

return Promise.try(this.model.saveAuthorizationCode, [code, client, user]);
Expand All @@ -117,6 +124,36 @@ CodeResponseType.prototype.generateAuthorizationCode = function() {
return tokenUtil.generateRandomToken();
};

/**
* Get Code challenge
*/
CodeResponseType.prototype.getCodeChallenge = function(request) {
var codeChallenge = request.body.code_challenge || request.query.code_challenge;

if (!codeChallenge || codeChallenge === '') {
return;
}

if (!is.vschar(codeChallenge)) {
throw new InvalidRequestError('Invalid parameter: `code_challenge`');
}

return codeChallenge;
};

/**
* Get Code challenge method
*/
CodeResponseType.prototype.getCodeChallengeMethod = function(request) {
var codeChallengeMethod = request.body.code_challenge_method || request.query.code_challenge_method || 'plain';

if (!_.includes(['S256', 'plain'], codeChallengeMethod)) {
throw new InvalidRequestError('Invalid parameter: `code_challenge_method`, use S256 instead');
}

return codeChallengeMethod;
};

/**
* Build redirect uri.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/utils/string-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

/**
* Export `StringUtil`.
*/

module.exports = {
base64URLEncode: function(str) {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
};
106 changes: 106 additions & 0 deletions test/integration/grant-types/authorization-code-grant-type_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error')
var InvalidGrantError = require('../../../lib/errors/invalid-grant-error');
var InvalidRequestError = require('../../../lib/errors/invalid-request-error');
var Promise = require('bluebird');
var crypto = require('crypto');
var Request = require('../../../lib/request');
var ServerError = require('../../../lib/errors/server-error');
var stringUtil = require('../../../lib/utils/string-util');
var should = require('should');

/**
Expand Down Expand Up @@ -361,6 +363,110 @@ describe('AuthorizationCodeGrantType integration', function() {
});
});

it('should throw an error if the `code_verifier` is invalid', function() {
var codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32));
var authorizationCode = {
authorizationCode: 12345,
client: { id: 'foobar' },
expiresAt: new Date(new Date() * 2),
user: {},
codeChallengeMethod: 'S256',
codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest())
};
var client = { id: 'foobar', isPublic: true };
var model = {
getAuthorizationCode: function() { return authorizationCode; },
revokeAuthorizationCode: function() {},
saveToken: function() {}
};
var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model });
var request = new Request({ body: { code: 12345, code_verifier: 'foo' }, headers: {}, method: {}, query: {} });

return grantType.getAuthorizationCode(request, client)
.then(should.fail)
.catch(function(e) {
e.should.be.an.instanceOf(InvalidGrantError);
e.message.should.equal('Invalid grant: code verifier is invalid');
});
});

it('should throw an error if the `code_verifier` is invalid', function() {
var authorizationCode = {
authorizationCode: 12345,
client: { id: 'foobar' },
expiresAt: new Date(new Date() * 2),
user: {},
codeChallengeMethod: 'plain',
codeChallenge: 'baz'
};
var client = { id: 'foobar', isPublic: true };
var model = {
getAuthorizationCode: function() { return authorizationCode; },
revokeAuthorizationCode: function() {},
saveToken: function() {}
};
var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model });
var request = new Request({ body: { code: 12345, code_verifier: 'foo' }, headers: {}, method: {}, query: {} });

return grantType.getAuthorizationCode(request, client)
.then(should.fail)
.catch(function(e) {
e.should.be.an.instanceOf(InvalidGrantError);
e.message.should.equal('Invalid grant: code verifier is invalid');
});
});

it('should return an auth code when `code_verifier` is valid', function() {
var codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32));
var authorizationCode = {
authorizationCode: 12345,
client: { id: 'foobar', isPublic: true },
expiresAt: new Date(new Date() * 2),
user: {},
codeChallengeMethod: 'S256',
codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest())
};
var client = { id: 'foobar', isPublic: true };
var model = {
getAuthorizationCode: function() { return authorizationCode; },
revokeAuthorizationCode: function() {},
saveToken: function() {}
};
var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model });
var request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, query: {} });

return grantType.getAuthorizationCode(request, client)
.then(function(data) {
data.should.equal(authorizationCode);
})
.catch(should.fail);
});

it('should return an auth code when `code_verifier` is valid', function() {
var authorizationCode = {
authorizationCode: 12345,
client: { id: 'foobar' },
expiresAt: new Date(new Date() * 2),
user: {},
codeChallengeMethod: 'plain',
codeChallenge: 'baz'
};
var client = { id: 'foobar', isPublic: true };
var model = {
getAuthorizationCode: function() { return authorizationCode; },
revokeAuthorizationCode: function() {},
saveToken: function() {}
};
var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model });
var request = new Request({ body: { code: 12345, code_verifier: 'baz' }, headers: {}, method: {}, query: {} });

return grantType.getAuthorizationCode(request, client)
.then(function(data) {
data.should.equal(authorizationCode);
})
.catch(should.fail);
});

it('should return an auth code', function() {
var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} };
var client = { id: 'foobar' };
Expand Down
12 changes: 6 additions & 6 deletions test/integration/handlers/token-handler_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -570,13 +570,13 @@ describe('TokenHandler integration', function() {
requireClientAuthentication: {
password: false
}
});
});
var request = new Request({
body: { grant_type: 'password'},
headers: { 'authorization': util.format('Basic %s', new Buffer('blah:').toString('base64')) },
method: {},
query: {}
});
body: { grant_type: 'password' },
headers: { 'authorization': util.format('Basic %s', new Buffer('blah:').toString('base64')) },
method: {},
query: {}
});

return handler.getClient(request)
.then(function(data) {
Expand Down
Loading