-
Notifications
You must be signed in to change notification settings - Fork 6
/
index.js
299 lines (253 loc) · 8.07 KB
/
index.js
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
'use strict';
var Dynamis = require('dynamis')
, ms = require('millisecond')
, crypto = require('crypto')
, bs58 = require('bs58');
/**
* Tolkien is a authentication system that allows users to login to your system
* without the use a password. It generates one time tokens for logging in.
*
* Options:
*
* - store: A store for the tokens. If no store is used will attempt to generate
* one from the options.
* - type: Type of store to generate (redis, memcached, couchdb).
* - client: Reference to the created store client.
* - expire: Expire time of a generated token.
* - namespace: Prefix the keys that we add.
*
* @constructor
* @param {Object} options Configuration.
* @api public
*/
function Tolkien(options) {
if (!this) return new Tolkien(options);
options = options || {};
this.store = options.store || new Dynamis(options.type, options.client, options);
this.expire = ms(options.expire || '5 minutes');
this.ns = options.namespace || 'tolkien:';
this.ratelimit = options.ratelimit || 3;
this.services = Object.create(null);
}
Tolkien.extend = require('extendible');
/**
* Received a login attempt, validate data, store generated token and send to
* user.
*
* @param {Object} data Data object with user information.
* @param {Function} fn Completion callback.
* @returns {Tolkien}
* @api public
*/
Tolkien.prototype.login = function login(data, fn) {
var service = this.services[data.service]
, tolkien = this
, ns = this.ns
, id = data.id
, err;
if (!Object.keys(tolkien.services).length) {
err = new Error('No authentication service configured.');
} else if (!id) {
err = new Error('Missing user id, cannot generate a token.');
} else if (!data.service) {
err = new Error('Missing authentication service.');
} else if (!service) {
err = new Error('Invalid or unknown authentication service selected.');
}
if (err) setImmediate(fn.bind(fn, err));
else tolkien.get(data, function get(err, result) {
if (!err && result) {
err = new Error('Please wait until the old token expires.');
}
if (err) return fn(err);
tolkien[service.type](service.size, function generated(err, token) {
if (err) return fn(err);
data.token = token;
tolkien.set(data, service.expire, function stored(err) {
//
// @TODO we might want to remove the token if the operation failed to
// ensure that we leave no "dead" tokens behind making it unable to
// login because of a pending token.
//
if (err) return fn(err);
service.send(data, function reply(err, response) {
data.response = response;
fn(err, data);
});
});
});
});
return tolkien;
};
/**
* Validate the given token.
*
* @param {Object} data Data object with id and token information.
* @param {Function} fn Completion callback.
* @returns {Tolkien}
* @api public
*/
Tolkien.prototype.validate = function validate(data, fn) {
var tolkien = this
, ns = this.ns
, err;
if (!data.token) {
err = new Error('Missing token, cannot validate request.');
} else if (!data.id) {
err = new Error('Missing user id, cannot validate token.');
}
if (err) setImmediate(fn.bind(fn, err));
else this.get(data, function get(err, result) {
if (err || !result) return fn(err, false, data);
//
// The get method returns the id belongs to the token or the token that
// belongs to our given id so we need to check if both are the same in order
// to confirm a validated id and token match. If it's invalid we don't need
// to delete tokens as they will auto expire and we don't want to remove the
// wrong id or token.
//
var validates = data.token === result.token && data.id === result.id;
if (!validates) return fn(err, validates, data);
tolkien.remove(data, function deleted(err) {
fn(err, validates, data);
});
});
return this;
};
/**
* Get the token that belongs to the id or the id that belongs to the token.
*
* @param {Object} data Data object which has the user id or token.
* @param {Function} fn Completion callback.
* @returns {Tolkien}
* @api private
*/
Tolkien.prototype.get = function get(data, fn) {
var result = {};
if (data.token) result.token = data.token;
if (data.id) result.id = data.id;
if (data.id) this.store.get(this.ns +'id:'+ data.id, function get(err, token) {
if (!token) return fn();
result.token = token;
fn(err, result);
});
else this.store.get(this.ns + 'token:'+ data.token, function get(err, id) {
if (!id) return fn();
result.id = id;
fn(err, result);
});
return this;
};
/**
* Store the token and the id in the supplied store.
*
* @param {Object} data Data object which has the user id or token.
* @param {Function} fn Completion callback.
* @returns {Tolkien}
* @api private
*/
Tolkien.prototype.set = function set(data, expire, fn) {
var ns = this.ns
, errors = []
, calls = 0;
function next(err) {
if (err) errors.push(err);
if (++calls === 2) fn(errors.pop(), data);
}
//
// We need to transform the expiree to a seconds instead of milliseconds.
//
this.store.set(ns +'token:'+ data.token, data.id, expire / 1000, next);
this.store.set(ns +'id:'+ data.id, data.token, expire / 1000, next);
return this;
};
/**
* Remove token and id from the store.
*
* @param {Object} data Data object which has the user id or token.
* @param {Function} fn Completion callback.
* @returns {Tolkien}
* @api private
*/
Tolkien.prototype.remove = function remove(data, fn) {
var ns = this.ns
, errors = []
, calls = 0;
function next(err) {
if (err) errors.push(err);
if (++calls === 2) fn(errors.pop(), data);
}
this.store.del(ns +'token:'+ data.token, next);
this.store.del(ns +'id:'+ data.id, next);
return this;
};
/**
* Register a new service that will be used to send the generated login token.
*
* The following options are accepted:
*
* - type: What kind of token do you want to generate? A `number` or a `token`.
* Numbers are ideal for SMS/TXT authentication.
* - size: The maximum size of the token in bytes or maximum length.
*
* @param {String} name Name of service.
* @param {Function} fn Callback for token and user information for sending.
* @param {Object} options Additional configuration for your service.
* @returns {Tolkien}
* @api public
*/
Tolkien.prototype.service = function service(name, fn, options) {
options = options || {};
var types = ['token', 'number']
, type = options.type || 'token'
, size = options.size || ('token' === type ? 16 : 9999);
if (!~types.indexOf(type)) {
throw new Error('Unknown type option ('+ type +') only accepts: '+ types.join());
} else if ('function' !== typeof fn) {
throw new Error('Invalid callback supplied, please make sure its a function');
}
this.services[name] = {
expire: ms(options.expire || this.expire),
size: size,
type: type,
send: fn
};
return this;
};
/**
* Generate a new random token which we can use.
*
* @param {Number} size The maximum length of token in bytes.
* @param {Function} fn Completion callback that receives the token.
* @returns {Tolkien}
* @api public
*/
Tolkien.prototype.token = function token(size, fn) {
crypto.randomBytes(size, function generated(err, buffer) {
if (err) return fn(err);
fn(err, bs58.encode(buffer));
});
return this;
};
/**
* Generate a new number which we can use SMS authentication as it's much easier
* to type on a phone. We already know that the user is owner of the phone so it
* doesn't matter if this is bit less cryptographically strong then a generated
* email token.
*
* @param {Number} size The maximum length of token in bytes.
* @param {Function} fn Completion callback that receives the token.
* @returns {Tolkien}
* @api public
*/
Tolkien.prototype.number = function number(max, fn) {
crypto.randomBytes(4, function generated(err, buffer) {
if (err) return fn(err);
fn(err, Math.floor(buffer.readUInt32BE(0) % max));
});
return this;
};
//
// Expose the module.
//
module.exports = Tolkien;