await this.db.context(async (dbCtx) => {
authData = await this.db.getAuthById(dbCtx, authenticationId);
});
- const secret = authData && authData.secret;
+ const secret = authData?.secret;
if (!secret) {
this.logger.debug(_scope, 'failed, invalid authenticationId', { ctx });
return false;
}
- // Update pwhash
- // authData.password = await argon2.hash(newPassword, { type: argon2.id });
if (authData.password.startsWith('$argon2')) {
if (await argon2.verify(authData.password, authenticationPass)) {
+ this.logger.debug(_scope, 'passed argon2 verify', { ctx });
+ } else {
this.logger.debug(_scope, 'failed argon2 verify', { ctx });
return false;
- } else {
- this.logger.debug(_scope, 'passed argon2 verify', { ctx });
}
} else {
- if (authData.password !== authenticationPass) {
+ if (authData.password.length !== authenticationPass.length
+ || !crypto.timingSafeEqual(Buffer.from(authData.password), Buffer.from(authenticationPass))
+ ) {
this.logger.debug(_scope, 'failed, password mismatch', { ctx });
return false;
}
+ // Update pwhash
+ const credential = await argon2.hash(authenticationPass, { type: argon2.argon2id });
+ await this.db.context(async (dbCtx) => {
+ await this.db.upsertAuth(dbCtx, authenticationId, authData.secret, credential);
+ });
+ this.logger.debug(_scope, 'migrated plain password', { ctx, authenticationId });
}
ctx.authenticationId = authenticationId;
return this.requestBasic(res);
}
- const linkId = ctx.params && ctx.params.id;
+ const linkId = ctx?.params?.id;
// If there is an id parameter, check for a valid token query parameter
if (linkId) {
authData = (ctx?.queryParams?.token) || (ctx?.parsedBody?.token);
}
// Allow a valid plain token.
- const linkId = ctx.params && ctx.params.id;
+ const linkId = ctx?.params?.id;
if (linkId) {
- const token = (ctx.queryParams && ctx.queryParams.token) || (ctx.parsedBody && ctx.parsedBody.token);
+ const token = ctx?.queryParams?.token || ctx?.parsedBody?.token;
if (token) {
const validToken = await this.isValidToken(linkId, token);
if (validToken) {
this._notImplemented('getAuthById', { dbCtx, id });
}
+ async upsertAuth(dbCtx, id, secert, credential) {
+ this._notImplemented('upsertAuthCredential', { dbCtx, id, credential });
+ }
+
async insertLink(dbCtx, id, url, authToken) {
this._notImplemented('insertLink', { dbCtx, id, url, authToken });
}
}
+ async upsertAuth(dbCtx, id, secret, credential) {
+ const _scope = _fileScope('upsertAuth');
+ this.logger.debug(_scope, 'called', { id });
+ dbCtx = dbCtx || this.db;
+
+ const result = await dbCtx.result(this.statement.authUpsert, { id, secret, credential });
+ this.logger.debug(_scope, 'result', PostgresDatabase._resultLog(result) );
+ }
+
+
static _epochFix(epoch) {
switch (epoch) {
case Infinity:
--- /dev/null
+--
+INSERT INTO auth (id, secret, password) VALUES ($(id), $(secret), $(credential))
+ON CONFLICT (id) DO
+UPDATE SET password = $(credential), secret = $(secret)
_commit: this.db.prepare('COMMIT'),
_rollback: this.db.prepare('ROLLBACK'),
getAuthById: this.db.prepare('SELECT * FROM auth WHERE id = :id'),
+ insertAuth: this.db.prepare('INSERT INTO auth (id, secret, password) VALUES (:id, :secret, :credential)'),
+ updateAuth: this.db.prepare('UPDATE auth SET password = :credential, secret = :secret WHERE id = :id'),
getLinkById: this.db.prepare('SELECT * FROM link WHERE id = :id'),
getLinkByUrl: this.db.prepare('SELECT * FROM link WHERE url = :url'),
insertLink: this.db.prepare('INSERT INTO link (id, url, auth_token) VALUES (:id, :url, :authToken)'),
return auth;
}
+ async upsertAuth(dbCtx, id, secret, credential) {
+ const _scope = _fileScope('upsertAuthCredential');
+ this.logger.debug(_scope, 'called', { id });
+
+ let info;
+ try {
+ info = this.statement.insertAuth.run({ id, secret, credential });
+ } catch (e) {
+ switch (e.code) {
+ case 'SQLITE_CONSTRAINT_UNIQUE':
+ case 'SQLITE_CONSTRAINT_PRIMARYKEY': {
+ this.logger.debug(_scope, 'updating existing auth', { id });
+ info = this.statement.updateAuth.run({ id, secret, credential });
+ break;
+ }
+
+ default: {
+ this.logger.error(_scope, 'failed to upsert auth credential', { error: e, id });
+ throw e;
+ }
+ }
+ }
+ this.logger.debug(_scope, 'run', { info });
+ if (info.changes != 1) {
+ this.logger.error(_scope, 'failed to upsert auth credential', { id, info });
+ throw new DBErrors.UnexpectedResult();
+ }
+
+ return this._sqliteInfo(info);
+ }
+
async insertLink(dbCtx, id, url, authToken) {
const _scope = _fileScope('insertLink');
this.logger.debug(_scope, 'called', { id, url });
context: async (fn) => fn({}),
getAuthById: async () => {},
getLinkById: async () => {},
+ upsertAuth: async () => {},
};
authenticator = new Authenticator(logger, db, options);
});
beforeEach(function () {
sinon.stub(authenticator, 'requestBasic');
+ sinon.stub(authenticator.db, 'getAuthById');
+ sinon.stub(authenticator.db, 'upsertAuth');
credentials = 'id:password';
ctx = {};
});
- it('accepts credentials', async function () {
- sinon.stub(authenticator.db, 'getAuthById').resolves({ password: 'password' });
+ it('accepts plain credential and migrates to hash', async function () {
+ authenticator.db.getAuthById.resolves({ password: 'password' });
const result = await authenticator.isValidBasic(credentials, ctx);
assert.strictEqual(result, true);
assert.strictEqual(ctx.authenticationId, 'id');
+ assert(authenticator.db.upsertAuth.called);
});
- it('rejects wrong password', async function () {
- sinon.stub(authenticator.db, 'getAuthById').resolves({ password: 'wrong_password' });
+ it('rejects wrong plain credential', async function () {
+ authenticator.db.getAuthById.resolves({ password: 'wrong_password' });
const result = await authenticator.isValidBasic(credentials, ctx);
assert.strictEqual(result, false);
assert(!('authenticationId' in ctx));
+ assert(authenticator.db.upsertAuth.notCalled);
+ });
+
+ it('accepts argon2 credential', async function () {
+ authenticator.db.getAuthById.resolves({ password: '$argon2id$v=19$m=65536,t=3,p=4$AQKIWU5puGDs3zKIPMo0Ew$Mzl/kzJE6/oRtJLHoGXaoUtlAiXs5HK2qLgHWF6euF8' });
+ const result = await authenticator.isValidBasic(credentials, ctx);
+ assert.strictEqual(result, true);
+ assert.strictEqual(ctx.authenticationId, 'id');
+ assert(authenticator.db.upsertAuth.notCalled);
+ });
+
+ it('rejects wrong argon2 credential', async function () {
+ authenticator.db.getAuthById.resolves({ password: '$argon2id$v=19$m=65536,t=3,p=4$BCPlf0NBgjyXOxdyUDs/FQ$wV4jERm50yByCpSr8lrD8Nu0uVPUsQcVghJQoix5ido' });
+ const result = await authenticator.isValidBasic(credentials, ctx);
+ assert.strictEqual(result, false);
+ assert(!('authenticationId' in ctx));
+ assert(authenticator.db.upsertAuth.notCalled);
});
it('rejects missing id', async function () {
- sinon.stub(authenticator.db, 'getAuthById').resolves();
+ authenticator.db.getAuthById.resolves();
const result = await authenticator.isValidBasic(credentials, ctx);
assert.strictEqual(result, false);
assert(!('authenticationId' in ctx));
+ assert(authenticator.db.upsertAuth.notCalled);
});
}); // isValidBasic
'context',
'transaction',
'getAuthById',
+ 'upsertAuth',
'insertLink',
'getLinkById',
'getLinkByUrl',
dbCtx = undefined;
});
+ it('covers constructor options', function () {
+ options = {
+ queryLogLevel: 'debug',
+ };
+ db = new PostgresDatabase(logger, options, pgpStub);
+ });
+
describe('context', function () {
it('covers', async function () {
const fn = sinon.stub();
});
}); // getAuthById
+ describe('upsertAuth', function () {
+ let id, secret, credential;
+ beforeEach(function () {
+ id = 'id';
+ secret = 'secret';
+ credential = 'credential';
+ });
+ it('stubbed success', async function () {
+ await db.upsertAuth(dbCtx, id, secret, credential);
+ });
+ }); // upsertAuth
+
describe('_epochFix', function () {
it('clamps infinity', function () {
const epoch = Infinity;
await db.transaction(dbCtx, fn);
assert(fn.called);
});
+ it('covers rollback', async function () {
+ const fn = sinon.stub();
+ fn.throws(new Error('rollback'));
+ try {
+ await db.transaction(dbCtx, fn);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert.strictEqual(e.message, 'rollback', noExpectedException);
+ }
+ });
}); // transaction
describe('getAuthById', function () {
});
}); // getAuthById
+ describe('upsertAuth', function () {
+ let id, secret, credential;
+ beforeEach(function () {
+ sinon.stub(db.statement.insertAuth, 'run').returns({ changes: 1n, lastInsertRowid: 123n });
+ sinon.stub(db.statement.updateAuth, 'run').returns({ changes: 1n, lastInsertRowid: 123n });
+ });
+ it('stubbed insert success', async function () {
+ await db.upsertAuth(dbCtx, id, secret, credential);
+ });
+ it('stubbed update success', async function () {
+ db.statement.insertAuth.run.throws({ code: 'SQLITE_CONSTRAINT_UNIQUE' });
+ await db.upsertAuth(dbCtx, id, secret, credential);
+ });
+ it('covers error', async function () {
+ const expectedException = new Error('blah');
+ db.statement.insertAuth.run.throws(expectedException);
+ try {
+ await db.upsertAuth(dbCtx, id, secret, credential);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert.deepStrictEqual(e, expectedException, noExpectedException);
+ }
+ });
+ it('covers unexpected error', async function () {
+ const expectedException = DBErrors.UnexpectedResult;
+ const returns = {
+ changes: 0n,
+ lastInsertRowid: undefined,
+ };
+ db.statement.insertAuth.run.returns(returns);
+ try {
+ await db.upsertAuth(dbCtx, id, secret, credential);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof expectedException, noExpectedException);
+ }
+ });
+ }); // upsertAuth
+
describe('insertLink', function () {
let id, url, authToken;
beforeEach(function () {