From 704d917c95661c65a555f9f55570b6751c63ea9b Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Thu, 17 Aug 2023 13:53:56 +0200 Subject: [PATCH 1/7] tests(compliance): added client credential workflow compliance tests --- .../client-credential-workflow_test.js | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 test/compliance/client-credential-workflow_test.js diff --git a/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js new file mode 100644 index 00000000..6b7a2898 --- /dev/null +++ b/test/compliance/client-credential-workflow_test.js @@ -0,0 +1,142 @@ +/** + * 4.4. Client Credentials Grant + * + * The client can request an access token using only its client + * credentials (or other supported means of authentication) when the + * client is requesting access to the protected resources under its + * control, or those of another resource owner that have been previously + * arranged with the authorization server (the method of which is beyond + * the scope of this specification). + * + * The client credentials grant type MUST only be used by confidential + * clients. + * + * @see https://www.rfc-editor.org/rfc/rfc6749#section-4.4 + */ + +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); + +require('chai').should(); + +const db = new DB(); +// this user represents requests in the name of an external server +// TODO: we should discuss, if we can make user optional for client credential workflows +// as it's not desired to have an extra fake-user representing a server just to pass validation +const userDoc = { id: 'machine2-123456789', name: 'machine2' }; +db.saveUser(userDoc); + +const oAuth2Server = new OAuth2Server({ + model: { + ...createModel(db), + getUserFromClient: async function (_client) { + // in a machine2machine setup we might not have a dedicated "user" + // but we need to return a truthy response to + const client = db.findClient(_client.id, _client.secret); + return client && { ...userDoc }; + } + } +}); + +const clientDoc = db.saveClient({ + id: 'client-credential-test-client', + secret: 'client-credential-test-secret', + grants: ['client_credentials'] +}); + +const enabledScope = 'read write'; + +describe('ClientCredentials Workflow Compliance (4.4)', function () { + describe('Access Token Request (4.4.1)', function () { + /** + * 4.4.2. Access Token Request + * + * The client makes a request to the token endpoint by adding the + * following parameters using the "application/x-www-form-urlencoded" + * format per Appendix B with a character encoding of UTF-8 in the HTTP + * request entity-body: + * + * grant_type + * REQUIRED. Value MUST be set to "client_credentials". + * + * scope + * OPTIONAL. The scope of the access request as described by + * Section 3.3. + * + * The client MUST authenticate with the authorization server as + * described in Section 3.2.1. + */ + it('authenticates the client with valid credentials', async function () { + const response = new Response(); + const request = createRequest({ + body: { + grant_type: 'client_credentials', + scope: enabledScope + }, + headers: { + 'authorization': 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded' + }, + method: 'POST', + }); + + const token = await oAuth2Server.token(request, response); + + response.status.should.equal(200); + response.headers.should.deep.equal( { 'cache-control': 'no-store', pragma: 'no-cache' }); + response.body.token_type.should.equal('Bearer'); + response.body.access_token.should.equal(token.accessToken); + response.body.expires_in.should.be.a('number'); + response.body.scope.should.equal(enabledScope); + ('refresh_token' in response.body).should.equal(false); + + token.accessToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + ('refreshToken' in token).should.equal(false); + ('refreshTokenExpiresAt' in token).should.equal(false); + token.scope.should.equal(enabledScope); + + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(false); + }); + + /** + * 7. Accessing Protected Resources + * + * The client accesses protected resources by presenting the access + * token to the resource server. The resource server MUST validate the + * access token and ensure that it has not expired and that its scope + * covers the requested resource. The methods used by the resource + * server to validate the access token (as well as any error responses) + * are beyond the scope of this specification but generally involve an + * interaction or coordination between the resource server and the + * authorization server. + */ + it('enables an authenticated request using the access token', async function () { + const [accessToken] = [...db.accessTokens.entries()][0]; + const response = new Response(); + const request = createRequest({ + query: {}, + headers: { + 'authorization': `Bearer ${accessToken}` + }, + method: 'GET', + }); + + const token = await oAuth2Server.authenticate(request, response); + token.accessToken.should.equal(accessToken); + token.user.should.deep.equal(userDoc); + token.client.should.deep.equal(clientDoc); + token.scope.should.equal(enabledScope); + + response.status.should.equal(200); + // there should be no information in the response as it + // should only add information, if permission is denied + response.body.should.deep.equal({}); + response.headers.should.deep.equal({}); + }); + }); +}); \ No newline at end of file From f0259dbbcc1fb3c6d645fc51d3c6d327e5da70e7 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Thu, 17 Aug 2023 13:54:43 +0200 Subject: [PATCH 2/7] tests(integration): grant types integration tests model integration covered --- lib/grant-types/abstract-grant-type.js | 10 +- .../authorization-code-grant-type.js | 10 +- .../client-credentials-grant-type.js | 4 +- lib/grant-types/password-grant-type.js | 8 +- .../grant-types/abstract-grant-type_test.js | 115 ++++-- .../authorization-code-grant-type_test.js | 383 +++++++++++------- .../client-credentials-grant-type_test.js | 71 ++-- .../grant-types/password-grant-type_test.js | 183 +++++---- test/integration/server_test.js | 18 +- test/unit/errors/oauth-error_test.js | 23 +- test/unit/request_test.js | 19 + 11 files changed, 544 insertions(+), 300 deletions(-) diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index 4fd02437..033fba36 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -36,8 +36,9 @@ function AbstractGrantType(options) { AbstractGrantType.prototype.generateAccessToken = async function(client, user, scope) { if (this.model.generateAccessToken) { - const accessToken = await this.model.generateAccessToken(client, user, scope); - return accessToken || tokenUtil.generateRandomToken(); + // We should not fall back to a random accessToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateAccessToken(client, user, scope); } return tokenUtil.generateRandomToken(); @@ -49,8 +50,9 @@ AbstractGrantType.prototype.generateAccessToken = async function(client, user, s AbstractGrantType.prototype.generateRefreshToken = async function(client, user, scope) { if (this.model.generateRefreshToken) { - const refreshToken = await this.model.generateRefreshToken(client, user, scope); - return refreshToken || tokenUtil.generateRandomToken(); + // We should not fall back to a random refreshToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateRefreshToken(client, user, scope); } return tokenUtil.generateRandomToken(); diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 2101462b..8b766bfc 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -195,11 +195,11 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); const token = { - accessToken: accessToken, - authorizationCode: authorizationCode, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, + accessToken, + authorizationCode, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, scope: validatedScope, }; diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index 211628d3..c348e5cf 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -73,8 +73,8 @@ class ClientCredentialsGrantType extends AbstractGrantType { const accessToken = await this.generateAccessToken(client, user, scope); const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(client, user, scope); const token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, + accessToken, + accessTokenExpiresAt, scope: validatedScope, }; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index f13b68aa..f483e188 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -94,10 +94,10 @@ class PasswordGrantType extends AbstractGrantType { const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); const token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, scope: validatedScope, }; diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index e874509f..22247d7a 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -7,6 +7,7 @@ const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); const Request = require('../../../lib/request'); +const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); const should = require('chai').should(); /** @@ -44,7 +45,7 @@ describe('AbstractGrantType integration', function() { }); it('should set the `model`', function() { - const model = {}; + const model = { async generateAccessToken () {} }; const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: model }); grantType.model.should.equal(model); @@ -58,70 +59,62 @@ describe('AbstractGrantType integration', function() { }); describe('generateAccessToken()', function() { - it('should return an access token', function() { + it('should return an access token', async function() { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateAccessToken() - .then(function(data) { - data.should.be.a.sha256(); - }) - .catch(should.fail); + const accessToken = await handler.generateAccessToken(); + accessToken.should.be.a.sha256(); }); - it('should support promises', function() { + it('should support promises', async function() { const model = { generateAccessToken: async function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal('long-hash-foo-bar'); }); - it('should support non-promises', function() { + it('should support non-promises', async function() { const model = { generateAccessToken: function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal('long-hash-foo-bar'); }); }); describe('generateRefreshToken()', function() { - it('should return a refresh token', function() { + it('should return a refresh token', async function() { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateRefreshToken() - .then(function(data) { - data.should.be.a.sha256(); - }) - .catch(should.fail); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.be.a.sha256(); }); - it('should support promises', function() { + it('should support promises', async function() { const model = { generateRefreshToken: async function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal('long-hash-foo-bar'); }); - it('should support non-promises', function() { + it('should support non-promises', async function() { const model = { generateRefreshToken: function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal('long-hash-foo-bar'); }); }); @@ -170,4 +163,64 @@ describe('AbstractGrantType integration', function() { handler.getScope(request).should.equal('foo'); }); }); + + describe('validateScope()', function () { + it('accepts the scope, if the model does not implement it', async function () { + const scope = 'some,scope,this,that'; + const user = { id: 123 }; + const client = { id: 456 }; + const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); + const validated = await handler.validateScope(user, client, scope); + validated.should.equal(scope); + }); + + it('accepts the scope, if the model accepts it', async function () { + const scope = 'some,scope,this,that'; + const user = { id: 123 }; + const client = { id: 456 }; + + const model = { + async validateScope (_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.equal(scope); + + return scope; + } + }; + const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); + const validated = await handler.validateScope(user, client, scope); + validated.should.equal(scope); + }); + + it('throws if the model rejects the scope', async function () { + const scope = 'some,scope,this,that'; + const user = { id: 123 }; + const client = { id: 456 }; + const returnTypes = [undefined, null, false, 0, '']; + + for (const type of returnTypes) { + const model = { + async validateScope (_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.equal(scope); + + return type; + } + }; + const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); + + try { + await handler.validateScope(user, client, scope); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: Requested scope is invalid'); + } + } + }); + }); }); 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 a4d69c40..f4598bde 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -75,9 +75,9 @@ describe('AuthorizationCodeGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -89,30 +89,33 @@ describe('AuthorizationCodeGrantType integration', function() { } }); - it('should throw an error if `client` is invalid', function() { - const client = {}; + it('should throw an error if `client` is invalid (not in code)', async function() { + const client = { id: 1234 }; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: function(code) { + code.should.equal(123456789); + return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); + const request = new Request({ body: { code: 123456789 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); + try { + await grantType.handle(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); + } }); it('should throw an error if `client` is missing', function() { - const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -128,18 +131,64 @@ describe('AuthorizationCodeGrantType integration', function() { it('should return a token', async function() { const client = { id: 'foobar' }; - const token = {}; + const scope = 'fooscope'; + const user = { name: 'foouser' }; + const codeDoc = { + authorizationCode: 12345, + expiresAt: new Date(new Date() * 2), + client, + user, + scope + }; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getAuthorizationCode: async function (code) { + code.should.equal('code-1234'); + + return codeDoc; + }, + revokeAuthorizationCode: async function (_codeDoc) { + _codeDoc.should.deep.equal(codeDoc); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return 'long-access-token-hash'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return 'long-refresh-token-hash'; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.accessToken.should.equal('long-access-token-hash'); + _token.refreshToken.should.equal('long-refresh-token-hash'); + _token.authorizationCode.should.equal(codeDoc.authorizationCode); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return _token; + }, }; - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - const data = await grantType.handle(request, client); - data.should.equal(token); + const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + const request = new Request({ body: { code: 'code-1234' }, headers: {}, method: {}, query: {} }); + + const token = await grantType.handle(request, client); + token.accessToken.should.equal('long-access-token-hash'); + token.refreshToken.should.equal('long-refresh-token-hash'); + token.authorizationCode.should.equal(codeDoc.authorizationCode); + token.accessTokenExpiresAt.should.be.instanceOf(Date); + token.refreshTokenExpiresAt.should.be.instanceOf(Date); }); it('should support promises', function() { @@ -173,9 +222,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the request body does not contain `code`', async function() { const client = {}; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -191,9 +240,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `code` is invalid', async function() { const client = {}; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 'ø倣‰' }, headers: {}, method: {}, query: {} }); @@ -210,9 +259,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authorizationCode` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() {}, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -226,136 +275,150 @@ describe('AuthorizationCodeGrantType integration', function() { } }); - it('should throw an error if `authorizationCode.client` is missing', function() { + it('should throw an error if `authorizationCode.client` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345 }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { return { authorizationCode: 12345 }; }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); + } }); - it('should throw an error if `authorizationCode.expiresAt` is missing', function() { + it('should throw an error if `authorizationCode.expiresAt` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { + return { authorizationCode: 12345, client: {}, user: {} }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `expiresAt` must be a Date instance'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `expiresAt` must be a Date instance'); + } }); - it('should throw an error if `authorizationCode.user` is missing', function() { + it('should throw an error if `authorizationCode.user` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { + return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); + } }); - it('should throw an error if the client id does not match', function() { + it('should throw an error if the client id does not match', async function() { const client = { id: 123 }; const model = { - getAuthorizationCode: function() { + getAuthorizationCode: async function() { return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, 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: authorization code is invalid'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code is invalid'); + } }); - it('should throw an error if the auth code is expired', function() { + it('should throw an error if the auth code is expired', async function() { const client = { id: 123 }; const date = new Date(new Date() / 2); const model = { - getAuthorizationCode: function() { + getAuthorizationCode: async function() { return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, 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: authorization code has expired'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code has expired'); + } }); - it('should throw an error if the `redirectUri` is invalid', function() { + it('should throw an error if the `redirectUri` is invalid (format)', async function() { const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', user: {} }; const client = { id: 'foobar' }; const model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { return authorizationCode; }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, 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: `redirect_uri` is not a valid URI'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); + } }); - it('should return an auth code', function() { - const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; + it('should return an auth code', async function() { + const authorizationCode = { + authorizationCode: 1234567, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), user: {} + }; const client = { id: 'foobar' }; const model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function(_code) { + _code.should.equal(12345); + return authorizationCode; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); + const code = await grantType.getAuthorizationCode(request, client); + code.should.deep.equal(authorizationCode); }); it('should support promises', function() { @@ -427,85 +490,113 @@ describe('AuthorizationCodeGrantType integration', function() { e.message.should.equal('Invalid request: `redirect_uri` is invalid'); } }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should revoke the auth code', function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + it('returns undefined and does not throw if `redirectUri` is valid', async function () { + const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; const model = { getAuthorizationCode: function() {}, revokeAuthorizationCode: function() { return true; }, saveToken: function() {} }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); + const request = new Request({ body: { code: 12345, redirect_uri: 'http://foo.bar' }, headers: {}, method: {}, query: {} }); + const value = grantType.validateRedirectUri(request, authorizationCode); + const isUndefined = value === undefined; + isUndefined.should.equal(true); }); + }); - it('should throw an error when the auth code is invalid', function() { + describe('revokeAuthorizationCode()', function() { + it('should revoke the auth code', async function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return false; }, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function(_code) { + _code.should.equal(authorizationCode); + return true; + }, + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(function(e) { + const data = await grantType.revokeAuthorizationCode(authorizationCode); + data.should.deep.equal(authorizationCode); + }); + + it('should throw an error when the auth code is invalid', async function() { + const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + const returnTypes = [false, null, undefined, 0, '']; + + for (const type of returnTypes) { + const model = { + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function(_code) { + _code.should.equal(authorizationCode); + return type; + }, + saveToken: () => should.fail() + }; + const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + + try { + await grantType.revokeAuthorizationCode(authorizationCode); + should.fail(); + } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); e.message.should.equal('Invalid grant: authorization code is invalid'); - }); + } + } }); it('should support promises', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; const model = { - getAuthorizationCode: function() {}, + getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: async function() { return true; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; const model = { - getAuthorizationCode: function() {}, + getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); }); describe('saveToken()', function() { - it('should save the token', function() { - const token = {}; + it('should save the token', async function() { + const token = { foo: 'bar' }; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: function(_token, _client= 'fallback', _user= 'fallback') { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.equal('foo'); + (_token.authorizationCode === undefined).should.equal(true); + _user.should.equal('fallback'); + _client.should.equal('fallback'); + return token; + }, + validateScope: function(_user= 'fallback', _client= 'fallback', _scope = 'fallback') { + _user.should.equal('fallback'); + _client.should.equal('fallback'); + _scope.should.equal('fallback'); + return 'foo'; + } }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.saveToken(); + data.should.equal(token); }); it('should support promises', function() { diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 1a70874e..a21b1a13 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -90,35 +90,50 @@ describe('ClientCredentialsGrantType integration', function() { } }); - it('should return a token', function() { + it('should return a token', async function() { const token = {}; + const client = { foo: 'bar' }; + const user = { name: 'foo' }; + const scope = 'fooscope'; + const model = { - getUserFromClient: async function(client) { - client.foo.should.equal('bar'); - return { id: '123'}; + getUserFromClient: async function(_client) { + _client.should.deep.equal(client); + return { ...user }; }, - saveToken: async function(_token, client, user) { - client.foo.should.equal('bar'); - user.id.should.equal('123'); + saveToken: async function(_token, _client, _user) { + _client.should.deep.equal(client); + _user.should.deep.equal(user); + _token.accessToken.should.equal('long-access-token-hash'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.scope.should.equal(scope); return token; }, - validateScope: function() { return 'foo'; } + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.equal(scope); + return 'long-access-token-hash'; + } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + const request = new Request({ body: { scope }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, { foo: 'bar' }) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.handle(request, client); + data.should.equal(token); }); it('should support promises', function() { const token = {}; const model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } + getUserFromClient: async function() { return {}; }, + saveToken: async function() { return token; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -143,7 +158,7 @@ describe('ClientCredentialsGrantType integration', function() { it('should throw an error if `user` is missing', function() { const model = { getUserFromClient: function() {}, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -160,7 +175,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUserFromClient: function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -176,7 +191,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUserFromClient: async function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -188,7 +203,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUserFromClient: function() {return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -198,26 +213,22 @@ describe('ClientCredentialsGrantType integration', function() { }); describe('saveToken()', function() { - it('should save the token', function() { + it('should save the token', async function() { const token = {}; const model = { - getUserFromClient: function() {}, + getUserFromClient: () => should.fail(), saveToken: function() { return token; }, validateScope: function() { return 'foo'; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.saveToken(token); + data.should.equal(token); }); it('should support promises', function() { const token = {}; const model = { - getUserFromClient: function() {}, + getUserFromClient:() => should.fail(), saveToken: async function() { return token; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); @@ -228,7 +239,7 @@ describe('ClientCredentialsGrantType integration', function() { it('should support non-promises', function() { const token = {}; const model = { - getUserFromClient: function() {}, + getUserFromClient: () => should.fail(), saveToken: function() { return token; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index 04452ee0..df1db899 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -45,7 +45,7 @@ describe('PasswordGrantType integration', function() { getUser: function() {} }; - new PasswordGrantType({ model: model }); + new PasswordGrantType({ model }); should.fail(); } catch (e) { @@ -58,10 +58,10 @@ describe('PasswordGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); try { await grantType.handle(); @@ -75,10 +75,10 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `client` is missing', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); try { await grantType.handle({}); @@ -90,32 +90,66 @@ describe('PasswordGrantType integration', function() { } }); - it('should return a token', function() { + it('should return a token', async function() { const client = { id: 'foobar' }; + const scope = 'baz'; const token = {}; + const user = { + id: 123456, + username: 'foo', + email: 'foo@example.com' + }; + const model = { - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } + getUser: async function(username, password) { + username.should.equal('foo'); + password.should.equal('bar'); + return user; + }, + validateScope: async function(_user, _client, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.equal(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.equal(scope); + return 'long-access-token-hash'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.equal(scope); + return 'long-refresh-token-hash'; + }, + saveToken: async function(_token, _client, _user) { + _client.should.equal(client); + _user.should.equal(user); + _token.accessToken.should.equal('long-access-token-hash'); + _token.refreshToken.should.equal('long-refresh-token-hash'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.handle(request, client); + data.should.equal(token); }); it('should support promises', async function() { const client = { id: 'foobar' }; const token = {}; const model = { - getUser: function() { return {}; }, + getUser: async function() { return {}; }, saveToken: async function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); const result = await grantType.handle(request, client); @@ -129,7 +163,7 @@ describe('PasswordGrantType integration', function() { getUser: function() { return {}; }, saveToken: function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); const result = await grantType.handle(request, client); @@ -140,10 +174,10 @@ describe('PasswordGrantType integration', function() { describe('getUser()', function() { it('should throw an error if the request body does not contain `username`', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -158,10 +192,10 @@ describe('PasswordGrantType integration', function() { it('should throw an error if the request body does not contain `password`', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); try { @@ -176,10 +210,10 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `username` is invalid', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, query: {} }); try { @@ -194,10 +228,10 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `password` is invalid', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, query: {} }); try { @@ -210,45 +244,47 @@ describe('PasswordGrantType integration', function() { } }); - it('should throw an error if `user` is missing', function() { + it('should throw an error if `user` is missing', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: async () => undefined, + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); + try { + await grantType.getUser(request); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + } }); - it('should return a user', function() { + it('should return a user', async function() { const user = { email: 'foo@bar.com' }; const model = { - getUser: function() { return user; }, - saveToken: function() {} + getUser: function(username, password) { + username.should.equal('foo'); + password.should.equal('bar'); + return user; + }, + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); + const data = await grantType.getUser(request); + data.should.equal(user); }); it('should support promises', function() { const user = { email: 'foo@bar.com' }; const model = { getUser: async function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); grantType.getUser(request).should.be.an.instanceOf(Promise); @@ -258,9 +294,9 @@ describe('PasswordGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUser: function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); grantType.getUser(request).should.be.an.instanceOf(Promise); @@ -268,29 +304,38 @@ describe('PasswordGrantType integration', function() { }); describe('saveToken()', function() { - it('should save the token', function() { + it('should save the token', async function() { const token = {}; const model = { - getUser: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getUser: () => should.fail(), + saveToken: async function(_token, _client = 'fallback', _user = 'fallback') { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.equal('foo'); + _client.should.equal('fallback'); + _user.should.equal('fallback'); + return token; + }, + validateScope: async function(_scope = 'fallback') { + _scope.should.equal('fallback'); + return 'foo'; + } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.saveToken(); + data.should.equal(token); }); it('should support promises', function() { const token = {}; const model = { - getUser: function() {}, + getUser: () => should.fail(), saveToken: async function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); @@ -298,10 +343,10 @@ describe('PasswordGrantType integration', function() { it('should support non-promises', function() { const token = {}; const model = { - getUser: function() {}, + getUser: () => should.fail(), saveToken: function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index cb717c76..aad03356 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -17,14 +17,16 @@ const should = require('chai').should(); describe('Server integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { - try { - new Server({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } + [null, undefined, {}].forEach(options => { + try { + new Server(options); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); }); it('should set the `model`', function() { diff --git a/test/unit/errors/oauth-error_test.js b/test/unit/errors/oauth-error_test.js index bad86f65..6d68a299 100644 --- a/test/unit/errors/oauth-error_test.js +++ b/test/unit/errors/oauth-error_test.js @@ -16,7 +16,7 @@ describe('OAuthError', function() { describe('constructor()', function() { it('should get `captureStackTrace`', function() { - const errorFn = function () { throw new OAuthError('test', {name: 'test_error'}); }; + const errorFn = function () { throw new OAuthError('test', {name: 'test_error', foo: 'bar'}); }; try { errorFn(); @@ -25,6 +25,8 @@ describe('OAuthError', function() { } catch (e) { e.should.be.an.instanceOf(OAuthError); + e.name.should.equal('test_error'); + e.foo.should.equal('bar'); e.message.should.equal('test'); e.code.should.equal(500); e.stack.should.not.be.null; @@ -34,4 +36,23 @@ describe('OAuthError', function() { } }); }); + it('supports undefined properties', function () { + const errorFn = function () { throw new OAuthError('test'); }; + + try { + errorFn(); + + should.fail(); + } catch (e) { + + e.should.be.an.instanceOf(OAuthError); + e.name.should.equal('Error'); + e.message.should.equal('test'); + e.code.should.equal(500); + e.stack.should.not.be.null; + e.stack.should.not.be.undefined; + e.stack.should.include('oauth-error_test.js'); + e.stack.should.include('40'); //error lineNUmber + } + }); }); diff --git a/test/unit/request_test.js b/test/unit/request_test.js index f292e2b4..0e23a419 100644 --- a/test/unit/request_test.js +++ b/test/unit/request_test.js @@ -5,6 +5,7 @@ */ const Request = require('../../lib/request'); +const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); const should = require('chai').should(); /** @@ -27,6 +28,24 @@ function generateBaseRequest() { } describe('Request', function() { + it('should throw on missing args', function () { + const args = [ + [undefined, InvalidArgumentError, 'Missing parameter: `headers`'], + [null, InvalidArgumentError, 'Missing parameter: `headers`'], + [{}, InvalidArgumentError, 'Missing parameter: `headers`'], + [{ headers: { }}, InvalidArgumentError, 'Missing parameter: `method`'], + [{ headers: {}, method: 'GET' }, InvalidArgumentError, 'Missing parameter: `query`'], + ]; + + args.forEach(([value, error, message]) => { + try { + new Request(value); + } catch (e) { + e.should.be.instanceOf(error); + e.message.should.equal(message); + } + }); + }); it('should instantiate with a basic request', function() { const originalRequest = generateBaseRequest(); From 9bf64c4b7d62541b6b1eac9baaf71c4d0c0bd889 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Aug 2023 09:00:02 +0200 Subject: [PATCH 3/7] tests(integration): deep cover refresh-token grant type --- lib/grant-types/refresh-token-grant-type.js | 4 +- .../refresh-token-grant-type_test.js | 427 ++++++++++-------- 2 files changed, 249 insertions(+), 182 deletions(-) diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index b9e89a27..5b5b6fd2 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -130,8 +130,8 @@ class RefreshTokenGrantType extends AbstractGrantType { const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); const token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, + accessToken, + accessTokenExpiresAt, scope: scope, }; diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index fede3776..316b8064 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -43,10 +43,10 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `revokeToken()`', function() { try { const model = { - getRefreshToken: function() {} + getRefreshToken: () => should.fail() }; - new RefreshTokenGrantType({ model: model }); + new RefreshTokenGrantType({ model }); should.fail(); } catch (e) { @@ -58,11 +58,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `saveToken()`', function() { try { const model = { - getRefreshToken: function() {}, - revokeToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail() }; - new RefreshTokenGrantType({ model: model }); + new RefreshTokenGrantType({ model }); should.fail(); } catch (e) { @@ -75,11 +75,11 @@ describe('RefreshTokenGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); try { await grantType.handle(); @@ -93,11 +93,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `client` is missing', async function() { const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -110,22 +110,51 @@ describe('RefreshTokenGrantType integration', function() { } }); - it('should return a token', function() { + it('should return a token', async function() { const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const token = { + accessToken: 'foo', + client: { id: 123 }, + user: { name: 'foo' }, + scope: 'read write', + refreshTokenExpiresAt: new Date( new Date() * 2) + }; const model = { - getRefreshToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return token; } + getRefreshToken: async function(_refreshToken) { + _refreshToken.should.equal('foobar_refresh'); + return token; + }, + revokeToken: async function(_token) { + _token.should.deep.equal(token); + return true; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _scope.should.equal('read write'); + return 'new-access-token'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _scope.should.equal('read write'); + return 'new-refresh-token'; + }, + saveToken: async function(_token, _client, _user) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _token.accessToken.should.equal('new-access-token'); + _token.refreshToken.should.equal('new-refresh-token'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); + const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); + const data = await grantType.handle(request, client); + data.should.equal(token); }); it('should support promises', function() { @@ -135,7 +164,7 @@ describe('RefreshTokenGrantType integration', function() { revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -144,11 +173,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return { accessToken: 'foo', client: {}, user: {} }; } + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, + revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, + saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -159,11 +188,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the `refreshToken` parameter is missing from the request body', async function() { const client = {}; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -176,92 +205,100 @@ describe('RefreshTokenGrantType integration', function() { } }); - it('should throw an error if `refreshToken` is not found', function() { + it('should throw an error if `refreshToken` is not found', async function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { return; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function() {} , + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + } }); - it('should throw an error if `refreshToken.client` is missing', function() { + it('should throw an error if `refreshToken.client` is missing', async function() { const client = {}; const model = { - getRefreshToken: function() { return {}; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function() { return {}; }, + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); + } }); - it('should throw an error if `refreshToken.user` is missing', function() { + it('should throw an error if `refreshToken.user` is missing', async function() { const client = {}; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); + } }); - it('should throw an error if the client id does not match', function() { + it('should throw an error if the client id does not match', async function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token was issued to another client'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token was issued to another client'); + } }); it('should throw an error if `refresh_token` contains invalid characters', async function() { const client = {}; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { client: { id: 456 }, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 'ø倣‰' }, headers: {}, method: {}, query: {} }); try { @@ -274,83 +311,100 @@ describe('RefreshTokenGrantType integration', function() { } }); - it('should throw an error if `refresh_token` is missing', function() { + it('should throw an error if `refresh_token` is missing', async function() { const client = {}; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token was issued to another client'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token was issued to another client'); + } }); - it('should throw an error if `refresh_token` is expired', function() { + it('should throw an error if `refresh_token` is expired', async function() { const client = { id: 123 }; const date = new Date(new Date() / 2); const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token has expired'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token has expired'); + } }); - it('should throw an error if `refreshTokenExpiresAt` is not a date value', function() { + it('should throw an error if `refreshTokenExpiresAt` is not a date value', async function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); + } }); - it('should return a token', function() { + it('should return a token', async function() { const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const token = { accessToken: 'foo', client: { id: 123 }, user: { name: 'foobar' } }; const model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function(_refreshToken) { + _refreshToken.should.equal('foobar_refresh'); + return token; + }, + revokeToken: async function(_token) { + _token.should.deep.equal(token); + return true; + }, + saveToken: async function(_token, _client, _user) { + _user.should.deep.equal(token.user); + _client.should.deep.equal(client); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); + const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.getRefreshToken(request, client); + data.should.equal(token); }); it('should support promises', function() { @@ -358,10 +412,10 @@ describe('RefreshTokenGrantType integration', function() { const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; const model = { getRefreshToken: async function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: async function() {}, + saveToken: async function() {} }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); @@ -371,11 +425,11 @@ describe('RefreshTokenGrantType integration', function() { const client = { id: 123 }; const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; const model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function() { return token; }, + revokeToken: async function() {}, + saveToken: async function() {} }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); @@ -383,46 +437,47 @@ describe('RefreshTokenGrantType integration', function() { }); describe('revokeToken()', function() { - it('should throw an error if the `token` is invalid', function() { + it('should throw an error if the `token` is invalid', async function() { const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: async () => {}, + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - grantType.revokeToken({}) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); + + try { + await grantType.revokeToken({}); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid or could not be revoked'); + } }); - it('should revoke the token', function() { + it('should revoke the token', async function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; const model = { - getRefreshToken: function() {}, - revokeToken: function() { return token; }, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: async function(_token) { + _token.should.deep.equal(token); + return token; + }, + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - return grantType.revokeToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.revokeToken(token); + data.should.equal(token); }); it('should support promises', function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; const model = { - getRefreshToken: function() {}, + getRefreshToken: () => should.fail(), revokeToken: async function() { return token; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); }); @@ -430,41 +485,53 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; const model = { - getRefreshToken: function() {}, + getRefreshToken: () => should.fail(), revokeToken: function() { return token; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); }); }); describe('saveToken()', function() { - it('should save the token', function() { - const token = {}; + it('should save the token', async function() { + const user = { name: 'foo' }; + const client = { id: 123465 }; + const scope = ['foo', 'bar']; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return token; } + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: async function(_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.scope.should.deep.equal(scope); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return { ..._token }; + } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); + + const data = await grantType.saveToken(user, client, scope); + data.accessToken.should.be.a.sha256(); + data.refreshToken.should.be.a.sha256(); + data.accessTokenExpiresAt.should.be.instanceOf(Date); + data.refreshTokenExpiresAt.should.be.instanceOf(Date); + data.scope.should.deep.equal(scope); }); it('should support promises', function() { const token = {}; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), saveToken: async function() { return token; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); @@ -472,11 +539,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const token = {}; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), saveToken: function() { return token; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); From 92cc613a7c5a6e73b63d4e11461aeb58f1044dac Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Aug 2023 09:02:18 +0200 Subject: [PATCH 4/7] tests(integration): deep cover authenticte handler --- .../handlers/authenticate-handler_test.js | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index c069ed9f..712dd7cd 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -101,16 +101,38 @@ describe('AuthenticateHandler integration', function() { }); describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + it('should throw an error if `request` is missing or not a Request instance', async function() { + class Request {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Request()]; + for (const request of values) { + const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + + try { + await handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: `request` must be an instance of Request'); + } + } + }); - try { - await handler.handle(); + it('should throw an error if `response` is missing or not a Response instance', async function() { + class Response {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Response()]; + const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); + for (const response of values) { + const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + try { + await handler.handle(request, response); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: `response` must be an instance of Response'); + } } }); From 323c91b03a8ebce4593b753173ae0441d4c2b9cf Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Aug 2023 09:02:40 +0200 Subject: [PATCH 5/7] tests(unit): improve coverage for TokenModel --- test/unit/models/token-model_test.js | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 7dcac615..e9039386 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -1,4 +1,5 @@ const TokenModel = require('../../../lib/models/token-model'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); const should = require('chai').should(); /** * Test `Server`. @@ -6,6 +7,101 @@ const should = require('chai').should(); describe('Model', function() { describe('constructor()', function() { + it('throws, if data is empty', function () { + try { + new TokenModel(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + it('throws, if `accessToken` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + client: 'bar', + user: 'tar', + accessTokenExpiresAt: atExpiresAt + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + it('throws, if `client` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + user: 'tar', + accessTokenExpiresAt: atExpiresAt + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + it('throws, if `user` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + client: 'bar', + accessTokenExpiresAt: atExpiresAt + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `user`'); + } + }); + it('throws, if `accessTokenExpiresAt` is not a Date', function () { + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + accessTokenExpiresAt: '11/10/2023' + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `accessTokenExpiresAt`'); + } + }); + it('throws, if `refreshTokenExpiresAt` is not a Date', function () { + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + refreshTokenExpiresAt: '11/10/2023' + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `refreshTokenExpiresAt`'); + } + }); it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', function() { const atExpiresAt = new Date(); atExpiresAt.setHours(new Date().getHours() + 1); From fde0915bdef4f30955a566dbd19f6950c946c69b Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Aug 2023 15:55:14 +0200 Subject: [PATCH 6/7] tests(unit): improve coverage for crypto util --- test/unit/utils/crypto-util_test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test/unit/utils/crypto-util_test.js diff --git a/test/unit/utils/crypto-util_test.js b/test/unit/utils/crypto-util_test.js new file mode 100644 index 00000000..7c3057e0 --- /dev/null +++ b/test/unit/utils/crypto-util_test.js @@ -0,0 +1,18 @@ +const cryptoUtil = require('../../../lib/utils/crypto-util'); +require('chai').should(); + +describe(cryptoUtil.createHash.name, function () { + it('creates a hash by given algorithm', function () { + const data = 'client-credentials-grant'; + const hash = cryptoUtil.createHash({ data, encoding: 'hex' }); + hash.should.equal('072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60'); + }); + it('should throw if data is missing', function () { + try { + cryptoUtil.createHash({}); + } catch (e) { + e.should.be.instanceOf(TypeError); + e.message.should.include('he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.'); + } + }); +}); From c0593ef7d53874130317e9538296973cb2646ad6 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Aug 2023 15:55:47 +0200 Subject: [PATCH 7/7] 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);