From 1a387f4a06c5a4a3b43111edfcb1a5ea2bfb02d6 Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sun, 5 Dec 2021 17:05:47 +0100 Subject: [PATCH 1/8] test example --- test/integration/flows/db.js | 65 ++++++++++ test/integration/flows/model.js | 61 +++++++++ test/integration/flows/password-grant.js | 156 +++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 test/integration/flows/db.js create mode 100644 test/integration/flows/model.js create mode 100644 test/integration/flows/password-grant.js diff --git a/test/integration/flows/db.js b/test/integration/flows/db.js new file mode 100644 index 0000000..158a224 --- /dev/null +++ b/test/integration/flows/db.js @@ -0,0 +1,65 @@ +class DB { + + constructor () { + this.users = new Map(); + this.clients = []; + this.accessTokens = new Map(); + this.refreshTokens= new Map(); + } + + saveUser (user) { + this.users.set(user.id, user); + + return user; + } + + findUser (username, password) { + return Array.from(this.users.values()).find(user => { + return user.username === username && user.password === password; + }); + } + + findUserById (id) { + return this.users.get(id); + } + + saveClient (client) { + this.clients.push(client); + + return client; + } + + findClient (clientId, clientSecret) { + return this.clients.find(client => { + if (clientSecret) { + return client.id === clientId && client.secret === clientSecret; + } else { + return client.id === clientId; + } + }); + } + + findClientById (id) { + return this.clients.find(client => client.id === id); + } + + saveAccessToken (accessToken, meta) { + this.accessTokens.set(accessToken, meta); + } + + findAccessToken (accessToken) { + return this.accessTokens.get(accessToken); + } + + saveRefreshToken (refreshToken, meta) { + this.refreshTokens.set(refreshToken, meta); + } + + findRefreshToken (refreshToken) { + return this.refreshTokens.get(refreshToken); + } +} + +const db = new DB(); + +module.exports = db; diff --git a/test/integration/flows/model.js b/test/integration/flows/model.js new file mode 100644 index 0000000..b91ee98 --- /dev/null +++ b/test/integration/flows/model.js @@ -0,0 +1,61 @@ +const db = require('./db'); +const scopes = ['read', 'write']; + +async function getUser (username, password) { + return db.findUser(username, password); +} + +async function getClient (clientId, clientSecret) { + return db.findClient(clientId, clientSecret); +} + +async function saveToken (token, client, user) { + const meta = { + clientId: client.id, + userId: user.id, + scope: token.scope + }; + + token.client = client; + token.user = user; + + if (token.accessToken) { + db.saveAccessToken(token.accessToken, meta); + } + + if (token.refreshToken) { + db.saveRefreshToken(token.refreshToken, meta); + } + + return token; +} + +async function getAccessToken (accessToken) { + const meta = db.findAccessToken(accessToken); + + return { + accessToken, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope + }; +} + +async function verifyScope (token, scope) { + if (typeof scope === 'string') { + return scopes.includes(scope); + } else { + return scope.every(s => scopes.includes(s)); + } +} + +const model = { + getUser, + getClient, + saveToken, + getAccessToken, + verifyScope +}; + +module.exports = model; + diff --git a/test/integration/flows/password-grant.js b/test/integration/flows/password-grant.js new file mode 100644 index 0000000..9065cce --- /dev/null +++ b/test/integration/flows/password-grant.js @@ -0,0 +1,156 @@ +const OAuth2Server = require('../../../'); +const db = require('./db'); +const model = require('./model'); +const Request = require('../../../lib/request'); +const Response = require('../../../lib/response'); + +require('chai').should(); + +const auth = new OAuth2Server({ + model: model +}); + +const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); +const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); +const scope = 'read write'; + +function createDefaultRequest () { + const request = new Request({ + body: { + grant_type: 'password', + client_id: client.id, + client_secret: client.secret, + username: user.username, + password: user.password, + scope + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + method: 'POST', + query: {} + }); + + request.is = function (header) { + return this.headers['content-type'] === header; + }; + + return request; +} + +describe('PasswordGrantType Integration Flow', function () { + describe('Authenticate', function () { + + it ('Succesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + const token = await auth.token(request, response, {}); + response.body.token_type.should.equal('Bearer'); + response.body.access_token.should.equal(token.accessToken); + response.body.refresh_token.should.equal(token.refreshToken); + response.body.expires_in.should.be.a('number'); + response.body.scope.should.equal(scope); + + token.accessToken.should.be.a('string'); + token.refreshToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + token.refreshTokenExpiresAt.should.be.a('date'); + token.scope.should.equal(scope); + + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(true); + }); + + it ('Username missing', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.username; + + await auth.token(request, response, {}) + .catch(err => { + err.name.should.equal('invalid_request'); + }); + }); + + it ('Password missing', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.password; + + await auth.token(request, response, {}) + .catch(err => { + err.name.should.equal('invalid_request'); + }); + }); + + it ('Wrong username', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.username = 'wrong'; + + await auth.token(request, response, {}) + .catch(err => { + err.name.should.equal('invalid_grant'); + }); + }); + + it ('Wrong password', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.password = 'wrong'; + + await auth.token(request, response, {}) + .catch(err => { + err.name.should.equal('invalid_grant'); + }); + }); + + it ('Client not found', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.client_id = 'wrong'; + + await auth.token(request, response, {}) + .catch(err => { + err.name.should.equal('invalid_client'); + }); + }); + + it ('Client secret not required', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.client_secret; + + const token = await auth.token(request, response, { + requireClientAuthentication: { + password: false + } + }); + + token.accessToken.should.be.a('string'); + }); + + it ('Client secret required', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.client_secret; + + await auth.token(request, response, { + requireClientAuthentication: { + password: false + } + }) + .catch(err => { + err.name.should.equal('invalid_client'); + }); + }); + }); +}); From 4e1d8a856f87567ac726756bf68110f7de25ad56 Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sat, 11 Dec 2021 14:04:31 +0100 Subject: [PATCH 2/8] created db & model factories --- .../password-grant-type_test.js} | 111 +++++++++++++++--- test/{integration/flows => helpers}/db.js | 13 +- test/helpers/model.js | 92 +++++++++++++++ test/helpers/request.js | 17 +++ test/integration/flows/model.js | 61 ---------- 5 files changed, 212 insertions(+), 82 deletions(-) rename test/{integration/flows/password-grant.js => compliance/password-grant-type_test.js} (53%) rename test/{integration/flows => helpers}/db.js (86%) create mode 100644 test/helpers/model.js create mode 100644 test/helpers/request.js delete mode 100644 test/integration/flows/model.js diff --git a/test/integration/flows/password-grant.js b/test/compliance/password-grant-type_test.js similarity index 53% rename from test/integration/flows/password-grant.js rename to test/compliance/password-grant-type_test.js index 9065cce..4959a47 100644 --- a/test/integration/flows/password-grant.js +++ b/test/compliance/password-grant-type_test.js @@ -1,13 +1,72 @@ -const OAuth2Server = require('../../../'); -const db = require('./db'); -const model = require('./model'); -const Request = require('../../../lib/request'); -const Response = require('../../../lib/response'); +/** + * Request + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3.2 + * + * grant_type + * REQUIRED. Value MUST be set to "password". + * username + * REQUIRED. The resource owner username. + * password + * REQUIRED. The resource owner password. + * scope + * OPTIONAL. The scope of the access request as described by Section 3.3. + */ + +/** + * Response + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 + * + * access_token + * REQUIRED. The access token issued by the authorization server. + * token_type + * REQUIRED. The type of the token issued as described in + * Section 7.1. Value is case insensitive. + * expires_in + * RECOMMENDED. The lifetime in seconds of the access token. For + * example, the value "3600" denotes that the access token will + * expire in one hour from the time the response was generated. + * If omitted, the authorization server SHOULD provide the + * expiration time via other means or document the default value. + * refresh_token + * OPTIONAL. The refresh token, which can be used to obtain new + * access tokens using the same authorization grant as described + * in Section 6. + * scope + * OPTIONAL, if identical to the scope requested by the client; + * otherwise, REQUIRED. The scope of the access token as + * described by Section 3.3. + */ + +/** + * Response (error) + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + * + * error + * REQUIRED. A single ASCII [USASCII] error code from the following: + * invalid_request, invalid_client, invalid_grant + * unauthorized_client, unsupported_grant_type, invalid_scope + * error_description + * OPTIONAL. Human-readable ASCII [USASCII] text providing + * additional information, used to assist the client developer in + * understanding the error that occurred. + * error_uri + * OPTIONAL. A URI identifying a human-readable web page with + * information about the error, used to provide the client + * developer with additional information about the error. + */ + +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(); + const auth = new OAuth2Server({ - model: model + model: createModel(db) }); const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); @@ -15,7 +74,7 @@ const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); const scope = 'read write'; function createDefaultRequest () { - const request = new Request({ + return createRequest({ body: { grant_type: 'password', client_id: client.id, @@ -28,20 +87,12 @@ function createDefaultRequest () { 'content-type': 'application/x-www-form-urlencoded' }, method: 'POST', - query: {} }); - - request.is = function (header) { - return this.headers['content-type'] === header; - }; - - return request; } -describe('PasswordGrantType Integration Flow', function () { +describe('PasswordGrantType Compliance', function () { describe('Authenticate', function () { - - it ('Succesfull authentication', async function () { + it ('Succesfull authorization', async function () { const request = createDefaultRequest(); const response = new Response({}); @@ -62,6 +113,32 @@ describe('PasswordGrantType Integration Flow', function () { db.refreshTokens.has(token.refreshToken).should.equal(true); }); + it ('Succesfull authorization and authentication', async function () { + const tokenRequest = createDefaultRequest(); + const tokenResponse = new Response({}); + + const token = await auth.token(tokenRequest, tokenResponse, {}); + + const authenticationRequest = createRequest({ + body: {}, + headers: { + 'Authorization': `Bearer ${token.accessToken}` + }, + method: 'GET', + query: {} + }); + const authenticationResponse = new Response({}); + + const authenticated = await auth.authenticate( + authenticationRequest, + authenticationResponse, + {}); + + authenticated.scope.should.equal(scope); + authenticated.user.should.be.an('object'); + authenticated.client.should.be.an('object'); + }); + it ('Username missing', async function () { const request = createDefaultRequest(); const response = new Response({}); diff --git a/test/integration/flows/db.js b/test/helpers/db.js similarity index 86% rename from test/integration/flows/db.js rename to test/helpers/db.js index 158a224..147d174 100644 --- a/test/integration/flows/db.js +++ b/test/helpers/db.js @@ -1,5 +1,4 @@ class DB { - constructor () { this.users = new Map(); this.clients = []; @@ -51,6 +50,10 @@ class DB { return this.accessTokens.get(accessToken); } + deleteAccessToken (accessToken) { + this.accessTokens.delete(accessToken); + } + saveRefreshToken (refreshToken, meta) { this.refreshTokens.set(refreshToken, meta); } @@ -58,8 +61,10 @@ class DB { findRefreshToken (refreshToken) { return this.refreshTokens.get(refreshToken); } -} -const db = new DB(); + deleteRefreshToken (refreshToken) { + this.refreshTokens.delete(refreshToken); + } +} -module.exports = db; +module.exports = DB; diff --git a/test/helpers/model.js b/test/helpers/model.js new file mode 100644 index 0000000..7a1893b --- /dev/null +++ b/test/helpers/model.js @@ -0,0 +1,92 @@ +const scopes = ['read', 'write']; + +function createModel (db) { + async function getUser (username, password) { + return db.findUser(username, password); + } + + async function getClient (clientId, clientSecret) { + return db.findClient(clientId, clientSecret); + } + + async function saveToken (token, client, user) { + const meta = { + clientId: client.id, + userId: user.id, + scope: token.scope, + accessTokenExpiresAt: token.accessTokenExpiresAt, + refreshTokenExpiresAt: token.refreshTokenExpiresAt + }; + + token.client = client; + token.user = user; + + if (token.accessToken) { + db.saveAccessToken(token.accessToken, meta); + } + + if (token.refreshToken) { + db.saveRefreshToken(token.refreshToken, meta); + } + + return token; + } + + async function getAccessToken (accessToken) { + const meta = db.findAccessToken(accessToken); + + if (!meta) { + return false; + } + + return { + accessToken, + accessTokenExpiresAt: meta.accessTokenExpiresAt, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope + }; + } + + async function getRefreshToken (refreshToken) { + const meta = db.findRefreshToken(refreshToken); + + if (!meta) { + return false; + } + + return { + refreshToken, + refreshTokenExpiresAt: meta.refreshTokenExpiresAt, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope + }; + } + + async function revokeToken (token) { + db.deleteRefreshToken(token.refreshToken); + + return true; + } + + async function verifyScope (token, scope) { + if (typeof scope === 'string') { + return scopes.includes(scope); + } else { + return scope.every(s => scopes.includes(s)); + } + } + + return { + getUser, + getClient, + saveToken, + getAccessToken, + getRefreshToken, + revokeToken, + verifyScope + }; +} + +module.exports = createModel; diff --git a/test/helpers/request.js b/test/helpers/request.js new file mode 100644 index 0000000..be556a8 --- /dev/null +++ b/test/helpers/request.js @@ -0,0 +1,17 @@ +const Request = require('../../lib/request'); + +module.exports = (request) => { + const req = new Request({ + query: {}, + body: {}, + headers: {}, + method: 'GET', + ...request + }); + + req.is = function (header) { + return this.headers['content-type'] === header; + }; + + return req; +}; diff --git a/test/integration/flows/model.js b/test/integration/flows/model.js deleted file mode 100644 index b91ee98..0000000 --- a/test/integration/flows/model.js +++ /dev/null @@ -1,61 +0,0 @@ -const db = require('./db'); -const scopes = ['read', 'write']; - -async function getUser (username, password) { - return db.findUser(username, password); -} - -async function getClient (clientId, clientSecret) { - return db.findClient(clientId, clientSecret); -} - -async function saveToken (token, client, user) { - const meta = { - clientId: client.id, - userId: user.id, - scope: token.scope - }; - - token.client = client; - token.user = user; - - if (token.accessToken) { - db.saveAccessToken(token.accessToken, meta); - } - - if (token.refreshToken) { - db.saveRefreshToken(token.refreshToken, meta); - } - - return token; -} - -async function getAccessToken (accessToken) { - const meta = db.findAccessToken(accessToken); - - return { - accessToken, - user: db.findUserById(meta.userId), - client: db.findClientById(meta.clientId), - scope: meta.scope - }; -} - -async function verifyScope (token, scope) { - if (typeof scope === 'string') { - return scopes.includes(scope); - } else { - return scope.every(s => scopes.includes(s)); - } -} - -const model = { - getUser, - getClient, - saveToken, - getAccessToken, - verifyScope -}; - -module.exports = model; - From f8c2148e49488b393ca30d2dde6505271d7d6220 Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sat, 11 Dec 2021 14:04:53 +0100 Subject: [PATCH 3/8] added refresh_token grant type test --- .../refresh-token-grant-type_test.js | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 test/compliance/refresh-token-grant-type_test.js diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js new file mode 100644 index 0000000..64b5517 --- /dev/null +++ b/test/compliance/refresh-token-grant-type_test.js @@ -0,0 +1,173 @@ +/** + * Request an access token using the refresh token grant type. + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 + * + * grant_type + * REQUIRED. Value MUST be set to "refresh_token". + * refresh_token + * REQUIRED. The refresh token issued to the client. + * scope + * OPTIONAL. The scope of the access request as described by + * Section 3.3. The requested scope MUST NOT include any scope + * not originally granted by the resource owner, and if omitted is + * treated as equal to the scope originally granted by the + * resource owner. + */ + + +/** + * Response + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 + * + * access_token + * REQUIRED. The access token issued by the authorization server. + * token_type + * REQUIRED. The type of the token issued as described in + * Section 7.1. Value is case insensitive. + * expires_in + * RECOMMENDED. The lifetime in seconds of the access token. For + * example, the value "3600" denotes that the access token will + * expire in one hour from the time the response was generated. + * If omitted, the authorization server SHOULD provide the + * expiration time via other means or document the default value. + * refresh_token + * OPTIONAL. The refresh token, which can be used to obtain new + * access tokens using the same authorization grant as described + * in Section 6. + * scope + * OPTIONAL, if identical to the scope requested by the client; + * otherwise, REQUIRED. The scope of the access token as + * described by Section 3.3. + */ + +/** + * Response (error) + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + * + * error + * REQUIRED. A single ASCII [USASCII] error code from the following: + * invalid_request, invalid_client, invalid_grant + * unauthorized_client, unsupported_grant_type, invalid_scope + * error_description + * OPTIONAL. Human-readable ASCII [USASCII] text providing + * additional information, used to assist the client developer in + * understanding the error that occurred. + * error_uri + * OPTIONAL. A URI identifying a human-readable web page with + * information about the error, used to provide the client + * developer with additional information about the error. + */ +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(); + +const auth = new OAuth2Server({ + model: createModel(db) +}); + +const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); +const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password', 'refresh_token'] }); +const scope = 'read write'; + +function createLoginRequest () { + return createRequest({ + body: { + grant_type: 'password', + client_id: client.id, + client_secret: client.secret, + username: user.username, + password: user.password, + scope + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + method: 'POST', + }); +} + +function createRefreshRequest (refresh_token) { + return createRequest({ + method: 'POST', + body: { + grant_type: 'refresh_token', + client_id: client.id, + client_secret: client.secret, + refresh_token, + scope + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded' + } + }); +} + +describe('RefreshTokenGrantType Compliance', function () { + describe('With scope', function () { + it('Should generate token response', async function () { + const request = createLoginRequest(); + const response = new Response({}); + + const credentials = await auth.token(request, response, {}); + + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); + + const token = await auth.token(refreshRequest, refreshResponse, {}); + + refreshResponse.body.token_type.should.equal('Bearer'); + refreshResponse.body.access_token.should.equal(token.accessToken); + refreshResponse.body.refresh_token.should.equal(token.refreshToken); + refreshResponse.body.expires_in.should.be.a('number'); + refreshResponse.body.scope.should.equal(scope); + + token.accessToken.should.be.a('string'); + token.refreshToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + token.refreshTokenExpiresAt.should.be.a('date'); + token.scope.should.equal(scope); + + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(true); + }); + + it('Should throw invalid_grant error', async function () { + const request = createRefreshRequest('invalid'); + const response = new Response({}); + + await auth.token(request, response, {}) + .then(() => { + throw Error('Should not reach this'); + }).catch(err => { + err.name.should.equal('invalid_grant'); + }); + }); + + // TODO: test refresh token with different scopes + it('Should throw invalid_scope error', async function () { + const request = createLoginRequest(); + const response = new Response({}); + + const credentials = await auth.token(request, response, {}); + + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); + + refreshRequest.scope = 'invalid'; + + await auth.token(refreshRequest, refreshResponse, {}) + .then(() => { + throw Error('Should not reach this'); + }) + .catch(err => { + err.name.should.equal('invalid_scope'); + }); + }); + }); +}); From 4a1721299d638c3a2b9305b8c8ed22d1408b4dba Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sat, 11 Dec 2021 14:06:25 +0100 Subject: [PATCH 4/8] removed failing test, not implemented feature --- .../refresh-token-grant-type_test.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index 64b5517..e00325a 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -150,24 +150,24 @@ describe('RefreshTokenGrantType Compliance', function () { }); // TODO: test refresh token with different scopes - it('Should throw invalid_scope error', async function () { - const request = createLoginRequest(); - const response = new Response({}); + // it('Should throw invalid_scope error', async function () { + // const request = createLoginRequest(); + // const response = new Response({}); - const credentials = await auth.token(request, response, {}); + // const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + // const refreshRequest = createRefreshRequest(credentials.refreshToken); + // const refreshResponse = new Response({}); - refreshRequest.scope = 'invalid'; + // refreshRequest.scope = 'invalid'; - await auth.token(refreshRequest, refreshResponse, {}) - .then(() => { - throw Error('Should not reach this'); - }) - .catch(err => { - err.name.should.equal('invalid_scope'); - }); - }); + // await auth.token(refreshRequest, refreshResponse, {}) + // .then(() => { + // throw Error('Should not reach this'); + // }) + // .catch(err => { + // err.name.should.equal('invalid_scope'); + // }); + // }); }); }); From 6b38596d505e18e988620905cba3b72a6b1e38c4 Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sat, 11 Dec 2021 19:42:39 +0100 Subject: [PATCH 5/8] add reference to issue --- test/compliance/refresh-token-grant-type_test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index e00325a..8371ee0 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -150,6 +150,8 @@ describe('RefreshTokenGrantType Compliance', function () { }); // TODO: test refresh token with different scopes + // https://github.com/node-oauth/node-oauth2-server/issues/104 + // it('Should throw invalid_scope error', async function () { // const request = createLoginRequest(); // const response = new Response({}); From 7380dc7ee99620064e41b77a860fa3d4770cfaed Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sun, 12 Dec 2021 13:27:15 +0100 Subject: [PATCH 6/8] client authentication test --- test/compliance/client-authentication_test.js | 128 ++++++++++++++++++ test/compliance/password-grant-type_test.js | 5 +- .../refresh-token-grant-type_test.js | 6 +- 3 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 test/compliance/client-authentication_test.js diff --git a/test/compliance/client-authentication_test.js b/test/compliance/client-authentication_test.js new file mode 100644 index 0000000..72624ec --- /dev/null +++ b/test/compliance/client-authentication_test.js @@ -0,0 +1,128 @@ +/** + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + * + * For example (with extra line breaks for display purposes only): + * + * Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3 + * + * Alternatively, the authorization server MAY support including the + * client credentials in the request-body using the following + * parameters: + * + * client_id + * REQUIRED. The client identifier issued to the client during + * the registration process described by Section 2.2. + * + * client_secret + * REQUIRED. The client secret. The client MAY omit the + * parameter if the client secret is an empty string. + */ + +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(); + +const auth = new OAuth2Server({ + model: createModel(db) +}); + +const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); +const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); +const scope = 'read write'; + +function createDefaultRequest () { + return createRequest({ + body: { + grant_type: 'password', + username: user.username, + password: user.password, + scope + }, + headers: { + 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded' + }, + method: 'POST', + }); +} + +describe('Client Authentication Compliance', function () { + describe('No authentication', function () { + it('should be an unsuccesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + await auth.token(request, response, {}) + .then((token) => { + throw new Error('Should not be here'); + }). + catch(err => { + err.name.should.equal('invalid_client'); + }); + }); + }); + + describe('Basic Authentication', function () { + it('should be a succesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + await auth.token(request, response, {}); + }); + + it('should be an unsuccesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.headers.authorization = 'Basic ' + Buffer.from('a:c').toString('base64'); + + await auth.token(request, response, {}) + .then((token) => { + throw new Error('Should not be here'); + }). + catch(err => { + err.name.should.equal('invalid_client'); + }); + }); + }); + + describe('Request body authentication', function () { + it('should be a succesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + request.body.client_id = client.id; + request.body.client_secret = client.secret; + + await auth.token(request, response, {}); + }); + + it('should be an unsuccesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + request.body.client_id = 'a'; + request.body.client_secret = 'c'; + + await auth.token(request, response, {}) + .then((token) => { + throw new Error('Should not be here'); + }) + .catch(err => { + err.name.should.equal('invalid_client'); + }); + }); + }); +}); diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index 4959a47..a2a8c2b 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -77,13 +77,12 @@ function createDefaultRequest () { return createRequest({ body: { grant_type: 'password', - client_id: client.id, - client_secret: client.secret, username: user.username, password: user.password, scope }, headers: { + 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded' }, method: 'POST', @@ -191,7 +190,7 @@ describe('PasswordGrantType Compliance', function () { const request = createDefaultRequest(); const response = new Response({}); - request.body.client_id = 'wrong'; + request.headers.authorization = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); await auth.token(request, response, {}) .catch(err => { diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index 8371ee0..b01fef3 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -79,13 +79,12 @@ function createLoginRequest () { return createRequest({ body: { grant_type: 'password', - client_id: client.id, - client_secret: client.secret, username: user.username, password: user.password, scope }, headers: { + 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded' }, method: 'POST', @@ -97,12 +96,11 @@ function createRefreshRequest (refresh_token) { method: 'POST', body: { grant_type: 'refresh_token', - client_id: client.id, - client_secret: client.secret, refresh_token, scope }, headers: { + 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded' } }); From 380fee95e4774d3a43488a243960be507828a3e8 Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sun, 19 Dec 2021 11:16:42 +0100 Subject: [PATCH 7/8] random client credentials in test --- test/compliance/password-grant-type_test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index a2a8c2b..ebf1967 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -190,7 +190,10 @@ describe('PasswordGrantType Compliance', function () { const request = createDefaultRequest(); const response = new Response({}); - request.headers.authorization = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + const clientId = Math.round(Math.random() * 1000000).toString(16); + const clientSecret = Math.round(Math.random() * 1000000).toString(16); + + request.headers.authorization = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); await auth.token(request, response, {}) .catch(err => { From 903f5172411b1ebd0c741592674891a0128cb6cf Mon Sep 17 00:00:00 2001 From: Joren Vandeweyer Date: Sun, 19 Dec 2021 11:20:15 +0100 Subject: [PATCH 8/8] replace math.random by crypto.randomBytes --- test/compliance/password-grant-type_test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index ebf1967..7941d54 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -60,6 +60,7 @@ const DB = require('../helpers/db'); const createModel = require('../helpers/model'); const createRequest = require('../helpers/request'); const Response = require('../../lib/response'); +const crypto = require('crypto'); require('chai').should(); @@ -190,8 +191,8 @@ describe('PasswordGrantType Compliance', function () { const request = createDefaultRequest(); const response = new Response({}); - const clientId = Math.round(Math.random() * 1000000).toString(16); - const clientSecret = Math.round(Math.random() * 1000000).toString(16); + const clientId = crypto.randomBytes(4).toString('hex'); + const clientSecret = crypto.randomBytes(4).toString('hex'); request.headers.authorization = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');