migrate ResourceAuthenticator into here from separate package
authorJustin Wind <justin.wind+git@gmail.com>
Sat, 11 May 2024 22:21:58 +0000 (15:21 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sun, 12 May 2024 19:19:32 +0000 (12:19 -0700)
19 files changed:
.npmrc [new file with mode: 0644]
CHANGELOG.md
README.md
index.js
lib/common.js
lib/errors.js
lib/resource-authenticator.js [new file with mode: 0644]
lib/session-manager.js
package-lock.json
package.json
test/lib/authenticator.js
test/lib/common.js
test/lib/resource-authenticator.js [new file with mode: 0644]
test/lib/session-manager.js
test/lib/template/ia-html.js
test/lib/template/login-html.js
test/lib/template/otp-html.js
test/lib/template/settings-html.js
test/stub-db.js

diff --git a/.npmrc b/.npmrc
new file mode 100644 (file)
index 0000000..258cd85
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+message="bump package version to %s"
index 3a1f9890337ed0218705338cfecfc58f08fe55eb..50887e75417ad31b7cd24fe594ef8066fb20400e 100644 (file)
@@ -4,6 +4,10 @@ Releases and notable changes to this project are documented here.
 
 ## [Unreleased]
 
+## [v1.5.0] - TBD
+
+- Migrated resource authenticator into this package, for inter-server API authentication.
+
 ## [v1.4.1] - 2024-05-11
 
 - updated dependencies and devDependencies
index 6b4400ea56b2285729fd59e3c6fcf307d71a89d4..c4234f651480faa38a3891313b64593e588d0973 100644 (file)
--- a/README.md
+++ b/README.md
@@ -41,6 +41,21 @@ Class providing service handler functions for rendering and processing session l
 
 - `sessionNavLinks` call from app templates to populate navLinks for account settings and logout
 
+### ResourceAuthenticator
+
+Class which fetches and validates resource identifiers and their secrets from database.
+
+Resources are assumed to be other services making API calls.
+
+This is just a time-gated pre-shared-secret HMAC Bearer token scheme.
+
+Default token format is a ':'-separated concatenation of:
+
+- resource identifier, which is a UUID, encoded as 24 bytes in base64url
+- current epoch, encoded as base10 string
+- salt value, randomness encoded as 28 bytes in base64url
+- sha256 HMAC digest of previous data, encoded in base64url
+
 ### Other Notes
 
 For the moment, this imposes a web structure of /admin/* for authentication management paths.
index 5d9da0c4c3b931845ea1edca8fbfdccca3cef3da..b4818641d2c3e45ffb36d71a39254ebc1cfbaa95 100644 (file)
--- a/index.js
+++ b/index.js
@@ -1,12 +1,14 @@
 'use strict';
 
 const Authenticator = require('./lib/authenticator');
+const ResourceAuthenticator = require('./lib/resource-authenticator');
 const SessionManager = require('./lib/session-manager');
 const stdioCredential = require('./lib/stdio-credential');
 const templateHelpers = require('./lib/template/helpers');
 
 module.exports = {
   Authenticator,
+  ResourceAuthenticator,
   SessionManager,
   stdioCredential,
   ...templateHelpers,
index 120c87045613e008988ed39f3db651aa09e2544c..5563254f0bab35b7b54322dd46ad0cfa9c49e3c4 100644 (file)
@@ -1,7 +1,10 @@
 'use strict';
 
 const { common } = require('@squeep/api-dingus');
+const { randomBytes } = require('node:crypto');
+const { promisify } = require('node:util');
 
+const randomBytesAsync = promisify(randomBytes);
 
 /**
  * Recursively freeze an object.
@@ -66,4 +69,5 @@ module.exports = Object.assign(Object.create(common), {
   mysteryBoxLogger,
   obscureAuthorizationHeader,
   omit,
+  randomBytesAsync,
 });
index 98a6f182921185a8f0b4951b4d1439cb4606c76a..5f646ab31f1d7089ad209bbc2cb681bc1ae6b029 100644 (file)
@@ -2,6 +2,17 @@
 
 const { Errors } = require('@squeep/api-dingus');
 
+class ResourceAuthenticatorError extends Error {
+  constructor(...args) {
+    super(...args);
+    delete this.stack;
+  }
+  get name() {
+    return this.constructor.name;
+  }
+}
+
 module.exports = {
+  ResourceAuthenticatorError,
   ...Errors,
 };
\ No newline at end of file
diff --git a/lib/resource-authenticator.js b/lib/resource-authenticator.js
new file mode 100644 (file)
index 0000000..a635d9c
--- /dev/null
@@ -0,0 +1,223 @@
+'use strict';
+
+const { createHmac } = require('node:crypto');
+const uuid = require('uuid');
+const common = require('./common');
+const Enum = require('./enum');
+const { ResourceAuthenticatorError, ResponseError } = require('./errors');
+
+const _fileScope = common.fileScope(__filename);
+
+/**
+ * @typedef {import('node:http')} http
+ */
+
+/**
+ * @typedef {object} ResourceRecord
+ * @property {string} secret secret
+ */
+
+/**
+ * @typedef {object} ConsoleLike
+ * @property {Function} debug log debug
+ * @property {Function} error log error
+ * @property {Function} info log info
+ */
+
+/**
+ * @callback DBContextExec
+ * @param {object} dbCtx db context
+ * @returns {Promise<any>} result
+ */
+
+/**
+ * @typedef {object} ResourceDBInterface
+ * @property {(DBContextExec) => Promise<any>} context db context runner
+ * @property {(dbCtx: any, identifier: string) => Promise<ResourceRecord>} resourceGet get a resource entry
+ */
+
+/**
+ * @typedef {object} ResourceAuthenticatorOptions
+ * @property {string=} digestAlgorithm digest algorithm
+ * @property {number=} graceSeconds grace seconds, give or take
+ * @property {number=} saltBytes salt bytes
+ */
+
+/**
+ * @type {ResourceAuthenticatorOptions}
+ */
+const defaultResourceAuthenticatorOptions = {
+  digestAlgorithm: 'sha256',
+  graceSeconds: 60,
+  saltBytes: 21,
+};
+
+class ResourceAuthenticator {
+  /**
+   * 
+   * @param {ConsoleLike} logger logger instance
+   * @param {ResourceDBInterface} db db instance
+   * @param {object} options options
+   * @param {ResourceAuthenticatorOptions} options.resourceAuthenticator resource authenticator options
+   */
+  constructor(logger, db, options = {}) {
+    this.logger = logger;
+    this.db = db;
+    common.setOptions(this, defaultResourceAuthenticatorOptions, options?.resourceAuthenticator || {});
+  }
+
+
+  /**
+   * Get the current epoch.
+   * @returns {number} epoch seconds
+   */
+  static get currentEpoch() {
+    return Math.ceil(Date.now() / 1000);
+  }
+
+
+  /**
+   * Compact uuid identifier as 24 character url-safe string.
+   * @param {string} identifier uuid
+   * @returns {string} base64url encoded uuid
+   */
+  static ensmallenIdentifier(identifier) {
+    return Buffer.from(uuid.parse(identifier)).toString('base64url');
+  }
+
+
+  /**
+   * Expand compacted uuid identifier.
+   * @param {string} compact base64url encoded uuid
+   * @returns {string} uuid
+   */
+  static embiggenIdentifier(compact) {
+    const uuidBuffer = Buffer.from(compact, 'base64url');
+    return uuid.stringify(uuidBuffer);
+  }
+
+
+  /**
+   * Get an encoded salt string.
+   * @param {number} saltBytes salt bytes
+   * @returns {Promise<string>} base64url encoded
+   */
+  async getSalt(saltBytes = this.saltBytes) {
+    const saltBuffer = await common.randomBytesAsync(saltBytes);
+    return saltBuffer.toString('base64url');
+  }
+
+
+  /**
+   * Smoosh some contents through the configured HMAC.
+   * @param {Buffer|string} secret secret
+   * @param  {...any} contents contents
+   * @returns {string} base64url encoded digest
+   */
+  createDigest(secret, ...contents) {
+    const hmac = createHmac(this.digestAlgorithm, secret);
+    contents.forEach((content) => hmac.update(Buffer.from(content)));
+    return hmac.digest('base64url');
+  }
+
+  /**
+   * Return an authorization header value for an identifier and secret.
+   * @param {string} identifier uuid
+   * @param {string} secret secret
+   * @returns {Promise<string>} authorization header
+   */
+  async authenticate(identifier, secret) {
+    const authenticationType = 'Bearer';
+    const currentEpoch = this.constructor.currentEpoch.toString();
+    const smallIdentifier = this.constructor.ensmallenIdentifier(identifier);
+    const salt = await this.getSalt();
+    const token = [
+      smallIdentifier,
+      currentEpoch,
+      salt,
+      this.createDigest(secret, smallIdentifier, currentEpoch, salt),
+    ].join(':');
+    return [authenticationType, token].join(' ');
+  }
+
+
+  /**
+   * Require a request to include a valid resource bearer token.
+   * Resource bearer tokens are of the form 'compact-identifier:epoch:salt:${hmac(identifier + epoch + salt)}'
+   * where the hmac using a pre-shared secret.
+   * Sets ctx.resourceIdentifier on success.
+   * @param {http.IncomingMessage} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @returns {Promise<void>}
+   */
+  async required(req, res, ctx) {
+    const _scope = _fileScope('required');
+    this.logger.debug(_scope, 'called');
+
+    try {
+      const authHeader = req.getHeader(Enum.Header.Authorization);
+      if (!authHeader) {
+        throw new ResourceAuthenticatorError('no authz header');
+      }
+
+      const [ authMethod, credentials ] = common.splitFirst(authHeader, ' ', '');
+      if (authMethod.toLowerCase() !== 'bearer' || !credentials) {
+        throw new ResourceAuthenticatorError('authz header not bearer type');
+      }
+
+      const [tIdentifier, tEpoch, tSalt, tDigest, ...tRest] = credentials.split(':');
+      if (!tIdentifier || !tEpoch || !tSalt || !tDigest) {
+        throw new ResourceAuthenticatorError('unparsable bearer token');
+      }
+      if (tRest.length) {
+        this.logger.debug(_scope, 'unexpected additional fields in bearer token', { tRest });
+      }
+
+      const identifier = this.constructor.embiggenIdentifier(tIdentifier);
+
+      const resource = await this.db.context(async (dbCtx) => {
+        return await this.db.resourceGet(dbCtx, identifier);
+      }); // dbCtx
+      if (!resource) {
+        throw new ResourceAuthenticatorError('invalid resource');
+      }
+
+      const resourceEpoch = parseInt(tEpoch, 10);
+      const epochDrift = Math.abs(resourceEpoch - this.constructor.currentEpoch);
+      if (epochDrift > this.graceSeconds) {
+        throw new ResourceAuthenticatorError('timestamp out of grace period');
+      }
+
+      const digest = this.createDigest(resource.secret, tIdentifier, tEpoch, tSalt);
+      if (digest !== tDigest) {
+        throw new ResourceAuthenticatorError('invalid digest');
+      }
+
+      // Success
+      ctx.resourceIdentifier = identifier;
+      this.logger.debug(_scope, 'success');
+    } catch (e) {
+      if (e instanceof ResourceAuthenticatorError) {
+        this.logger.debug(_scope, 'invalid', { error: e });
+        return this.constructor.requestBearer(res);
+      }
+      this.logger.error(_scope, 'failed', { error: e });
+      throw e;
+    }
+  }
+
+
+  /**
+   * Return a status code requesting bearer authentication.
+   * @param {http.ServerResponse} res response
+   * @param {object} response defaults to Unauthorized status
+   */
+  static requestBearer(res, response = Enum.ErrorResponse.Unauthorized) {
+    res.setHeader(Enum.Header.WWWAuthenticate, 'Bearer');
+    throw new ResponseError(response);
+  }
+
+}
+
+module.exports = ResourceAuthenticator;
\ No newline at end of file
index 7b33d8600b986cde34eab5dcf87a7a37b9aae150..d399e5bd1fb480044459cde001e7eda8a90f213b 100644 (file)
@@ -8,7 +8,7 @@
 const { Communication: IndieAuthCommunication } = require('@squeep/indieauth-helper');
 const { MysteryBox } = require('@squeep/mystery-box');
 const { TOTP } = require('@squeep/totp');
-const { randomUUID } = require('crypto');
+const { randomUUID } = require('node:crypto');
 const common = require('./common');
 const Enum = require('./enum');
 const Template = require('./template');
@@ -604,7 +604,7 @@ class SessionManager {
             await this._otpDisable(dbCtx, ctx, authData);
             return;
 
-          case 'confirm': 
+          case 'confirm':
             await this._otpConfirm(dbCtx, ctx);
             return;
 
index cadd7ce785ce5b5fdacf6ee2169910d34abbfd23..ea9befc9b32bd45a5a374d2e64796f43b5bb78d8 100644 (file)
@@ -13,7 +13,8 @@
         "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
         "@squeep/indieauth-helper": "^1",
         "@squeep/mystery-box": "^2",
-        "@squeep/totp": "^1"
+        "@squeep/totp": "^1",
+        "uuid": "^9"
       },
       "devDependencies": {
         "@squeep/eslint-config": "^1",
index 9bcb4985c0401bf17229f0c0f8e3d0c82171e9e9..60cfb5949e6bf502dadfae844cfd5bea89cb6cfd 100644 (file)
@@ -1,7 +1,7 @@
 {
   "name": "@squeep/authentication-module",
   "version": "1.4.1",
-  "description": "Wrangles authenticated sessions; intended for use in Squeep Framework Applications.",
+  "description": "Wrangles authenticated sessions, endpoints, and resources; intended for use in Squeep Framework Applications.",
   "main": "index.js",
   "scripts": {
     "audit": "npm audit",
@@ -39,7 +39,8 @@
     "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
     "@squeep/indieauth-helper": "^1",
     "@squeep/mystery-box": "^2",
-    "@squeep/totp": "^1"
+    "@squeep/totp": "^1",
+    "uuid": "^9"
   },
   "optionalDependencies": {
     "argon2": "^0.40.1",
index 84ee1c5332564cec56b6b6e616c1f5b743a18475..75ae9a883547ba055f341a480104959d7fa50b7d 100644 (file)
@@ -3,7 +3,7 @@
 /* eslint-disable jsdoc/require-jsdoc */
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const sinon = require('sinon');
 const Authenticator = require('../../lib/authenticator');
 const stubLogger = require('../stub-logger');
index 82b80f0db7a18f7e1ef35b2b13bccdec18d671a0..5ddc3d752a4c3b0778062a4da2e886019f6831f1 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-env mocha */
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const sinon = require('sinon');
 const stubLogger = require('../stub-logger');
 const common = require('../../lib/common');
diff --git a/test/lib/resource-authenticator.js b/test/lib/resource-authenticator.js
new file mode 100644 (file)
index 0000000..7f360e3
--- /dev/null
@@ -0,0 +1,239 @@
+'use strict';
+
+const assert = require('node:assert');
+const sinon = require('sinon');
+const ResourceAuthenticator = require('../../lib/resource-authenticator');
+const stubLogger = require('../stub-logger');
+const stubDb = require('../stub-db');
+const Config = require('../stub-config');
+const { ResponseError, ResourceAuthenticatorError } = require('../../lib/errors');
+const Enum = require('../../lib/enum');
+
+describe('Resource Authenticator', function () {
+  const noExpectedException = 'did not receive expected exception';
+  let ra, options;
+
+  beforeEach(function () {
+    stubDb._reset();
+    stubLogger._reset();
+    options = new Config('test');
+    ra = new ResourceAuthenticator(stubLogger, stubDb, options);
+  });
+
+  afterEach(function () {
+    sinon.restore();
+  });
+
+  it('covers no option default', function () {
+    ra = new ResourceAuthenticator(stubLogger, stubDb);
+  });
+
+  describe('currentEpoch', function () {
+    it('covers', function () {
+      const now = 1648836413503;
+      const expected = Math.ceil(now / 1000);
+      sinon.stub(Date, 'now').returns(now);
+      const result = ResourceAuthenticator.currentEpoch;
+      assert.strictEqual(result, expected);
+    });
+  }); // currentEpoch
+
+  describe('Identifier Compaction', function () {
+    it('reciprocates', function () {
+      const identifier = '6eaed948-b1e4-11ec-9a91-0025905f714a';
+      const smaller = ResourceAuthenticator.ensmallenIdentifier(identifier);
+      const bigger = ResourceAuthenticator.embiggenIdentifier(smaller);
+      assert.strictEqual(bigger, identifier);
+    });
+  }); // Identifier Compaction
+
+  describe('getSalt', function () {
+    it('covers', async function () {
+      const result = await ra.getSalt();
+      assert(result);
+      assert.strictEqual(result.length, 28);
+    });
+  }); // getSalt
+
+  describe('createDigest', function () {
+    let secret;
+    beforeEach(function () {
+      secret = 'secret';
+    });
+    it('creates empty digest', function () {
+      const result = ra.createDigest(secret);
+      const expected = '-eZuF5tnR65UEI-C-K3os8Jddv0wr95sOVgixTAZYWk';
+      assert.strictEqual(result, expected);
+    });
+    it('creates digest', function () {
+      const result = ra.createDigest(secret, 'data');
+      const expected = 'GywWt1vSqHDBFBU8zaW8_KYzFLxyL6Fg1pDeEzzLuds';
+      assert.strictEqual(result, expected);
+    });
+  }); // createDigest
+
+  describe('authenticate', function () {
+    it('covers', async function () {
+      const identifier = '6eaed948-b1e4-11ec-9a91-0025905f714a';
+      const secret = 'secrety';
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836029);
+      sinon.stub(ra, 'getSalt').resolves('xxxxx');
+      const expected = 'Bearer bq7ZSLHkEeyakQAlkF9xSg:1648836029:xxxxx:fdUYC8Gqe0nAyX_-SWvRsPsx0UjY-vV-Ff0A52j6Zfw';
+      const result = await ra.authenticate(identifier, secret);
+      assert.strictEqual(result, expected);
+    });
+  }); // authenticate
+
+  describe('required', function () {
+    let resource, res, req, ctx;
+    const validBearerHeader = 'Bearer bq7ZSLHkEeyakQAlkF9xSg:1648836029:xxxxx:fdUYC8Gqe0nAyX_-SWvRsPsx0UjY-vV-Ff0A52j6Zfw';
+    beforeEach(function () {
+      resource = {
+        secret: 'secrety',
+      };
+      req = {
+        getHeader: sinon.stub(),
+      };
+      res = {
+        setHeader: sinon.stub(),
+      };
+      ctx = {
+        params: {},
+        parsedBody: {},
+        queryParams: {},
+        session: {},
+      };  
+    });
+    it('requires auth header', async function () {
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires bearer token', async function () {
+      req.getHeader.returns('Basic Zm9vcABiYXJr');
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires proper bearer token', async function () {
+      req.getHeader.returns('Bearer Zm9vcABiYXJr');
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires identifier to exist', async function () {
+      req.getHeader.returns(validBearerHeader);
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('covers db failure', async function () {
+      const expected = new Error('oh no');
+      ra.db.resourceGet.rejects(expected);
+      req.getHeader.returns(validBearerHeader);
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected, noExpectedException);
+      }
+    });
+    it('requires timestamp within grace', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648838184);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns(validBearerHeader);
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires digest to match', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836031);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns('Bearer bq7ZSLHkEeyakQAlkF9xSg:1648836029:xxxxx:invalid1M2j9wtoerc3Pqe6kRzqFrkrkwqdeYXG331Q');
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('succeeds', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836031);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns(validBearerHeader);
+      await ra.required(req, res, ctx);
+      assert.strictEqual(ra.logger.debug.args[1][1], 'success');
+    });
+    it('covers extra bearer token fields', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836031);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns(validBearerHeader + ':extra');
+      await ra.required(req, res, ctx);
+      assert.deepStrictEqual(ra.logger.debug.args[1][2], { tRest: ['extra'] });
+      assert.strictEqual(ra.logger.debug.args[2][1], 'success');
+    });
+  }); // required
+
+  describe('requestBearer', function () {
+    let res;
+    beforeEach(function () {
+      res = {
+        setHeader: sinon.stub(),
+      };
+    });
+    it('covers default response', function () {
+      try {
+        ResourceAuthenticator.requestBearer(res);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(res.setHeader.called);
+        assert.strictEqual(res.setHeader.args[0][0], 'WWW-Authenticate');
+        assert.strictEqual(res.setHeader.args[0][1], 'Bearer');
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('covers other response', function () {
+      try {
+        ResourceAuthenticator.requestBearer(res, Enum.ErrorResponse.Forbidden);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(res.setHeader.called);
+        assert.strictEqual(res.setHeader.args[0][0], 'WWW-Authenticate');
+        assert.strictEqual(res.setHeader.args[0][1], 'Bearer');
+        assert(e instanceof ResponseError);
+        assert.strictEqual(e.statusCode, 403);
+      }
+    });
+  }); // requestBearer
+
+  describe('ResourceAuthenticatorError', function () {
+    it('covers', function () {
+      const e = new ResourceAuthenticatorError();
+      const result = e.name;
+      assert.strictEqual(result, 'ResourceAuthenticatorError');
+    });
+  }); // ResourceAuthenticationError
+
+}); // Resource Authenticator
\ No newline at end of file
index 888cfa5c10fcb25d07325894023cb2cbe1caa8b0..16243d732daf04ae6ea7400f137a35752e0af044 100644 (file)
@@ -3,7 +3,7 @@
 
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const sinon = require('sinon');
 
 const SessionManager = require('../../lib/session-manager');
index 87bdf0a7070ff9be3f7d81a0224ecf4cf449d5f8..aabb9ace64c7ab3de414af804c12ed993168edbd 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-env mocha */
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const { IAHTML } = require('../../../lib/template');
 const lintHtml = require('../../lint-html');
 
index 04e755584d6ee7c30584ea166b7f20963946b2dd..44d8fbbf250c797a417c542407dabffb812c8c13 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-env mocha */
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const { LoginHTML } = require('../../../lib/template');
 const lintHtml = require('../../lint-html');
 
index 0e84a4950055ae59a1e2d547ef7f45bb8155a16d..2a49a71959245af873001d058b1dcae5c5f657c2 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-env mocha */
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const { OTPHTML } = require('../../../lib/template');
 const lintHtml = require('../../lint-html');
 
index 3bd561965d7d18983a435a1afe2f3e7f23592511..e4ba5295efaef9929035801bff8b5b5302594154 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-env mocha */
 'use strict';
 
-const assert = require('assert');
+const assert = require('node:assert');
 const { SettingsHTML } = require('../../../lib/template');
 const lintHtml = require('../../lint-html');
 
index f5306b37893b14055ff9e02785a9614ec3c33ad1..537c92b88859a7a40c104032aafe8965f72b363c 100644 (file)
@@ -14,6 +14,7 @@ const stubFns = [
   'authenticationSuccess',
   'authenticationUpdateCredential',
   'authenticationUpdateOTPKey',
+  'resourceGet',
 ];
 
 const stubDatabase = {