From c0593ef7d53874130317e9538296973cb2646ad6 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Aug 2023 15:55:47 +0200 Subject: [PATCH] tests(integration): deep-cover model integration in AuthorizeHandler tests --- lib/handlers/authorize-handler.js | 1 + .../handlers/authorize-handler_test.js | 634 +++++++++++------- 2 files changed, 398 insertions(+), 237 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 65168324..19922ed5 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -347,6 +347,7 @@ AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) error: error.name }; + if (error.message) { uri.query.error_description = error.message; } diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 17df3160..1e4a515d 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -20,6 +20,15 @@ const UnauthorizedClientError = require('../../../lib/errors/unauthorized-client const should = require('chai').should(); const url = require('url'); +const createModel = (model = {}) => { + return { + getAccessToken: () => should.fail(), + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail(), + ...model + }; +}; + /** * Test `AuthorizeHandler` integration. */ @@ -29,7 +38,6 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if `options.authorizationCodeLifetime` is missing', function() { try { new AuthorizeHandler(); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -40,7 +48,6 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if `options.model` is missing', function() { try { new AuthorizeHandler({ authorizationCodeLifetime: 120 }); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -51,7 +58,6 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `getClient()`', function() { try { new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -61,8 +67,7 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: function() {} } }); - + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: () => should.fail() } }); should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -72,12 +77,12 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `getAccessToken()`', function() { const model = { - getClient: function() {}, - saveAuthorizationCode: function() {} + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail() }; try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); should.fail(); } catch (e) { @@ -87,51 +92,58 @@ describe('AuthorizeHandler integration', function() { }); it('should set the `authorizationCodeLifetime`', function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.authorizationCodeLifetime.should.equal(120); }); - it('should set the `authenticateHandler`', function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + it('should throw if the custom `authenticateHandler` does not implement a `handle` method', function () { + const model = createModel(); + const authenticateHandler = {}; // misses handle() method + + try { + new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, model }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: authenticateHandler does not implement `handle()`'); + } + }); + it('should set the default `authenticateHandler`, if no custom one is passed', function() { + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); }); - it('should set the `model`', function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + it('should set the custom `authenticateHandler`, if valid', function () { + const model = createModel(); + + class CustomAuthenticateHandler { + async handle () {} + } + + const authenticateHandler = new CustomAuthenticateHandler(); + const handler = new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, model }); + handler.authenticateHandler.should.be.an.instanceOf(CustomAuthenticateHandler); + handler.authenticateHandler.should.not.be.an.instanceOf(AuthenticateHandler); + }); + it('should set the `model`', function() { + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.model.should.equal(model); }); }); describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); try { await handler.handle(); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -140,17 +152,12 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `response` is missing', async function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { await handler.handle(request); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -158,28 +165,35 @@ describe('AuthorizeHandler integration', function() { } }); - it('should redirect to an error response if user denied access', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if user denied access', async function() { + const client = { + id: 'client-12345', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; + const model = createModel({ + getAccessToken: async function(_token) { + _token.should.equal('foobarbazmootoken'); return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + getClient: async function(clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + } + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { - client_id: 12345, + client_id: client.id, response_type: 'code' }, method: {}, headers: { - 'Authorization': 'Bearer foo' + 'Authorization': 'Bearer foobarbazmootoken' }, query: { state: 'foobar', @@ -188,29 +202,39 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Access denied: user denied access to application'); + response + .get('location') + .should + .equal('http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar'); + } }); - it('should redirect to an error response if a non-oauth error is thrown', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if a non-oauth error is thrown', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; + getClient: async function() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; }, - saveAuthorizationCode: function() { - throw new Error('Unhandled exception'); + saveAuthorizationCode: async function() { + throw new CustomError('Unhandled exception'); } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + class CustomError extends Error {} + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -226,29 +250,35 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); // non-oauth-errors are converted to ServerError + e.message.should.equal('Unhandled exception'); + response + .get('location') + .should + .equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); + } }); - it('should redirect to an error response if an oauth error is thrown', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if an oauth error is thrown', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { + saveAuthorizationCode: async function() { throw new AccessDeniedError('Cannot request this auth code'); } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -264,69 +294,87 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Cannot request this auth code'); + response + .get('location') + .should + .equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); + } }); - it('should redirect to a successful response with `code` and `state` if successful', function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { - getAccessToken: function() { + it('should redirect to a successful response with `code` and `state` if successful', async function() { + const client = { + id: 'client-12343434', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; + const model = createModel({ + getAccessToken: async function(_token) { + _token.should.equal('foobarbaztokenmoo'); return { - client: client, + client, user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return client; + getClient: async function(clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + saveAuthorizationCode: async function() { + return { + authorizationCode: 'fooobar-long-authzcode-?', + client + }; } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { - client_id: 12345, + client_id: client.id, response_type: 'code' }, headers: { - 'Authorization': 'Bearer foo' + 'Authorization': 'Bearer foobarbaztokenmoo' }, method: {}, query: { - state: 'foobar' + state: 'foobarbazstatemoo' } }); const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - response.get('location').should.equal('http://example.com/cb?code=12345&state=foobar'); - }) - .catch(should.fail); - }); - - it('should redirect to an error response if `scope` is invalid', function() { - const model = { - getAccessToken: function() { + const data = await handler.handle(request, response); + data.authorizationCode.should.equal('fooobar-long-authzcode-?'); + data.client.should.deep.equal(client); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); + }); + + it('should redirect to an error response if `scope` is invalid', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { + saveAuthorizationCode: async function() { return {}; } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -343,14 +391,18 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + response.status.should.equal(302); + response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar'); + } }); - it('should redirect to a successful response if `model.validateScope` is not defined', function() { + it('should redirect to a successful response if `model.validateScope` is not defined', async function() { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { getAccessToken: function() { @@ -364,10 +416,10 @@ describe('AuthorizeHandler integration', function() { return client; }, saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + return { authorizationCode: 'fooobar-long-authzcode-?', client }; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -379,42 +431,44 @@ describe('AuthorizeHandler integration', function() { method: {}, query: { scope: 'read', - state: 'foobar' + state: 'foobarbazstatemoo' } }); const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.eql({ - authorizationCode: 12345, - client: client - }); - }) - .catch(should.fail); + const data = await handler.handle(request, response); + data.should.deep.equal({ + authorizationCode: 'fooobar-long-authzcode-?', + client: client + }); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); }); - it('should redirect to an error response if `scope` is insufficient', function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; + it('should redirect to an error response if `scope` is insufficient (validateScope)', async function() { + const client = { id: 12345, grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { - getAccessToken: function() { + getAccessToken: async function() { return { client: client, - user: {}, + user: { name: 'foouser' }, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return client; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + saveAuthorizationCode: async function() { + return { authorizationCode: 12345, client }; }, - validateScope: function() { + validateScope: async function(_user, _client, _scope) { + _scope.should.equal('read'); return false; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -431,29 +485,36 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch(e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: Requested scope is invalid'); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar'); + } }); - it('should redirect to an error response if `state` is missing', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if `state` is missing', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { + saveAuthorizationCode: async function() { throw new AccessDeniedError('Cannot request this auth code'); } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -467,29 +528,34 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); + } }); - it('should redirect to an error response if `response_type` is invalid', function() { + it('should redirect to an error response if `response_type` is invalid', async function() { const model = { - getAccessToken: function() { + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: {} }; - } + saveAuthorizationCode: () => should.fail() // should fail before call }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -505,33 +571,43 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedResponseTypeError); + e.message.should.equal('Unsupported response type: `response_type` is not supported'); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); + } }); - it('should fail on invalid `response_type` before calling model.saveAuthorizationCode()', function() { + it('should return the `code` if successful', async function() { + const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { - getAccessToken: function() { + getAccessToken: async function() { return { + client: client, user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; + getClient: async function() { + return client; }, - saveAuthorizationCode: function() { - throw new Error('must not be reached'); + generateAuthorizationCode: async () => 'some-code', + saveAuthorizationCode: async function(code) { + return { authorizationCode: code.authorizationCode, client: client }; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, - response_type: 'test' + response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' @@ -543,24 +619,111 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: 'some-code', + client: client + }); }); - it('should return the `code` if successful', function() { + it('should return the `code` if successful (full model implementation)', async function () { + const user = { name: 'fooUser' }; + const state = 'fooobarstatebaz'; + const scope = 'read'; + const client = { + id: 'client-1322132131', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; + const authorizationCode = 'long-authz-code-?'; + const accessTokenDoc = { + accessToken: 'some-access-token-code-?', + client, + user, + scope, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) + }; + const model = { + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + getAccessToken: async function (_token) { + _token.should.equal(accessTokenDoc.accessToken); + return { ...accessTokenDoc }; + }, + verifyScope: async function (_tokenDoc, _scope) { + _tokenDoc.should.equal(accessTokenDoc); + _scope.should.equal(accessTokenDoc.scope); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return _scope; + }, + generateAuthorizationCode: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return authorizationCode; + }, + saveAuthorizationCode: async function (code, _client, _user) { + code.authorizationCode.should.equal(authorizationCode); + code.expiresAt.should.be.instanceOf(Date); + _user.should.deep.equal(user); + _client.should.deep.equal(client); + return { ...code, client, user }; + } + }; + + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + const request = new Request({ + body: { + client_id: client.id, + response_type: 'code' + }, + headers: { + 'Authorization': `Bearer ${accessTokenDoc.accessToken}` + }, + method: {}, + query: { state, scope } + }); + + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.scope.should.equal(scope); + data.client.should.deep.equal(client); + data.user.should.deep.equal(user); + data.expiresAt.should.be.instanceOf(Date); + data.redirectUri.should.equal(client.redirectUris[0]); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?code=long-authz-code-%3F&state=fooobarstatebaz'); + }); + + it('should support a custom `authenticateHandler`', async function () { + const user = { name: 'user1' }; + const authenticateHandler = { + handle: async function () { + // all good + return { ...user }; + } + }; const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { - getAccessToken: function() { + getAccessToken: async function() { return { client: client, user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return client; }, generateAuthorizationCode: async () => 'some-code', @@ -568,7 +731,7 @@ describe('AuthorizeHandler integration', function() { return { authorizationCode: code.authorizationCode, client: client }; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model, authenticateHandler }); const request = new Request({ body: { client_id: 12345, @@ -584,14 +747,11 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(function(data) { - data.should.eql({ - authorizationCode: 'some-code', - client: client - }); - }) - .catch(should.fail); + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: 'some-code', + client: client + }); }); }); @@ -602,7 +762,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); return handler.generateAuthorizationCode() .then(function(data) { @@ -620,7 +780,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); @@ -634,7 +794,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); @@ -647,7 +807,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); }); @@ -661,7 +821,7 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.validateRedirectUri('http://example.com/a', { redirectUris: ['http://example.com/a'] }).should.be.an.instanceOf(Promise); }); @@ -676,7 +836,7 @@ describe('AuthorizeHandler integration', function() { } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise); }); @@ -691,7 +851,7 @@ describe('AuthorizeHandler integration', function() { } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise); }); @@ -704,7 +864,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); try { @@ -723,7 +883,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 'ø倣‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); try { @@ -742,7 +902,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); try { @@ -761,7 +921,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -780,7 +940,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -799,7 +959,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -816,7 +976,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: ['authorization_code'] }; }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -835,7 +995,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -854,7 +1014,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345 }, headers: {}, @@ -873,7 +1033,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345 }, headers: {}, @@ -894,7 +1054,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: { client_id: 12345 } }); return handler.getClient(request) @@ -913,7 +1073,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); try { @@ -933,7 +1093,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); handler.getScope(request).should.equal('foo'); @@ -947,7 +1107,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { scope: 'foo' } }); handler.getScope(request).should.equal('foo'); @@ -962,7 +1122,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -981,7 +1141,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const state = handler.getState(request); should.equal(state, undefined); @@ -993,7 +1153,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'ø倣‰' } }); try { @@ -1013,7 +1173,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); handler.getState(request).should.equal('foobar'); @@ -1027,7 +1187,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); handler.getState(request).should.equal('foobar'); @@ -1042,7 +1202,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const response = new Response(); @@ -1066,7 +1226,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -1088,7 +1248,7 @@ describe('AuthorizeHandler integration', function() { return authorizationCode; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); return handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz') .then(function(data) { @@ -1105,7 +1265,7 @@ describe('AuthorizeHandler integration', function() { return {}; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); @@ -1118,7 +1278,7 @@ describe('AuthorizeHandler integration', function() { return {}; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); @@ -1131,7 +1291,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -1150,7 +1310,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); try { @@ -1170,7 +1330,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); const ResponseType = handler.getResponseType(request); @@ -1185,7 +1345,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); const ResponseType = handler.getResponseType(request); @@ -1201,7 +1361,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const responseType = new CodeResponseType(12345); const redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); @@ -1217,7 +1377,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); @@ -1230,7 +1390,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=Bad%20Request'); @@ -1244,7 +1404,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const response = new Response({ body: {}, headers: {} }); const uri = url.parse('http://example.com/cb'); @@ -1261,7 +1421,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge_method: 'S256'}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1274,7 +1434,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge_method: 'foo'}, headers: {}, method: {}, query: {} }); try { @@ -1294,7 +1454,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1309,7 +1469,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge: 'challenge'}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallenge(request);