@bedrock/authn-token
Advanced tools
Comparing version 10.2.0 to 10.3.0
# 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 @@ |
@@ -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', () => { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
6
30
2900
113459