Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39538
1
###
2
Password reset and change functionality.
3
###
4
5
async = require('async')
6
misc = require('smc-util/misc')
7
message = require('smc-util/message') # message protocol between front-end and back-end
8
email = require('./email')
9
{defaults, required} = misc
10
{is_valid_password} = require('./create-account')
11
auth = require('./auth')
12
13
exports.forgot_password = (opts) ->
14
opts = defaults opts,
15
mesg : required
16
database : required
17
ip_address : required
18
cb : required
19
###
20
Send an email message to the given email address with a code that
21
can be used to reset the password for a certain account.
22
23
Anti-spam/DOS throttling policies:
24
* a given email address can be sent at most 30 password resets per hour
25
* a given ip address can send at most 100 password reset request per minute
26
* a given ip can send at most 250 per hour
27
###
28
if opts.mesg.event != 'forgot_password'
29
opts.cb("Incorrect message event type: #{opts.mesg.event}")
30
return
31
32
# This is an easy check to save work and also avoid empty email_address, which causes trouble below
33
if not misc.is_valid_email_address(opts.mesg.email_address)
34
opts.cb("Invalid email address.")
35
return
36
37
opts.mesg.email_address = misc.lower_email_address(opts.mesg.email_address)
38
39
id = null
40
async.series([
41
(cb) ->
42
# Record this password reset attempt in our database
43
opts.database.record_password_reset_attempt
44
email_address : opts.mesg.email_address
45
ip_address : opts.ip_address
46
cb : cb
47
(cb) ->
48
# POLICY 1: We limit the number of password resets that an email address can receive
49
opts.database.count_password_reset_attempts
50
email_address : opts.mesg.email_address
51
age_s : 60*60 # 1 hour
52
cb : (err, count) ->
53
if err
54
cb(err)
55
else if count >= 31
56
cb("Too many password resets for this email per hour; try again later.")
57
else
58
cb()
59
60
(cb) ->
61
# POLICY 2: a given ip address can send at most 10 password reset requests per minute
62
opts.database.count_password_reset_attempts
63
ip_address : opts.ip_address
64
age_s : 60 # 1 minute
65
cb : (err, count) ->
66
if err
67
cb(err)
68
else if count > 10
69
cb("Too many password resets per minute; try again later.")
70
else
71
cb()
72
(cb) ->
73
# POLICY 3: a given ip can send at most 60 per hour
74
opts.database.count_password_reset_attempts
75
ip_address : opts.ip_address
76
age_s : 60*60 # 1 hour
77
cb : (err, count) ->
78
if err
79
cb(err)
80
else if count > 60
81
cb("Too many password resets per hour; try again later.")
82
else
83
cb()
84
(cb) ->
85
opts.database.account_exists
86
email_address : opts.mesg.email_address
87
cb : (err, exists) ->
88
if err
89
cb(err)
90
else if not exists
91
cb("No account with e-mail address #{opts.mesg.email_address}")
92
else
93
cb()
94
(cb) ->
95
# We now know that there is an account with this email address.
96
# put entry in the password_reset uuid:value table with ttl of
97
# 1 hour, and send an email
98
opts.database.set_password_reset
99
email_address : opts.mesg.email_address
100
ttl : 60*60
101
cb : (err, _id) ->
102
id = _id; cb(err)
103
(cb) ->
104
# send an email to opts.mesg.email_address that has a password reset link
105
{DOMAIN_NAME, HELP_EMAIL, SITE_NAME} = require('smc-util/theme')
106
body = """
107
<div>Hello,</div>
108
<div>&nbsp;</div>
109
<div>
110
Somebody just requested to change the password of your #{SITE_NAME} account.
111
If you requested this password change, please click this link:</div>
112
<div>&nbsp;</div>
113
<div style="text-align: center;">
114
<span style="font-size:12px;"><b>
115
<a href="#{DOMAIN_NAME}/app#forgot-#{id}">#{DOMAIN_NAME}/app#forgot-#{id}</a>
116
</b></span>
117
</div>
118
<div>&nbsp;</div>
119
<div>If you don't want to change your password, ignore this message.</div>
120
<div>&nbsp;</div>
121
<div>In case of problems, email
122
<a href="mailto:#{HELP_EMAIL}">#{HELP_EMAIL}</a> immediately
123
(or just reply to this email).
124
<div>&nbsp;</div>
125
"""
126
127
email.send_email
128
subject : "#{SITE_NAME} Password Reset"
129
body : body
130
from : "CoCalc Help <#{HELP_EMAIL}>"
131
to : opts.mesg.email_address
132
category: "password_reset"
133
cb : cb
134
], opts.cb)
135
136
exports.reset_forgot_password = (opts) ->
137
opts = defaults opts,
138
mesg : required
139
database : required
140
cb : required
141
if opts.mesg.event != 'reset_forgot_password'
142
opts.cb("incorrect message event type: #{opts.mesg.event}")
143
return
144
145
email_address = account_id = db = null
146
147
async.series([
148
(cb) ->
149
# Verify password is valid and compute its hash.
150
[valid, reason] = is_valid_password(opts.mesg.new_password)
151
if not valid
152
cb(reason); return
153
# Check that request is still valid
154
opts.database.get_password_reset
155
id : opts.mesg.reset_code
156
cb : (err, x) ->
157
if err
158
cb(err)
159
else if not x
160
cb("Password reset request is no longer valid.")
161
else
162
email_address = x
163
cb()
164
(cb) ->
165
# Get the account_id.
166
opts.database.get_account
167
email_address : email_address
168
columns : ['account_id']
169
cb : (err, account) ->
170
account_id = account?.account_id; cb(err)
171
(cb) ->
172
# Make the change
173
opts.database.change_password
174
account_id : account_id
175
password_hash : auth.password_hash(opts.mesg.new_password)
176
cb : (err, account) ->
177
if err
178
cb(err)
179
else
180
# only allow successful use of this reset token once
181
opts.database.delete_password_reset
182
id : opts.mesg.reset_code
183
cb : cb
184
], opts.cb)
185
186
exports.change_password = (opts) ->
187
opts = defaults opts,
188
mesg : required
189
account_id : required
190
database : required
191
ip_address : required
192
cb : required
193
account = null
194
opts.mesg.email_address = misc.lower_email_address(opts.mesg.email_address)
195
async.series([
196
(cb) ->
197
if not opts.mesg.email_address?
198
# There are no guarantees about incoming messages
199
cb("email_address must be specified")
200
return
201
# get account and validate the password
202
opts.database.get_account
203
email_address : opts.mesg.email_address
204
columns : ['password_hash', 'account_id']
205
cb : (error, result) ->
206
if error
207
cb({other:error})
208
return
209
if result.account_id != opts.account_id
210
cb({other:'invalid account_id'})
211
return
212
account = result
213
auth.is_password_correct
214
database : opts.database
215
account_id : result.account_id
216
password : opts.mesg.old_password
217
password_hash : account.password_hash
218
allow_empty_password : true
219
cb : (err, is_correct) ->
220
if err
221
cb(err)
222
else
223
if not is_correct
224
err = "invalid old password"
225
opts.database.log
226
event : 'change_password'
227
value : {email_address:opts.mesg.email_address, client_ip_address:opts.ip_address, message:err}
228
cb(err)
229
else
230
cb()
231
(cb) ->
232
# check that new password is valid
233
[valid, reason] = is_valid_password(opts.mesg.new_password)
234
if not valid
235
cb({new_password:reason})
236
else
237
cb()
238
239
(cb) ->
240
# record current password hash (just in case?) and that we
241
# are changing password and set new password
242
opts.database.log
243
event : "change_password"
244
value :
245
account_id : account.account_id
246
client_ip_address : opts.ip_address
247
previous_password_hash : account.password_hash
248
249
opts.database.change_password
250
account_id : account.account_id
251
password_hash : auth.password_hash(opts.mesg.new_password),
252
cb : cb
253
], opts.cb)
254
255
exports.change_email_address = (opts) ->
256
opts = defaults opts,
257
mesg : required
258
database : required
259
account_id : required
260
ip_address : required
261
logger : undefined
262
cb : required
263
264
if opts.logger?
265
dbg = (m...) -> opts.logger?.debug("change_email_address(#{opts.mesg.account_id}): ", m...)
266
dbg()
267
else
268
dbg = ->
269
270
opts.mesg.new_email_address = misc.lower_email_address(opts.mesg.new_email_address)
271
272
if not misc.is_valid_email_address(opts.mesg.new_email_address)
273
dbg("invalid email address")
274
opts.cb('email_invalid')
275
return
276
277
if opts.mesg.account_id != opts.account_id
278
opts.cb("account_id in mesg is not what user is signed in as")
279
return
280
281
async.series([
282
(cb) ->
283
auth.is_password_correct
284
database : opts.database
285
account_id : opts.mesg.account_id
286
password : opts.mesg.password
287
allow_empty_password : true # in case account created using a linked passport only
288
cb : (err, is_correct) ->
289
if err
290
cb("Error checking password -- please try again in a minute -- #{err}.")
291
else if not is_correct
292
cb("invalid_password")
293
else
294
cb()
295
296
(cb) ->
297
# Record current email address (just in case?) and that we are
298
# changing email address to the new one. This will make it
299
# easy to implement a "change your email address back" feature
300
# if I need to at some point.
301
dbg("log change to db")
302
opts.database.log
303
event : 'change_email_address'
304
value :
305
client_ip_address : opts.ip_address
306
new_email_address : opts.mesg.new_email_address
307
308
dbg("actually make change in db")
309
opts.database.change_email_address
310
account_id : opts.mesg.account_id
311
email_address : opts.mesg.new_email_address
312
cb : cb
313
(cb) ->
314
# If they just changed email to an address that has some actions, carry those out...
315
# TODO: move to hook this only after validation of the email address?
316
opts.database.do_account_creation_actions
317
email_address : opts.mesg.new_email_address
318
account_id : opts.mesg.account_id
319
cb : cb
320
], opts.cb)
321
322