Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39539
1
###
2
User sign in
3
4
Throttling policy: It basically like this, except we reset the counters
5
each minute and hour, so a crafty attacker could get twice as many tries by finding the
6
reset interval and hitting us right before and after. This is an acceptable tradeoff
7
for making the data structure trivial.
8
9
* POLICY 1: A given email address is allowed at most 3 failed login attempts per minute.
10
* POLICY 2: A given email address is allowed at most 30 failed login attempts per hour.
11
* POLICY 3: A given ip address is allowed at most 10 failed login attempts per minute.
12
* POLICY 4: A given ip address is allowed at most 50 failed login attempts per hour.
13
###
14
15
async = require('async')
16
17
message = require('smc-util/message')
18
misc = require('smc-util/misc')
19
{required, defaults} = misc
20
21
auth = require('./auth')
22
23
sign_in_fails =
24
email_m : {}
25
email_h : {}
26
ip_m : {}
27
ip_h : {}
28
29
clear_sign_in_fails_m = () ->
30
sign_in_fails.email_m = {}
31
sign_in_fails.ip_m = {}
32
33
clear_sign_in_fails_h = () ->
34
sign_in_fails.email_h = {}
35
sign_in_fails.ip_h = {}
36
37
_sign_in_fails_intervals = undefined
38
39
record_sign_in_fail = (opts) ->
40
{email, ip, logger} = defaults opts,
41
email : required
42
ip : required
43
logger : undefined
44
if not _sign_in_fails_intervals?
45
# only start clearing if there has been a failure...
46
_sign_in_fails_intervals = [setInterval(clear_sign_in_fails_m, 60000), setInterval(clear_sign_in_fails_h, 60*60000)]
47
48
logger?("WARNING: record_sign_in_fail(#{email}, #{ip})")
49
s = sign_in_fails
50
if not s.email_m[email]?
51
s.email_m[email] = 0
52
if not s.ip_m[ip]?
53
s.ip_m[ip] = 0
54
if not s.email_h[email]?
55
s.email_h[email] = 0
56
if not s.ip_h[ip]?
57
s.ip_h[ip] = 0
58
s.email_m[email] += 1
59
s.email_h[email] += 1
60
s.ip_m[ip] += 1
61
s.ip_h[ip] += 1
62
63
sign_in_check = (opts) ->
64
{email, ip} = defaults opts,
65
email : required
66
ip : required
67
s = sign_in_fails
68
if s.email_m[email] > 3
69
# A given email address is allowed at most 3 failed login attempts per minute
70
return "Wait a minute, then try to login again. If you can't remember your password, reset it or email [email protected]."
71
if s.email_h[email] > 30
72
# A given email address is allowed at most 30 failed login attempts per hour.
73
return "Wait an hour, then try to login again. If you can't remember your password, reset it or email [email protected]."
74
if s.ip_m[ip] > 10
75
# A given ip address is allowed at most 10 failed login attempts per minute.
76
return "Wait a minute, then try to login again. If you can't remember your password, reset it or email [email protected]."
77
if s.ip_h[ip] > 50
78
# A given ip address is allowed at most 50 failed login attempts per hour.
79
return "Wait an hour, then try to login again. If you can't remember your password, reset it or email [email protected]."
80
return false
81
82
exports.sign_in = (opts) ->
83
{client, mesg} = opts = defaults opts,
84
client : required
85
mesg : required
86
logger : undefined
87
database : required
88
host : undefined
89
port : undefined
90
cb : undefined
91
92
if opts.logger?
93
dbg = (m) ->
94
opts.logger.debug("sign_in(#{mesg.email_address}): #{m}")
95
dbg()
96
else
97
dbg = ->
98
tm = misc.walltime()
99
100
sign_in_error = (error) ->
101
dbg("sign_in_error -- #{error}")
102
exports.record_sign_in
103
database : opts.database
104
ip_address : client.ip_address
105
successful : false
106
email_address : mesg.email_address
107
account_id : account?.account_id
108
client.push_to_client(message.sign_in_failed(id:mesg.id, email_address:mesg.email_address, reason:error))
109
opts.cb?(error)
110
111
if not mesg.email_address
112
sign_in_error("Empty email address.")
113
return
114
115
if not mesg.password
116
sign_in_error("Empty password.")
117
return
118
119
mesg.email_address = misc.lower_email_address(mesg.email_address)
120
121
m = sign_in_check
122
email : mesg.email_address
123
ip : client.ip_address
124
if m
125
sign_in_error("sign_in_check fail(ip=#{client.ip_address}): #{m}")
126
return
127
128
signed_in_mesg = undefined
129
account = undefined
130
async.series([
131
(cb) ->
132
dbg("get account and check credentials")
133
# NOTE: Despite people complaining, we do give away info about whether
134
# the e-mail address is for a valid user or not.
135
# There is no security in not doing this, since the same information
136
# can be determined via the invite collaborators feature.
137
opts.database.get_account
138
email_address : mesg.email_address
139
columns : ['password_hash', 'account_id', 'passports']
140
cb : (err, _account) ->
141
account = _account; cb(err)
142
(cb) ->
143
dbg("got account; now checking if password is correct...")
144
auth.is_password_correct
145
database : opts.database
146
account_id : account.account_id
147
password : mesg.password
148
password_hash : account.password_hash
149
cb : (err, is_correct) ->
150
if err
151
cb("Error checking correctness of password -- #{err}")
152
return
153
if not is_correct
154
if not account.password_hash
155
cb("The account #{mesg.email_address} exists but doesn't have a password. Either set your password by clicking 'Forgot Password?' or log in using #{misc.keys(account.passports).join(', ')}. If that doesn't work, email [email protected] and we will sort this out.")
156
else
157
cb("Incorrect password for #{mesg.email_address}. You can reset your password by clicking the 'Forgot Password?' link. If that doesn't work, email [email protected] and we will sort this out.")
158
else
159
cb()
160
# remember me
161
(cb) ->
162
if mesg.remember_me
163
dbg("remember_me -- setting the remember_me cookie")
164
signed_in_mesg = message.signed_in
165
id : mesg.id
166
account_id : account.account_id
167
email_address : mesg.email_address
168
remember_me : false
169
hub : opts.host + ':' + opts.port
170
client.remember_me
171
account_id : signed_in_mesg.account_id
172
email_address : signed_in_mesg.email_address
173
cb : cb
174
else
175
cb()
176
], (err) ->
177
if err
178
dbg("send error to user (in #{misc.walltime(tm)}seconds) -- #{err}")
179
sign_in_error(err)
180
opts.cb?(err)
181
else
182
dbg("user got signed in fine (in #{misc.walltime(tm)}seconds) -- sending them a message")
183
client.signed_in(signed_in_mesg)
184
client.push_to_client(signed_in_mesg)
185
opts.cb?()
186
)
187
188
exports.sign_in_using_auth_token = (opts) ->
189
{client, mesg} = opts = defaults opts,
190
client : required
191
mesg : required
192
logger : undefined
193
database : required
194
host : undefined
195
port : undefined
196
cb : undefined
197
198
if opts.logger?
199
dbg = (m) ->
200
opts.logger.debug("sign_in_using_auth_token(#{mesg.email_address}): #{m}")
201
dbg()
202
else
203
dbg = ->
204
tm = misc.walltime()
205
206
sign_in_error = (error) ->
207
dbg("sign_in_using_auth_token_error -- #{error}")
208
exports.record_sign_in
209
database : opts.database
210
ip_address : client.ip_address
211
successful : false
212
email_address : mesg.auth_token # yes, we abuse the email_address field
213
account_id : account?.account_id
214
client.push_to_client(message.error(id:mesg.id, error:error))
215
opts.cb?(error)
216
217
if not mesg.auth_token
218
sign_in_error("missing auth_token.")
219
return
220
221
if mesg.auth_token?.length != 24
222
sign_in_error("auth_token must be exactly 24 characters long")
223
return
224
225
m = sign_in_check
226
email : mesg.auth_token
227
ip : client.ip_address
228
if m
229
sign_in_error("sign_in_check fail(ip=#{client.ip_address}): #{m}")
230
return
231
232
signed_in_mesg = undefined
233
account = account_id = undefined
234
async.series([
235
(cb) ->
236
dbg("get account and check credentials")
237
# NOTE: Despite people complaining, we do give away info about whether
238
# the e-mail address is for a valid user or not.
239
# There is no security in not doing this, since the same information
240
# can be determined via the invite collaborators feature.
241
opts.database.get_auth_token_account_id
242
auth_token : mesg.auth_token
243
cb : (err, _account_id) ->
244
if not err and not _account_id
245
err = 'auth_token is not valid'
246
account_id = _account_id; cb(err)
247
(cb) ->
248
dbg("successly got account_id; now getting more information about the account")
249
opts.database.get_account
250
account_id : account_id
251
columns : ['email_address']
252
cb : (err, _account) ->
253
account = _account; cb(err)
254
# remember me
255
(cb) ->
256
dbg("remember_me -- setting the remember_me cookie")
257
signed_in_mesg = message.signed_in
258
id : mesg.id
259
account_id : account_id
260
email_address : account.email_address
261
remember_me : false
262
hub : opts.host + ':' + opts.port
263
client.remember_me
264
account_id : signed_in_mesg.account_id
265
email_address : signed_in_mesg.email_address
266
ttl : 12*3600
267
cb : cb
268
], (err) ->
269
if err
270
dbg("send error to user (in #{misc.walltime(tm)}seconds) -- #{err}")
271
sign_in_error(err)
272
opts.cb?(err)
273
else
274
dbg("user got signed in fine (in #{misc.walltime(tm)}seconds) -- sending them a message")
275
client.signed_in(signed_in_mesg)
276
client.push_to_client(signed_in_mesg)
277
opts.cb?()
278
)
279
280
281
# Record to the database a failed and/or successful login attempt.
282
exports.record_sign_in = (opts) ->
283
opts = defaults opts,
284
ip_address : required
285
successful : required
286
database : required
287
email_address : undefined
288
account_id : undefined
289
remember_me : false
290
if not opts.successful
291
record_sign_in_fail
292
email : opts.email_address
293
ip : opts.ip_address
294
else
295
opts.database.log
296
event : 'successful_sign_in'
297
value :
298
ip_address : opts.ip_address
299
email_address : opts.email_address ? null
300
remember_me : opts.remember_me
301
account_id : opts.account_id
302
303