diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 7eae70f8f..787dfabff 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -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. @@ -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) { @@ -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; }); }; diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 5d9d58208..75b540b0b 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -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; }); }; @@ -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. */ diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js index f0dd8a141..35c0817f9 100644 --- a/lib/response-types/code-response-type.js +++ b/lib/response-types/code-response-type.js @@ -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'); /** @@ -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) @@ -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; @@ -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]); @@ -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. */ diff --git a/lib/utils/string-util.js b/lib/utils/string-util.js new file mode 100644 index 000000000..1ad8045ba --- /dev/null +++ b/lib/utils/string-util.js @@ -0,0 +1,14 @@ +'use strict'; + +/** + * Export `StringUtil`. + */ + +module.exports = { + base64URLEncode: function(str) { + return str.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + } +}; diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 7f84e3443..31be214d6 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -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'); /** @@ -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' }; diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 82dece8f5..518dcfdf5 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -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) { diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index 93284b61d..9ce68b878 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -6,6 +6,8 @@ var CodeResponseType = require('../../../lib/response-types/code-response-type'); var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +var Request = require('../../../lib/request'); var Promise = require('bluebird'); var should = require('should'); var sinon = require('sinon'); @@ -220,11 +222,11 @@ describe('CodeResponseType integration', function() { }; var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); - return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz', 'boz') + return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz', 'boz', 'buz', 'bez') .then(function() { model.saveAuthorizationCode.callCount.should.equal(1); model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: 'qux' }); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: 'qux', codeChallenge: 'buz', codeChallengeMethod: 'bez' }); model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); }) @@ -249,4 +251,81 @@ describe('CodeResponseType integration', function() { .catch(should.fail); }); }); + + describe('getCodeChallenge()', function() { + describe('with `code_challenge` in the request body', function() { + it('should return the code_challenge', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + var request = new Request({ body: { code_challenge: 'foo' }, headers: {}, method: {}, query: {} }); + + handler.getCodeChallenge(request).should.equal('foo'); + }); + }); + + describe('with `code_challenge` in the request query', function() { + it('should return the code_challenge', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: { code_challenge: 'foo' } }); + + handler.getCodeChallenge(request).should.equal('foo'); + }); + }); + }); + + describe('getCodeChallengeMethod()', function() { + it('should throw an error if `code_challenge_method` is invalid', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + var request = new Request({ body: { code_challenge_method: 'foo' }, headers: {}, method: {}, query: {} }); + + try { + handler.getCodeChallengeMethod(request); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code_challenge_method`, use S256 instead'); + } + }); + describe('with `code_challenge_method` in the request body', function() { + it('should return the code_challenge_method', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + var request = new Request({ body: { code_challenge_method: 'plain' }, headers: {}, method: {}, query: {} }); + + handler.getCodeChallengeMethod(request).should.equal('plain'); + }); + }); + + describe('with `code_challenge_method` in the request query', function() { + it('should return the code_challenge_method', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: { code_challenge_method: 'S256' } }); + + handler.getCodeChallengeMethod(request).should.equal('S256'); + }); + }); + }); });