diff --git a/index.d.ts b/index.d.ts index 260e34a..3d780b3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -306,7 +306,7 @@ declare namespace OAuth2Server { * */ saveAuthorizationCode( - code: Pick, + code: Pick, client: Client, user: User, callback?: Callback): Promise; @@ -410,6 +410,8 @@ declare namespace OAuth2Server { scope?: string | string[] | undefined; client: Client; user: User; + codeChallenge?: string; + codeChallengeMethod?: string; [key: string]: any; } diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 830bfa7..9a586d2 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -114,8 +114,10 @@ AuthorizeHandler.prototype.handle = function(request, response) { }) .then(function(authorizationCode) { ResponseType = this.getResponseType(request); + const codeChallenge = this.getCodeChallenge(request); + const codeChallengeMethod = this.getCodeChallengeMethod(request); - return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user); + return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user, codeChallenge, codeChallengeMethod); }) .then(function(code) { const responseType = new ResponseType(code.authorizationCode); @@ -293,13 +295,20 @@ AuthorizeHandler.prototype.getRedirectUri = function(request, client) { * Save authorization code. */ -AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) { - const code = { +AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod) { + let code = { authorizationCode: authorizationCode, expiresAt: expiresAt, redirectUri: redirectUri, scope: scope }; + + if(codeChallenge && codeChallengeMethod){ + code = Object.assign({ + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod + }, code); + } return promisify(this.model.saveAuthorizationCode, 3).call(this.model, code, client, user); }; @@ -369,6 +378,18 @@ AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, stat response.redirect(url.format(redirectUri)); }; +AuthorizeHandler.prototype.getCodeChallenge = function(request) { + return request.body.code_challenge; +}; + +/** + * Get code challenge method from request or defaults to plain. + * https://www.rfc-editor.org/rfc/rfc7636#section-4.3 + */ +AuthorizeHandler.prototype.getCodeChallengeMethod = function(request) { + return request.body.code_challenge_method || 'plain'; +}; + /** * Export constructor. */ diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 054e2cc..e14b12c 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -1321,4 +1321,44 @@ describe('AuthorizeHandler integration', function() { response.get('location').should.equal('http://example.com/cb?state=foobar'); }); }); + + describe('getCodeChallengeMethod()', function() { + it('should get code challenge method', function() { + const model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {code_challenge_method: 'S256'}, headers: {}, method: {}, query: {} }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal('S256'); + }); + + it('should get default code challenge method plain if missing', function() { + const model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal('plain'); + }); + }); + + describe('getCodeChallenge()', function() { + it('should get code challenge', function() { + const model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {code_challenge: 'challenge'}, headers: {}, method: {}, query: {} }); + + const codeChallengeMethod = handler.getCodeChallenge(request); + codeChallengeMethod.should.equal('challenge'); + }); + }); }); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 376bc1e..0038c7c 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -98,6 +98,26 @@ describe('AuthorizeHandler', function() { }) .catch(should.fail); }); + + it('should call `model.saveAuthorizationCode()` with code challenge', function() { + const model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: sinon.stub().returns({}) + }; + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + + return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod') + .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', codeChallenge: 'codeChallenge', codeChallengeMethod: 'codeChallengeMethod' }); + model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); }); describe('validateRedirectUri()', function() {