forked from SmartDropLabs/smartdrop-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapiKeys.js
More file actions
128 lines (106 loc) · 2.96 KB
/
Copy pathapiKeys.js
File metadata and controls
128 lines (106 loc) · 2.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
const crypto = require('crypto');
const cache = require('./cache');
const config = require('../config');
const KEY_PREFIX = 'api_key:';
const HASH_PREFIX = 'api_key_hash:';
const IDS_KEY = 'api_keys';
function hashApiKey(apiKey) {
return crypto.createHash('sha256').update(apiKey).digest('hex');
}
function constantTimeSecretEqual(actual, expected) {
const actualDigest = crypto.createHash('sha256').update(actual).digest();
const expectedDigest = crypto.createHash('sha256').update(expected).digest();
return crypto.timingSafeEqual(actualDigest, expectedDigest);
}
function sanitize(record) {
if (!record) return null;
const { key_hash, ...safe } = record;
return safe;
}
function generateApiKey() {
return crypto.randomBytes(32).toString('hex');
}
function keyId() {
return `key_${crypto.randomUUID().replace(/-/g, '')}`;
}
function keyPath(id) {
return `${KEY_PREFIX}${id}`;
}
function hashPath(hash) {
return `${HASH_PREFIX}${hash}`;
}
async function getKey(id) {
return cache.get(keyPath(id));
}
async function listKeys() {
const redis = cache.getClient();
const ids = await redis.zrevrange(IDS_KEY, 0, -1);
const records = await Promise.all(ids.map((id) => getKey(id)));
return records.filter(Boolean).map(sanitize);
}
async function createKey({ label, scopes = ['default'] }) {
const apiKey = generateApiKey();
const hashed = hashApiKey(apiKey);
const now = new Date().toISOString();
const record = {
id: keyId(),
label,
key_prefix: apiKey.slice(0, 8),
key_hash: hashed,
scopes,
created_at: now,
last_used_at: null,
};
const redis = cache.getClient();
await cache.set(keyPath(record.id), record);
await cache.set(hashPath(hashed), record.id);
await redis.zadd(IDS_KEY, Date.now(), record.id);
return {
api_key: apiKey,
key: sanitize(record),
};
}
async function revokeKey(id) {
const record = await getKey(id);
if (!record) return null;
const redis = cache.getClient();
await cache.del(keyPath(id));
await cache.del(hashPath(record.key_hash));
await redis.zrem(IDS_KEY, id);
return sanitize(record);
}
async function touch(record) {
const updated = {
...record,
last_used_at: new Date().toISOString(),
};
await cache.set(keyPath(record.id), updated);
return sanitize(updated);
}
async function validateApiKey(apiKey) {
if (!apiKey) return null;
if (config.auth.adminApiKey && constantTimeSecretEqual(apiKey, config.auth.adminApiKey)) {
return {
id: 'admin',
label: 'Bootstrap admin key',
key_prefix: apiKey.slice(0, 8),
scopes: ['admin'],
created_at: null,
last_used_at: new Date().toISOString(),
};
}
const hashed = hashApiKey(apiKey);
const id = await cache.get(hashPath(hashed));
if (!id) return null;
const record = await getKey(id);
if (!record || record.key_hash !== hashed) return null;
return touch(record);
}
module.exports = {
createKey,
getKey,
hashApiKey,
listKeys,
revokeKey,
validateApiKey,
};