Added API key authentication.
This commit is contained in:
103
src/auth.js
103
src/auth.js
@@ -4,13 +4,17 @@ import crypto from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { shapes } from '@dicebear/collection';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import redis from './redis.js';
|
||||
import { curateUser, fetchUser } from './users.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import initLogger from './logger.js';
|
||||
import slugify from '../utils/slugify.js';
|
||||
import initLogger, { initAccessLogger } from './logger.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const accessLogger = initAccessLogger();
|
||||
const scrypt = util.promisify(crypto.scrypt);
|
||||
|
||||
async function verifyPassword(password, storedPassword) {
|
||||
@@ -138,3 +142,100 @@ export async function signup(credentials, userIp) {
|
||||
|
||||
return fetchUser(userId);
|
||||
}
|
||||
|
||||
function curateKey(key) {
|
||||
return {
|
||||
id: key.id,
|
||||
identifier: key.identifier,
|
||||
lastUsedAt: key.last_used_at,
|
||||
lastUsedIp: key.last_used_ip,
|
||||
createdAt: key.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchUserKeys(reqUser) {
|
||||
const keys = await knex('users_keys')
|
||||
.where('user_id', reqUser.id)
|
||||
.orderBy('created_at', 'asc');
|
||||
|
||||
return keys.map((key) => curateKey(key));
|
||||
}
|
||||
|
||||
export async function verifyKey(userId, key, req) {
|
||||
if (!key || !userId) {
|
||||
throw new HttpError('The API credentials are not provided.', 401);
|
||||
}
|
||||
|
||||
const hashedKey = (await scrypt(key, '', 64)).toString('hex'); // salt redundant for randomly generated key
|
||||
|
||||
const storedKey = await knex('users_keys')
|
||||
.where('user_id', userId)
|
||||
.where('key', hashedKey)
|
||||
.first();
|
||||
|
||||
if (!storedKey) {
|
||||
throw new HttpError('The API credentials are invalid.', 401);
|
||||
}
|
||||
|
||||
accessLogger.access({
|
||||
userId,
|
||||
identifier: storedKey.identifier,
|
||||
ip: req.userIp,
|
||||
});
|
||||
|
||||
knex('users_keys')
|
||||
.where('id', storedKey.id)
|
||||
.update('last_used_at', knex.raw('now()'))
|
||||
.update('last_used_ip', req.userIp)
|
||||
.then(() => {
|
||||
// no need to wait for this
|
||||
});
|
||||
}
|
||||
|
||||
export async function createKey(reqUser) {
|
||||
const cooldownKey = `traxxx:key_create_cooldown:${reqUser.id}`;
|
||||
|
||||
if (reqUser.role !== 'admin' && await redis.exists(cooldownKey)) {
|
||||
throw new HttpError(`You can only create a new API key once every ${config.apiAccess.keyCooldown} minutes.`, 429);
|
||||
}
|
||||
|
||||
const keys = await fetchUserKeys(reqUser);
|
||||
|
||||
if (keys.length >= config.apiAccess.keyLimit) {
|
||||
throw new HttpError(`You can only hold ${config.apiAccess.keyLimit} API keys at one time. Please remove a key.`, 400);
|
||||
}
|
||||
|
||||
const key = crypto.randomBytes(config.apiAccess.keySize).toString('base64url');
|
||||
const hashedKey = (await scrypt(key, '', 64)).toString('hex'); // salt redundant for randomly generated key
|
||||
|
||||
const identifier = slugify([faker.word.adjective(), faker.animal[faker.animal.type()]()]);
|
||||
|
||||
const [newKey] = await knex('users_keys')
|
||||
.insert({
|
||||
user_id: reqUser.id,
|
||||
key: hashedKey,
|
||||
identifier,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
await redis.set(cooldownKey, identifier);
|
||||
await redis.expire(cooldownKey, config.apiAccess.keyCooldown * 60);
|
||||
|
||||
return {
|
||||
...curateKey(newKey),
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeUserKey(reqUser, identifier) {
|
||||
await knex('users_keys')
|
||||
.where('user_id', reqUser.id)
|
||||
.where('identifier', identifier)
|
||||
.delete();
|
||||
}
|
||||
|
||||
export async function flushUserKeys(reqUser) {
|
||||
await knex('users_keys')
|
||||
.where('user_id', reqUser.id)
|
||||
.delete();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user