Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bedrock/authn-token

Package Overview
Dependencies
Maintainers
5
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bedrock/authn-token - npm Package Compare versions

Comparing version 10.2.0 to 10.3.0

test/mocha/06-setAuthenticationRequirements.js

13

CHANGELOG.md
# bedrock-authn-token ChangeLog
## 10.3.0 - 2022-12-11
### Changed
- Require `@bedrock/account@8.2` to get latest update API features.
- Use `@bedrock/account` APIs internally to reduce possibility for
breakage if the internals of `@bedrock/account` change.
### Removed
- Non-breaking removal of internal `explain` flags from APIs that
were directly accessing `@bedrock/account` database collections.
- Non-breaking removal of peer dependency on `@bedrock/mongodb` because
the database is no longer directly accessed.
## 10.2.0 - 2022-05-28

@@ -4,0 +17,0 @@

64

lib/authenticationMethods.js

@@ -6,3 +6,2 @@ /*!

import * as brAccount from '@bedrock/account';
import * as database from '@bedrock/mongodb';
import assert from 'assert-plus';

@@ -39,9 +38,8 @@

* that must first be satisfied before this token can be verified.
* @param {boolean} [options.explain=false] - An optional explain boolean.
*
* @returns {Promise | ExplainObject} - Returns a Promise that resolves once
* the operation completes or an ExplainObject if `explain=true`.
* @returns {Promise} - Returns a Promise that resolves once the operation
* completes.
*/
export async function setAuthenticationRequirements({
accountId, requiredAuthenticationMethods, explain = false
accountId, requiredAuthenticationMethods
} = {}) {

@@ -51,26 +49,34 @@ assert.string(accountId, 'accountId');

// add token to meta
const query = {id: database.hash(accountId)};
// ignore concurrent account updates when setting required methods
while(true) {
try {
// get existing record meta
const record = await brAccount.get({id: accountId});
if(explain) {
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(query).limit(1);
return cursor.explain('executionStats');
}
const result = await database.collections.account.updateOne(
query, {
$set: {
'meta.bedrock-authn-token.requiredAuthenticationMethods':
requiredAuthenticationMethods
// add required authentication methods to meta
const meta = {
...record.meta,
sequence: record.meta.sequence + 1
};
if(!meta['bedrock-authn-token']) {
meta['bedrock-authn-token'] = {requiredAuthenticationMethods};
} else {
meta['bedrock-authn-token'].requiredAuthenticationMethods =
requiredAuthenticationMethods;
}
});
if(result.result.n === 0) {
// check if account exists
if(!await brAccount.exists({id: accountId})) {
return await brAccount.update({id: accountId, meta});
} catch(e) {
if(e.name === 'InvalidStateError') {
// loop to try again; concurrently updated
continue;
}
// differentiate not found error from other errors
const notFound = e.name === 'NotFoundError';
throw new BedrockError(
'Could not set account required authentication methods. ' +
'Account not found.',
'NotFoundError', {httpStatusCode: 404, public: true});
'Could not set account required authentication methods.' +
notFound ? ' Account not found.' : '', {
name: notFound ? 'NotFoundError' : 'OperationError',
details: {httpStatusCode: notFound ? 404 : 500, public: true},
cause: e
});
}

@@ -136,7 +142,1 @@ }

}
/**
* An object containing information on the query plan.
*
* @typedef {object} ExplainObject
*/

@@ -5,3 +5,3 @@ /*!

import * as bedrock from '@bedrock/core';
import * as database from '@bedrock/mongodb';
import * as brAccount from '@bedrock/account';
import assert from 'assert-plus';

@@ -17,8 +17,2 @@ import {fastHash} from './helpers.js';

/**
* An object containing information on the query plan.
*
* @typedef {object} ExplainObject
*/
/**
* Sets a token client, i.e., a client that may submit tokens, for an account.

@@ -64,9 +58,8 @@ *

* user, `false` not to.
* @param {boolean} [options.explain=false] - An optional explain boolean.
*
* @returns {Promise | ExplainObject} - Returns a Promise that resolves once
* the operation completes or an ExplainObject if `explain=true`.
* @returns {Promise} - Returns a Promise that resolves once the operation
* completes.
*/
export async function set({
accountId, email, clientId, authenticated, notify = true, explain = false
accountId, email, clientId, authenticated, notify = true
} = {}) {

@@ -81,10 +74,3 @@ assert.bool(authenticated, 'authenticated');

// add/set token client in meta
const query = {};
if(accountId) {
query.id = database.hash(accountId);
} else {
query['account.email'] = email;
}
// prepare to add/set token client in account meta
const key = fastHash({data: clientId, encoding: 'base64'});

@@ -106,20 +92,40 @@ const client = {

if(explain) {
// 'find()' is used here because 'updateMany()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(query);
return cursor.explain('executionStats');
}
if(!warning) {
// ignore concurrent account updates when setting client
while(true) {
try {
// get existing record meta
const record = await brAccount.get({id: accountId, email});
if(!warning) {
const result = await database.collections.account.updateMany(
query, {
$set: {
[`meta.bedrock-authn-token.clients.${key}`]: client
// add client to meta
const meta = {
...record.meta,
sequence: record.meta.sequence + 1
};
if(!meta['bedrock-authn-token']) {
meta['bedrock-authn-token'] = {clients: {[key]: client}};
} else {
const tokenMeta = meta['bedrock-authn-token'];
if(!tokenMeta.clients) {
tokenMeta.clients = {[key]: client};
} else {
tokenMeta.clients[key] = client;
}
}
});
if(result.result.n === 0) {
throw new BedrockError(
'Could not set account token client. Account not found.',
'NotFoundError', {httpStatusCode: 404, public: true});
return await brAccount.update({id: accountId, meta});
} catch(e) {
if(e.name === 'InvalidStateError') {
// loop to try again; concurrently updated
continue;
}
// differentiate not found error from other errors
const notFound = e.name === 'NotFoundError';
throw new BedrockError(
'Could not set account token client.' +
notFound ? ' Account not found.' : '', {
name: notFound ? 'NotFoundError' : 'OperationError',
details: {httpStatusCode: notFound ? 404 : 500, public: true},
cause: e
});
}
}

@@ -166,10 +172,7 @@ }

* (e.g., a cookie value).
* @param {boolean} [options.explain=false] - An optional explain boolean.
*
* @returns {Promise<object> | ExplainObject} - Returns a Promise that resolves
* to {registered: true|false} or an ExplainObject if `explain=true`.
* @returns {Promise<object>} - Returns a Promise that resolves
* to {registered: true|false}.
*/
export async function isRegistered({
accountId, email, clientId, explain = false
} = {}) {
export async function isRegistered({accountId, email, clientId} = {}) {
assert.optionalString(accountId, 'accountId');

@@ -181,24 +184,18 @@ assert.optionalString(email, 'email');

// get token from meta
// get key for client
const key = fastHash({data: clientId, encoding: 'base64'});
const clientKey = `meta.bedrock-authn-token.clients.${key}`;
const query = {[clientKey]: {id: key, authenticated: true}};
const projection = {_id: 0, [clientKey]: 1};
if(accountId) {
query.id = database.hash(accountId);
} else {
query['account.email'] = email;
}
if(explain) {
// 'find().limit(1)' is used here because 'findOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(
query, {projection}).limit(1);
return cursor.explain('executionStats');
try {
const record = await brAccount.get({id: accountId, email});
// ensure account has authenticated client with matching `key`
const client = record.meta?.['bedrock-authn-token']?.clients?.[key];
return {registered: client?.id === key && client?.authenticated};
} catch(e) {
// throw any error other than not found
if(e.name !== 'NotFoundError') {
throw e;
}
// account not found, so client is not registered
return {registered: false};
}
const record = await database.collections.account.findOne(
query, {projection});
return {registered: !!record};
}

@@ -6,4 +6,3 @@ /*!

import * as totp from '@digitalbazaar/totp';
import assert from 'assert-plus';
import {checkAuthenticationRequirements} from './authenticationMethods.js';
import {deserializePhc, pbkdf2} from './pbkdf2.js';
import {

@@ -15,9 +14,10 @@ fastHash,

} from './helpers.js';
import {generateId} from 'bnid';
import {pbkdf2, deserializePhc} from './pbkdf2.js';
import {
getAccountRecord, setToken, pushToken, removeToken, removeExpiredTokens
getAccountRecord, pushToken, removeExpiredTokens, removeToken, setToken
} from './tokenStorage.js';
import {notify as _notify} from './notify.js';
import assert from 'assert-plus';
import {checkAuthenticationRequirements} from './authenticationMethods.js';
import {generateId} from 'bnid';
import {logger} from './logger.js';
import {notify as _notify} from './notify.js';

@@ -24,0 +24,0 @@ const {config, util: {BedrockError}} = bedrock;

@@ -5,45 +5,49 @@ /*!

import * as bedrock from '@bedrock/core';
import * as database from '@bedrock/mongodb';
import * as brAccount from '@bedrock/account';
const {util: {BedrockError}} = bedrock;
const META_KEY = 'bedrock-authn-token';
export async function getAccountRecord({
accountId, email, id, type, requireToken, explain
accountId, email, id, type, requireToken
} = {}) {
// tokens are found in account meta; query accordingly
const typeKey = `meta.bedrock-authn-token.tokens.${type}`;
const query = {};
if(requireToken) {
query[typeKey] = {$exists: true};
}
const projection = {
_id: 0, 'account.id': 1, 'account.email': 1, [typeKey]: 1
};
// get account record
const options = {};
if(accountId) {
query.id = database.hash(accountId);
options.id = accountId;
} else {
query['account.email'] = email;
options.email = email;
}
let record = await brAccount.get(options);
if(id) {
const tokenIdKey = `meta.bedrock-authn-token.tokens.${type}.id`;
query[tokenIdKey] = id;
}
// if a token is required or an `id` for the token was given, ensure the
// record has such a token; get tokens of `type` to apply further filtering
const tokens = record.meta?.[META_KEY]?.tokens?.[type];
if(explain) {
// 'find().limit(1)' is used here because 'findOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(
query, {projection}).limit(1);
return cursor.explain('executionStats');
// `tokens` may be a single token or an array of tokens
if(Array.isArray(tokens)) {
if(id) {
// specific token with `id` required, else record not found
if(!tokens.some(t => t.id === id)) {
record = null;
}
} else if(requireToken && tokens.length === 0) {
// any token of `type` required, but none found; no matching record
record = null;
}
} else {
if(id) {
// specific token with `id` required, else record not found
if(tokens?.id !== id) {
record = null;
}
} else if(requireToken && !tokens) {
// any token of `type` required, but none found; no matching record
record = null;
}
}
const record = await database.collections.account.findOne(
query, {projection});
// if no matching record or a token was required and an empty array was found
// then throw `NotFoundError`
const result = record?.meta?.['bedrock-authn-token']?.tokens?.[type];
if(!record ||
(requireToken && Array.isArray(result) && result.length === 0)) {
// if no record found, throw `NotFoundError`
if(!record) {
const subject = requireToken ? 'Authentication token' : 'Account';

@@ -62,136 +66,217 @@ throw new BedrockError(`${subject} not found.`, {

export async function pushToken({
accountId, email, type, token, maxCount, explain
accountId, email, type, token, maxCount
} = {}) {
const maxIndex = maxCount - 1;
const query = {
// push token the max index does not exist
[`meta.bedrock-authn-token.tokens.${type}.${maxIndex}`]: {$exists: false}
};
return _addToken(
{accountId, email, query, type, token, op: '$push', explain});
return _addToken({accountId, email, type, token, op: 'push', maxCount});
}
export async function setToken({accountId, email, type, token, explain} = {}) {
const updated = await _addToken(
{accountId, email, type, token, op: '$set', explain});
if(explain) {
// return explain result
return updated;
}
if(!updated) {
throw new BedrockError(
'Could not set authentication token. Account not found.',
'NotFoundError', {httpStatusCode: 404, public: true});
}
export async function setToken({accountId, email, type, token} = {}) {
return _addToken({accountId, email, type, token, op: 'set'});
}
export async function removeToken({accountId, type, id, explain = false} = {}) {
const query = {id: database.hash(accountId)};
let update;
const tokenTypeKey = `meta.bedrock-authn-token.tokens.${type}`;
if(id && type === 'nonce') {
update = {
$pull: {
[tokenTypeKey]: {id}
export async function removeToken({accountId, type, id} = {}) {
// ignore concurrent account updates when removing token(s)
while(true) {
try {
// get existing record meta
const record = await brAccount.get({id: accountId});
// check for existing token(s)
const tokens = record.meta?.[META_KEY]?.tokens?.[type];
let hasToken = false;
if(tokens) {
if(id === undefined) {
// no `id` given, at least one token must exist
hasToken = !Array.isArray(tokens) || tokens.length > 0;
} else if(Array.isArray(tokens)) {
// one of the tokens must match the `id`
hasToken = tokens.some(t => t.id === id);
} else {
// the token must match the `id`
hasToken = tokens?.id === id;
}
}
};
} else {
if(id) {
query[`${tokenTypeKey.id}`] = id;
}
update = {
$unset: {
[tokenTypeKey]: true
if(!hasToken) {
throw new BedrockError(
'Token not found.', {
name: 'NotFoundError',
details: {httpStatusCode: 404, public: true}
});
}
};
}
if(explain) {
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(query).limit(1);
return cursor.explain('executionStats');
}
// prepare to update meta
const meta = {...record.meta, sequence: record.meta.sequence + 1};
// remove token from meta
const result = await database.collections.account.updateOne(query, update);
if(result.result.n === 0) {
throw new BedrockError(
'Could not remove authentication token. Account or token not found.',
'NotFoundError', {httpStatusCode: 404, public: true});
if(id) {
if(Array.isArray(tokens)) {
// remove the token from the array
const filtered = tokens.filter(t => t.id !== id);
if(filtered.length === 0) {
// delete entire array
delete meta[META_KEY].tokens[type];
} else {
meta[META_KEY].tokens[type] = filtered;
}
} else {
// delete the token
delete meta[META_KEY].tokens[type];
}
} else {
// delete all tokens, no `id` specified
delete meta[META_KEY].tokens[type];
}
return await brAccount.update({id: record.account.id, meta});
} catch(e) {
if(e.name === 'InvalidStateError') {
// loop to try again; concurrently updated
continue;
}
// differentiate not found error from other errors
const notFound = e.name === 'NotFoundError';
throw new BedrockError(
'Could not remove authentication token. ' +
notFound ? ' Account or token not found.' : '', {
name: notFound ? 'NotFoundError' : 'OperationError',
details: {httpStatusCode: notFound ? 404 : 500, public: true},
cause: e
});
}
}
}
export async function removeExpiredTokens({
accountId, email, type, explain
} = {}) {
const query = {
$or: [{
[`meta.bedrock-authn-token.tokens.${type}.expires`]: {$lte: new Date()}
}, {
// also remove any legacy bcrypt tokens
[`meta.bedrock-authn-token.tokens.${type}.salt`]: {$exists: true}
}]
};
if(accountId) {
query.id = database.hash(accountId);
} else {
query['account.email'] = email;
}
export async function removeExpiredTokens({accountId, email, type} = {}) {
// ignore concurrent account updates when removing expired tokens
while(true) {
try {
// get existing record meta
const record = await brAccount.get({id: accountId, email});
// check for existing token(s)
const tokens = record.meta?.[META_KEY]?.tokens?.[type];
if(!tokens) {
// nothing to remove, return
return false;
}
if(explain) {
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(query).limit(1);
return cursor.explain('executionStats');
}
// determine if there are any expired tokens
let updatedTokens;
const now = new Date();
if(!Array.isArray(tokens)) {
// also remove any legacy bcrypt tokens (indicated by `salt` presence)
// note: if `tokens.expires` does not exist, then the comparison will
// fail as desired
if(!(tokens.salt || now > tokens.expires)) {
// nothing to remove, return
return false;
}
} else {
// note: if `t.expires` does not exist, then the comparison will
// fail as desired
updatedTokens = tokens.filter(t => !(t.salt || now > t.expires));
if(updatedTokens.length === tokens.length) {
// nothing to remove, return
return false;
}
if(updatedTokens.length === 0) {
// no tokens remaining, clear entire array
updatedTokens = undefined;
}
}
const update = {
$pull: {
[`meta.bedrock-authn-token.tokens.${type}`]: {
$or: [{
expires: {$lte: new Date()}
}, {
// also remove any legacy bcrypt tokens
salt: {$exists: true}
}]
// prepare to update meta
const meta = {...record.meta, sequence: record.meta.sequence + 1};
if(updatedTokens === undefined) {
delete meta[META_KEY].tokens[type];
} else {
meta[META_KEY].tokens[type] = updatedTokens;
}
return await brAccount.update({id: record.account.id, meta});
} catch(e) {
if(e.name === 'InvalidStateError') {
// loop to try again; concurrently updated
continue;
}
// no tokens to remove if account has been removed
if(e.name === 'NotFoundError') {
return false;
}
// some other error
throw new BedrockError(
'Could not remove expired tokens.', {
name: 'OperationError',
details: {httpStatusCode: 500, public: true},
cause: e
});
}
};
const result = await database.collections.account.updateOne(query, update);
return result.result.n > 0;
}
}
async function _addToken({
accountId, email, query, type, token, op, explain
accountId, email, type, token, op, maxCount
}) {
// update existing query
query = {...query};
if(accountId) {
query.id = database.hash(accountId);
} else {
query['account.email'] = email;
}
// ignore concurrent account updates when adding a token
while(true) {
try {
// get existing record meta
const record = await brAccount.get({id: accountId, email});
if(explain) {
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await database.collections.account.find(query).limit(1);
return cursor.explain('executionStats');
}
// get any existing token(s)
const tokens = record.meta?.[META_KEY]?.tokens?.[type];
// add token to account `meta`
const update = {
[op]: {
[`meta.bedrock-authn-token.tokens.${type}`]: token
// prepare to update meta
const meta = {...record.meta, sequence: record.meta.sequence + 1};
if(op === 'push') {
if(tokens && !Array.isArray(tokens)) {
// should never happen unless code is called improperly with a
// 'push' `op` on a single-token type
throw new TypeError('Token type mismatch.');
}
if(!tokens) {
// first token
if(!meta[META_KEY]) {
meta[META_KEY] = {tokens: {[type]: [token]}};
} else if(!meta[META_KEY].tokens) {
meta[META_KEY].tokens = {[type]: [token]};
} else {
meta[META_KEY].tokens[type] = [token];
}
} else if(tokens.length < maxCount) {
// append token
meta[META_KEY].tokens[type] = [...tokens, token];
} else {
// max token count reached
return false;
}
} else {
// presume `op` === 'set'
if(tokens && Array.isArray(tokens)) {
// should never happen unless code is called improperly with a
// 'set' `op` on a multi-token type
throw new TypeError('Token type mismatch.');
}
if(!meta[META_KEY]) {
meta[META_KEY] = {tokens: {[type]: token}};
} else if(!meta[META_KEY].tokens) {
meta[META_KEY].tokens = {[type]: token};
} else {
meta[META_KEY].tokens[type] = token;
}
}
return await brAccount.update({id: record.account.id, meta});
} catch(e) {
if(e.name === 'InvalidStateError') {
// loop to try again; concurrently updated
continue;
}
// differentiate not found error from other errors
const notFound = e.name === 'NotFoundError';
throw new BedrockError(
'Could not add authentication token to account.' +
notFound ? ' Account not found.' : '', {
name: notFound ? 'NotFoundError' : 'OperationError',
details: {httpStatusCode: notFound ? 404 : 500, public: true},
cause: e
});
}
};
const result = await database.collections.account.updateOne(query, update);
return result.result.n > 0;
}
}
/**
* An object containing information on the query plan.
*
* @typedef {object} ExplainObject
*/
{
"name": "@bedrock/authn-token",
"version": "10.2.0",
"version": "10.3.0",
"type": "module",

@@ -33,5 +33,4 @@ "description": "Simple token-based authentication for Bedrock apps",

"peerDependencies": {
"@bedrock/account": "^8.0.0",
"@bedrock/core": "^6.0.0",
"@bedrock/mongodb": "^10.0.0"
"@bedrock/account": "^8.2.0",
"@bedrock/core": "^6.0.0"
},

@@ -42,7 +41,7 @@ "directories": {

"devDependencies": {
"eslint": "^7.32.0",
"eslint-config-digitalbazaar": "^2.8.0",
"eslint-plugin-jsdoc": "^37.9.7",
"jsdoc-to-markdown": "^7.1.1"
"eslint": "^8.29.0",
"eslint-config-digitalbazaar": "^4.2.0",
"eslint-plugin-jsdoc": "^39.6.4",
"jsdoc-to-markdown": "^8.0.0"
}
}
/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
*/
import * as brAccount from '@bedrock/account';
import * as brAuthnToken from '@bedrock/authn-token';

@@ -219,171 +220,39 @@ import * as helpers from './helpers.js';

describe('TOTP Database Tests', () => {
describe('Indexes', async () => {
let accountId;
// NOTE: the accounts collection is getting erased before each test
// this allows for the creation of tokens using the same account info
beforeEach(async () => {
await helpers.prepareDatabase(mockData);
accountId = mockData.accounts['alpha@example.com'].account.id;
const accountId2 = mockData.accounts['beta@example.com'].account.id;
describe('TOTP storage', () => {
let accountId;
// NOTE: the accounts collection is getting erased before each test
// this allows for the creation of tokens using the same account info
beforeEach(async () => {
await helpers.prepareDatabase(mockData);
accountId = mockData.accounts['alpha@example.com'].account.id;
// creates token
await brAuthnToken.set({
accountId,
type: 'totp'
});
// second token is created here in order to do proper assertions for
// 'nReturned', 'totalKeysExamined' and 'totalDocsExamined'.
await brAuthnToken.set({
accountId: accountId2,
type: 'totp'
});
// creates token
await brAuthnToken.set({
accountId,
type: 'totp'
});
describe('setToken() for set()', () => {
it(`is properly indexed for 'id' in setToken()`, async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
const {executionStats} = await brAuthnToken._tokenStorage.setToken({
accountId: accountId3,
type: 'totp',
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
it('setToken() for set() using id', async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
const result = await brAuthnToken._tokenStorage.setToken({
accountId: accountId3,
type: 'totp',
token: {id: '1'}
});
describe('getAccountRecord() for get()', () => {
it(`is properly indexed for 'id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
accountId,
type: 'totp',
// token must exist
requireToken: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
it(`is properly indexed for 'account.email'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
email: 'alpha@example.com',
type: 'totp',
// token must exist
requireToken: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
should.exist(result);
result.should.equal(true);
const record2 = await brAccount.get({id: accountId3});
should.exist(record2.meta['bedrock-authn-token']?.tokens?.totp?.id);
record2.meta['bedrock-authn-token']?.totp?.id.should.equal('1');
});
it('removeToken() for remove()', async () => {
const result = await brAuthnToken._tokenStorage.removeToken({
accountId,
type: 'totp'
});
describe('getAccountRecord() for getAll()', () => {
it(`is properly indexed for 'id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
accountId,
type: 'totp',
// getAll
requireToken: false,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
it(`is properly indexed for 'account.email'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
email: 'alpha@example.com',
type: 'totp',
// getAll
requireToken: false,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
describe('removeToken() for remove()', () => {
it(`is properly indexed for 'id'`, async () => {
const {executionStats} = await brAuthnToken._tokenStorage.removeToken({
accountId,
type: 'totp',
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
describe('getAccountRecord() for verify()', () => {
it(`is properly indexed for 'id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
accountId,
type: 'totp',
// token must exist
requiredToken: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
it(`is properly indexed for 'account.email'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
email: 'alpha@example.com',
type: 'totp',
// token must exist
requiredToken: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
it(`is properly indexed for 'id' in setAuthenticationRequirements()`,
async () => {
const requiredAuthenticationMethods = [];
const {executionStats} = await brAuthnToken
.setAuthenticationRequirements({
accountId,
requiredAuthenticationMethods,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
should.exist(result);
result.should.equal(true);
const record2 = await brAccount.get({id: accountId});
should.not.exist(record2.meta['bedrock-authn-token']?.tokens?.totp);
});
});

@@ -7,4 +7,4 @@ /*!

import * as brAuthnToken from '@bedrock/authn-token';
import {mockData} from './mock.data.js';
import {prepareDatabase} from './helpers.js';
import {mockData} from './mock.data.js';
import sinon from 'sinon';

@@ -411,3 +411,3 @@

});
it('should remove an expired nonce', async () => {
it('should not return an expired nonce', async () => {
const accountId = mockData.accounts['alpha@example.com'].account.id;

@@ -451,3 +451,3 @@ // set a token with an older date.

// get all existing nonce for the same account again after undoing the stub,
// it should give an empty array as getAll drops any expired token.
// it should give an empty array as getAll drops any expired token
let result2;

@@ -467,194 +467,220 @@ try {

});
});
describe('Nonce Database Tests', () => {
describe('Indexes', () => {
let accountId;
let nonce;
// NOTE: the accounts collection is getting erased before each test
// this allows for the creation of tokens using the same account info
beforeEach(async () => {
await prepareDatabase(mockData);
accountId = mockData.accounts['alpha@example.com'].account.id;
const accountId2 = mockData.accounts['beta@example.com'].account.id;
// creates token
nonce = await brAuthnToken.set({
accountId,
type: 'nonce'
});
// second token is created here in order to do proper assertions for
// 'nReturned', 'totalKeysExamined' and 'totalDocsExamined'.
await brAuthnToken.set({
accountId: accountId2,
type: 'nonce'
});
});
describe('pushToken() for set()', () => {
it(`is properly indexed for 'id' and ` +
`'meta.bedrock-authn-token.tokens.nonce'`, async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
const {executionStats} = await brAuthnToken._tokenStorage.pushToken({
accountId: accountId3,
it('should remove all expired nonces from storage', async () => {
const accountId = mockData.accounts['alpha@example.com'].account.id;
// set some tokens with an older date
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const clock = sinon.useFakeTimers(yesterday.getTime());
let nonce1;
{
let err;
try {
nonce1 = await brAuthnToken.set({
accountId,
type: 'nonce',
maxCount: 10,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
it(`is properly indexed for 'account.email' and ` +
`'meta.bedrock-authn-token.tokens.nonce'`, async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
const record = await brAccount.get({id: accountId3});
const email = record.account.email;
const {executionStats} = await brAuthnToken._tokenStorage.pushToken({
email,
type: 'nonce',
maxCount: 10,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
describe('getAccountRecord() for get()', () => {
it(`is properly indexed for 'id' and ` +
`'meta.bedrock-authn-token.tokens.nonce.id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
} catch(e) {
err = e;
}
assertNoError(err);
should.exist(nonce1);
}
let nonce2;
{
let err;
try {
nonce2 = await brAuthnToken.set({
accountId,
type: 'nonce',
id: nonce.id,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
it(`is properly indexed for 'account.email' and ` +
`'meta.bedrock-authn-token.tokens.nonce.id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
email: 'alpha@example.com',
type: 'nonce',
id: nonce.id,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
} catch(e) {
err = e;
}
assertNoError(err);
should.exist(nonce2);
}
// undo the stub
clock.restore();
// should remove expired nonces
const result1 = await brAuthnToken._tokenStorage.removeExpiredTokens({
accountId, type: 'nonce'
});
describe('getAccountRecord() for getAll()', () => {
it(`is properly indexed for 'id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
accountId,
type: 'nonce',
// getAll
requireToken: false,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
should.exist(result1);
result1.should.equal(true);
// get all existing nonce for the same account again after undoing the stub,
// it should give an empty array
let result2;
let err;
try {
result2 = await brAuthnToken.getAll({
accountId,
type: 'nonce',
});
it(`is properly indexed for 'account.email'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
email: 'alpha@example.com',
type: 'nonce',
// getAll
requireToken: false,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
describe('removeToken() for remove()', () => {
it(`is properly indexed for 'id'`, async () => {
const {executionStats} = await brAuthnToken._tokenStorage.removeToken({
} catch(e) {
err = e;
}
assertNoError(err);
should.exist(result2);
result2.should.be.an('object');
result2.tokens.should.eql([]);
result2.allTokens.should.eql([]);
});
it('should remove one expired nonce from storage', async () => {
const accountId = mockData.accounts['alpha@example.com'].account.id;
// set a nonce with current date (non-expired)
let nonce1;
{
let err;
try {
nonce1 = await brAuthnToken.set({
accountId,
type: 'nonce',
id: nonce.id,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
});
describe('getAccountRecord() for verify()', () => {
it(`is properly indexed for 'id'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
} catch(e) {
err = e;
}
assertNoError(err);
should.exist(nonce1);
}
// set a token with an older date
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const clock = sinon.useFakeTimers(yesterday.getTime());
let nonce2;
{
let err;
try {
nonce2 = await brAuthnToken.set({
accountId,
type: 'nonce',
// get single token, it MUST exist
requireToken: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
it(`is properly indexed for 'account.email'`, async () => {
const {
executionStats
} = await brAuthnToken._tokenStorage.getAccountRecord({
email: 'alpha@example.com',
type: 'nonce',
// get single token, it MUST exist
requireToken: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
});
} catch(e) {
err = e;
}
assertNoError(err);
should.exist(nonce2);
}
// undo the stub
clock.restore();
// should remove one expired nonce
const result1 = await brAuthnToken._tokenStorage.removeExpiredTokens({
accountId, type: 'nonce'
});
it(`is properly indexed for 'id' in setAuthenticationRequirements()`,
async () => {
const requiredAuthenticationMethods = [];
const {executionStats} = await brAuthnToken
.setAuthenticationRequirements({
accountId,
requiredAuthenticationMethods,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.stage
.should.equal('IXSCAN');
should.exist(result1);
result1.should.equal(true);
// get all existing nonce for the same account again after undoing the stub,
// it should give an empty array as getAll drops any expired token.
let result2;
let err;
try {
result2 = await brAuthnToken.getAll({
accountId,
type: 'nonce',
});
} catch(e) {
err = e;
}
assertNoError(err);
should.exist(result2);
result2.should.be.an('object');
should.exist(result2.tokens[0]);
result2.tokens[0].id.should.eql(nonce1.id);
});
});
describe('Nonce storage', () => {
let accountId;
let nonce;
// NOTE: the accounts collection is getting erased before each test
// this allows for the creation of tokens using the same account info
beforeEach(async () => {
await prepareDatabase(mockData);
accountId = mockData.accounts['alpha@example.com'].account.id;
// creates token
nonce = await brAuthnToken.set({
accountId,
type: 'nonce'
});
});
it('pushToken() for set() using id', async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
// push first nonce
const result = await brAuthnToken._tokenStorage.pushToken({
accountId: accountId3,
type: 'nonce',
token: {id: '1'},
maxCount: 10
});
should.exist(result);
result.should.equal(true);
const record2 = await brAccount.get({id: accountId3});
should.exist(record2.meta['bedrock-authn-token']?.tokens?.nonce?.[0]);
// push another nonce
const result2 = await brAuthnToken._tokenStorage.pushToken({
accountId: accountId3,
type: 'nonce',
token: {id: '2'},
maxCount: 10
});
should.exist(result2);
result2.should.equal(true);
const record3 = await brAccount.get({id: accountId3});
should.exist(record3.meta['bedrock-authn-token']?.tokens?.nonce?.[0]);
should.exist(record3.meta['bedrock-authn-token']?.tokens?.nonce?.[1]);
});
it('pushToken() for set() using email', async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
// push first nonce
const record = await brAccount.get({id: accountId3});
const email = record.account.email;
const result = await brAuthnToken._tokenStorage.pushToken({
email,
type: 'nonce',
token: {id: '1'},
maxCount: 10
});
should.exist(result);
result.should.equal(true);
const record2 = await brAccount.get({id: accountId3});
should.exist(record2.meta['bedrock-authn-token']?.tokens?.nonce?.[0]);
// push another nonce
const result2 = await brAuthnToken._tokenStorage.pushToken({
email,
type: 'nonce',
token: {id: '2'},
maxCount: 10
});
should.exist(result2);
result2.should.equal(true);
const record3 = await brAccount.get({id: accountId3});
should.exist(record3.meta['bedrock-authn-token']?.tokens?.nonce?.[0]);
should.exist(record3.meta['bedrock-authn-token']?.tokens?.nonce?.[1]);
// remove first nonce
await brAuthnToken._tokenStorage.removeToken({
accountId: record.account.id,
type: 'nonce',
id: '1'
});
const record4 = await brAccount.get({id: accountId3});
should.exist(record4.meta['bedrock-authn-token']?.tokens?.nonce?.[0]?.id);
record4.meta['bedrock-authn-token']
?.tokens?.nonce?.[0]?.id.should.equal('2');
});
it('removeToken() for remove()', async () => {
const result = await brAuthnToken._tokenStorage.removeToken({
accountId,
type: 'nonce',
id: nonce.id
});
should.exist(result);
result.should.equal(true);
const record2 = await brAccount.get({id: accountId});
should.not.exist(record2.meta['bedrock-authn-token']?.tokens?.nonce);
});
});

@@ -8,82 +8,74 @@ /*!

describe('Clients Database Tests', () => {
describe('Indexes', async () => {
let accountId;
// NOTE: the accounts collection is getting erased before each test
// this allows for the creation of tokens using the same account info
beforeEach(async () => {
await helpers.prepareDatabase(mockData);
accountId = mockData.accounts['alpha@example.com'].account.id;
const accountId2 = mockData.accounts['beta@example.com'].account.id;
describe('clients', () => {
let accountId;
let accountId2;
// NOTE: the accounts collection is getting erased before each test
// this allows for the creation of tokens using the same account info
beforeEach(async () => {
await helpers.prepareDatabase(mockData);
accountId = mockData.accounts['alpha@example.com'].account.id;
accountId2 = mockData.accounts['beta@example.com'].account.id;
await brAuthnToken.clients.set({
accountId,
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be',
authenticated: true
});
// second client is created here in order to do proper assertions for
// 'nReturned', 'totalKeysExamined' and 'totalDocsExamined'.
await brAuthnToken.clients.set({
accountId: accountId2,
clientId: 'd55d8879-6d01-472e-84a7-eb22ebf319e5',
authenticated: true
});
await brAuthnToken.clients.set({
accountId,
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be',
authenticated: true
});
it(`is properly indexed for 'id' in set()`, async () => {
const accountId3 = mockData.accounts['gamma@example.com'].account.id;
const {executionStats} = await brAuthnToken.clients.set({
accountId: accountId3,
clientId: '4ce0fe94-8478-469e-a4df-08b42acdaf72',
authenticated: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.stage
.should.equal('IXSCAN');
});
it('should check is registered via "id"', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
accountId,
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be'
});
it(`is properly indexed for 'account.email' in set()`, async () => {
const {executionStats} = await brAuthnToken.clients.set({
email: 'gamma@example.com',
clientId: '4ce0fe94-8478-469e-a4df-08b42acdaf72',
authenticated: true,
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.stage
.should.equal('IXSCAN');
should.exist(registered);
registered.should.equal(true);
});
it('should check is registered via "email"', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
email: 'alpha@example.com',
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be'
});
it(`is properly indexed for 'id' and ` +
`'meta.bedrock-authn-token.clients.{key}' in ` +
'isRegistered()', async () => {
const {executionStats} = await brAuthnToken.clients.isRegistered({
accountId,
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be',
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
should.exist(registered);
registered.should.equal(true);
});
it('should check is NOT registered for mismatched client ID', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
accountId,
clientId: 'does not exist'
});
it(`is properly indexed for 'account.email' and ` +
`'meta.bedrock-authn-token.clients.{key}' in ` +
'isRegistered()', async () => {
const {executionStats} = await brAuthnToken.clients.isRegistered({
email: 'alpha@example.com',
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be',
explain: true
});
executionStats.nReturned.should.equal(1);
executionStats.totalKeysExamined.should.equal(1);
executionStats.totalDocsExamined.should.equal(1);
executionStats.executionStages.inputStage.inputStage.inputStage.stage
.should.equal('IXSCAN');
should.exist(registered);
registered.should.equal(false);
});
it('should check is NOT registered via "id"', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
accountId: accountId2,
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be'
});
should.exist(registered);
registered.should.equal(false);
});
it('should check is NOT registered via "email"', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
email: 'beta@example.com',
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be'
});
should.exist(registered);
registered.should.equal(false);
});
it('should check is NOT registered via "id" (non-existent)', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
accountId: 'doesnotexist',
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be'
});
should.exist(registered);
registered.should.equal(false);
});
it('should check is NOT registered via "email" (non-existant)', async () => {
const {registered} = await brAuthnToken.clients.isRegistered({
email: 'doesnotexist@example.com',
clientId: '670753bd-2cf3-4878-8de4-4aa5e28989be'
});
should.exist(registered);
registered.should.equal(false);
});
});

@@ -5,4 +5,4 @@ /*!

import * as brAuthnToken from '@bedrock/authn-token';
import {mockData} from './mock.data.js';
import {prepareDatabase} from './helpers.js';
import {mockData} from './mock.data.js';

@@ -9,0 +9,0 @@ describe('Password API', () => {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc