Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39538
1
##############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
###
23
Passport Authentication (oauth, etc.)
24
###
25
26
async = require('async')
27
uuid = require('node-uuid')
28
winston = require('winston')
29
passport= require('passport')
30
31
misc = require('smc-util/misc')
32
message = require('smc-util/message') # message protocol between front-end and back-end
33
34
Cookies = require('cookies')
35
36
express_session = require('express-session')
37
38
{defaults, required} = misc
39
40
########################################
41
# Password hashing
42
########################################
43
44
password_hash_library = require('password-hash')
45
crypto = require('crypto')
46
47
# You can change the parameters at any time and no existing passwords
48
# or cookies should break. This will only impact newly created
49
# passwords and cookies. Old ones can be read just fine (with the old
50
# parameters).
51
HASH_ALGORITHM = 'sha512'
52
HASH_ITERATIONS = 1000
53
HASH_SALT_LENGTH = 32
54
55
# This function is private and burried inside the password-hash
56
# library. To avoid having to fork/modify that library, we've just
57
# copied it here. We need it for remember_me cookies.
58
exports.generate_hash = generate_hash = (algorithm, salt, iterations, password) ->
59
iterations = iterations || 1
60
hash = password
61
for i in [1..iterations]
62
hash = crypto.createHmac(algorithm, salt).update(hash).digest('hex')
63
return algorithm + '$' + salt + '$' + iterations + '$' + hash
64
65
exports.password_hash = password_hash = (password) ->
66
return password_hash_library.generate(password,
67
algorithm : HASH_ALGORITHM
68
saltLength : HASH_SALT_LENGTH
69
iterations : HASH_ITERATIONS # This blocks the server for about 5-9ms.
70
)
71
72
passport_login = (opts) ->
73
opts = defaults opts,
74
database : required
75
strategy : required # name of the auth strategy, e.g., 'google', 'facebook', etc.
76
profile : required # will just get saved in database
77
id : required # unique id given by oauth provider
78
first_name : undefined
79
last_name : undefined
80
full_name : undefined
81
emails : undefined # if user not logged in (via remember_me) already, and existing account with same email, and passport not created, then get an error instead of login or account creation.
82
req : required # request object
83
res : required # response object
84
base_url : ''
85
host : required
86
cb : undefined
87
88
dbg = (m) -> winston.debug("passport_login: #{m}")
89
BASE_URL = opts.base_url
90
91
dbg(misc.to_json(opts.req.user))
92
93
if opts.full_name? and not opts.first_name? and not opts.last_name?
94
name = opts.full_name
95
i = name.lastIndexOf(' ')
96
if i == -1
97
opts.first_name = name
98
opts.last_name = name
99
else
100
opts.first_name = name.slice(0,i).trim()
101
opts.last_name = name.slice(i).trim()
102
if not opts.first_name?
103
opts.first_name = "Anonymous"
104
if not opts.last_name?
105
opts.last_name = "User"
106
107
if opts.emails?
108
opts.emails = (x.toLowerCase() for x in opts.emails when (x? and x.toLowerCase? and misc.is_valid_email_address(x)))
109
110
opts.id = "#{opts.id}" # convert to string (id is often a number)
111
112
has_valid_remember_me = false
113
account_id = undefined
114
email_address = undefined
115
async.series([
116
(cb) ->
117
dbg("check if user has a valid remember_me token, in which case we can trust who they are already")
118
cookies = new Cookies(opts.req)
119
value = cookies.get(BASE_URL + 'remember_me')
120
if not value?
121
cb()
122
return
123
x = value.split('$')
124
if x.length != 4
125
cb()
126
return
127
hash = generate_hash(x[0], x[1], x[2], x[3])
128
opts.database.get_remember_me
129
hash : hash
130
cb : (err, signed_in_mesg) ->
131
if err
132
cb(err)
133
else if signed_in_mesg?
134
account_id = signed_in_mesg.account_id
135
has_valid_remember_me = true
136
cb()
137
else
138
cb()
139
(cb) ->
140
dbg("check to see if the passport already exists indexed by the given id -- in that case we will log user in")
141
opts.database.passport_exists
142
strategy : opts.strategy
143
id : opts.id
144
cb : (err, _account_id) ->
145
if err
146
cb(err)
147
else
148
if not _account_id and has_valid_remember_me
149
dbg("passport doesn't exist, but user is authenticated (via remember_me), so we add this passport for them.")
150
opts.database.create_passport
151
account_id : account_id
152
strategy : opts.strategy
153
id : opts.id
154
profile : opts.profile
155
cb : cb
156
else
157
if has_valid_remember_me and account_id != _account_id
158
dbg("passport exists but is associated with another account already")
159
cb("Your #{opts.strategy} account is already attached to another CoCalc account. First sign into that account and unlink #{opts.strategy} in account settings if you want to instead associate it with this account.")
160
else
161
if has_valid_remember_me
162
dbg("passport already exists and is associated to the currently logged into account")
163
else
164
dbg("passport exists and is already associated to a valid account, which we'll log user into")
165
account_id = _account_id
166
cb()
167
(cb) ->
168
if account_id or not opts.emails?
169
cb(); return
170
dbg("passport doesn't exist and emails available, so check for existing account with a matching email -- if we find one it's an error")
171
f = (email, cb) ->
172
if account_id
173
dbg("already found a match with account_id=#{account_id} -- done")
174
cb()
175
else
176
dbg("checking for account with email #{email}...")
177
opts.database.account_exists
178
email_address : email.toLowerCase()
179
cb : (err, _account_id) ->
180
if account_id # already done, so ignore
181
dbg("already found a match with account_id=#{account_id} -- done")
182
cb()
183
else if err or not _account_id
184
cb(err)
185
else
186
account_id = _account_id
187
email_address = email.toLowerCase()
188
dbg("found matching account #{account_id} for email #{email_address}")
189
cb("There is already an account with email address #{email_address}; please sign in using that email account, then link #{opts.strategy} to it in account settings.")
190
async.map(opts.emails, f, cb)
191
(cb) ->
192
if account_id
193
cb(); return
194
dbg("no existing account to link, so create new account that can be accessed using this passport")
195
if opts.emails?
196
email_address = opts.emails[0]
197
async.series([
198
(cb) ->
199
opts.database.create_account
200
first_name : opts.first_name
201
last_name : opts.last_name
202
email_address : email_address
203
passport_strategy : opts.strategy
204
passport_id : opts.id
205
passport_profile : opts.profile
206
cb : (err, _account_id) ->
207
account_id = _account_id
208
cb(err)
209
(cb) ->
210
if not email_address?
211
cb()
212
else
213
opts.database.do_account_creation_actions
214
email_address : email_address
215
account_id : account_id
216
cb : cb
217
], cb)
218
219
(cb) ->
220
target = BASE_URL + "/app#login"
221
222
if has_valid_remember_me
223
opts.res.redirect(target)
224
cb()
225
return
226
dbg("passport created: set remember_me cookie, so user gets logged in")
227
# create and set remember_me cookie, then redirect.
228
# See the remember_me method of client for the algorithm we use.
229
signed_in_mesg = message.signed_in
230
remember_me : true
231
hub : opts.host
232
account_id : account_id
233
first_name : opts.first_name
234
last_name : opts.last_name
235
236
dbg("create remember_me cookie")
237
session_id = uuid.v4()
238
hash_session_id = password_hash(session_id)
239
ttl = 24*3600*30 # 30 days
240
x = hash_session_id.split('$')
241
remember_me_value = [x[0], x[1], x[2], session_id].join('$')
242
dbg("set remember_me cookies in client")
243
expires = new Date(new Date().getTime() + ttl*1000)
244
cookies = new Cookies(opts.req, opts.res)
245
cookies.set(BASE_URL + 'remember_me', remember_me_value, {expires:expires})
246
dbg("set remember_me cookie in database")
247
opts.database.save_remember_me
248
account_id : account_id
249
hash : hash_session_id
250
value : signed_in_mesg
251
ttl : ttl
252
cb : (err) ->
253
if err
254
cb(err)
255
else
256
dbg("finally redirect the client to #{target}, who should auto login")
257
opts.res.redirect(target)
258
cb()
259
], (err) ->
260
if err
261
opts.res.send("Error trying to login using #{opts.strategy} -- #{err}")
262
opts.cb?(err)
263
)
264
265
exports.init_passport = (opts) ->
266
opts = defaults opts,
267
router : required
268
database : required
269
base_url : required
270
host : required
271
cb : required
272
273
{router, database, base_url, host, cb} = opts
274
# Initialize authentication plugins using Passport
275
dbg = (m) -> winston.debug("init_passport: #{m}")
276
dbg()
277
278
# initialize use of middleware
279
router.use(express_session({secret:misc.uuid()})) # secret is totally random and per-hub session
280
router.use(passport.initialize())
281
router.use(passport.session())
282
283
# Define user serialization
284
passport.serializeUser (user, done) ->
285
done(null, user)
286
passport.deserializeUser (user, done) ->
287
done(null, user)
288
289
strategies = [] # configured strategies listed here.
290
get_conf = (strategy, cb) ->
291
database.get_passport_settings
292
strategy : strategy
293
cb : (err, settings) ->
294
if err
295
dbg("error getting passport settings for #{strategy} -- #{err}")
296
cb(err)
297
else
298
if settings?
299
if strategy != 'site_conf'
300
strategies.push(strategy)
301
cb(undefined, settings)
302
else
303
dbg("WARNING: passport strategy #{strategy} not configured")
304
cb(undefined, undefined)
305
306
# Return the configured and supported authentication strategies.
307
router.get '/auth/strategies', (req, res) ->
308
res.json(strategies)
309
310
# Set the site conf like this:
311
#
312
# require 'c'; db()
313
# db.set_passport_settings(strategy:'site_conf', conf:{auth:'https://cocalc.com/auth'}, cb:done())
314
#
315
# or when doing development in a project # TODO: far too brittle, especially the port/base_url stuff!
316
#
317
# db.set_passport_settings(strategy:'site_conf', conf:{auth:'https://cocalc.com/project_uuid.../port/YYYYY/auth'}, cb:done())
318
319
320
auth_url = undefined # gets set below
321
322
init_local = (cb) ->
323
dbg("init_local")
324
# Strategy: local email address / password login
325
PassportStrategy = require('passport-local').Strategy
326
327
verify = (username, password, done) ->
328
if username == 'a'
329
return done(null, false, { message: 'Incorrect password.' })
330
console.log("local strategy validating user #{username}")
331
done(null, {username:username})
332
333
passport.use(new PassportStrategy(verify))
334
335
router.get '/auth/local', (req, res) ->
336
res.send("""<form action="/auth/local" method="post">
337
<label>Email</label>
338
<input type="text" name="username">
339
<label>Password</label>
340
<input type="password" name="password">
341
<button type="submit" value="Log In"/>Login</button>
342
</form>""")
343
344
router.post '/auth/local', passport.authenticate('local'), (req, res) ->
345
console.log("authenticated... ")
346
res.json(req.user)
347
348
cb()
349
350
init_google = (cb) ->
351
dbg("init_google")
352
# Strategy: Google OAuth 2 -- https://github.com/jaredhanson/passport-google-oauth
353
#
354
# NOTE: The passport-recommend library passport-google uses openid2, which
355
# is deprecated in a few days! So instead, I have to use oauth2, which
356
# is in https://github.com/jaredhanson/passport-google-oauth, which I found by luck!?!
357
#
358
PassportStrategy = require('passport-google-oauth').OAuth2Strategy
359
strategy = 'google'
360
get_conf strategy, (err, conf) ->
361
if err or not conf?
362
cb(err)
363
return
364
# docs for getting these for your app
365
# https://developers.google.com/accounts/docs/OpenIDConnect#appsetup
366
#
367
# You must then put them in the database, via
368
#
369
# require 'c'; db()
370
# db.set_passport_settings(strategy:'google', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
371
opts =
372
clientID : conf.clientID
373
clientSecret : conf.clientSecret
374
callbackURL : "#{auth_url}/#{strategy}/return"
375
376
verify = (accessToken, refreshToken, profile, done) ->
377
done(undefined, {profile:profile})
378
passport.use(new PassportStrategy(opts, verify))
379
380
winston.debug("opts=#{misc.to_json(opts)}")
381
382
# Enabling "profile" below I think required that I explicitly go to Google Developer Console for the project,
383
# then select API&Auth, then API's, then Google+, then explicitly enable it. Otherwise, stuff just mysteriously
384
# didn't work. To figure out that this was the problem, I had to grep the source code of the passport-google-oauth
385
# library and put in print statements to see what the *REAL* errors were, since that
386
# library hid the errors (**WHY**!!?).
387
router.get "/auth/#{strategy}", passport.authenticate(strategy, {'scope': 'openid email profile'})
388
389
router.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
390
profile = req.user.profile
391
passport_login
392
database : database
393
strategy : strategy
394
profile : profile # will just get saved in database
395
id : profile.id
396
first_name : profile.name.givenName
397
last_name : profile.name.familyName
398
emails : (x.value for x in profile.emails)
399
req : req
400
res : res
401
base_url : base_url
402
host : host
403
404
cb()
405
406
init_github = (cb) ->
407
dbg("init_github")
408
# Strategy: Github OAuth2 -- https://github.com/jaredhanson/passport-github
409
PassportStrategy = require('passport-github').Strategy
410
strategy = 'github'
411
get_conf strategy, (err, conf) ->
412
if err or not conf?
413
cb(err)
414
return
415
# Get these here:
416
# https://github.com/settings/applications/new
417
# You must then put them in the database, via
418
# db.set_passport_settings(strategy:'github', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
419
420
opts =
421
clientID : conf.clientID
422
clientSecret : conf.clientSecret
423
callbackURL : "#{auth_url}/#{strategy}/return"
424
425
verify = (accessToken, refreshToken, profile, done) ->
426
done(undefined, {profile:profile})
427
passport.use(new PassportStrategy(opts, verify))
428
429
router.get "/auth/#{strategy}", passport.authenticate(strategy)
430
431
router.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
432
profile = req.user.profile
433
passport_login
434
database : database
435
strategy : strategy
436
profile : profile # will just get saved in database
437
id : profile.id
438
full_name : profile.name or profile.displayName or profile.username
439
emails : (x.value for x in (profile.emails ? []))
440
req : req
441
res : res
442
base_url : base_url
443
host : host
444
cb()
445
446
init_facebook = (cb) ->
447
dbg("init_facebook")
448
# Strategy: Facebook OAuth2 --
449
PassportStrategy = require('passport-facebook').Strategy
450
strategy = 'facebook'
451
get_conf strategy, (err, conf) ->
452
if err or not conf?
453
cb(err)
454
return
455
# Get these by going to https://developers.facebook.com/ and creating a new application.
456
# For that application, set the url to the site CoCalc will be served from.
457
# The Facebook "App ID" and is clientID and the Facebook "App Secret" is the clientSecret
458
# for oauth2, as I discovered by a lucky guess... (sigh).
459
#
460
# You must then put them in the database, via
461
# db.set_passport_settings(strategy:'facebook', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
462
463
opts =
464
clientID : conf.clientID
465
clientSecret : conf.clientSecret
466
callbackURL : "#{auth_url}/#{strategy}/return"
467
enableProof : false
468
469
verify = (accessToken, refreshToken, profile, done) ->
470
done(undefined, {profile:profile})
471
passport.use(new PassportStrategy(opts, verify))
472
473
router.get "/auth/#{strategy}", passport.authenticate(strategy)
474
475
router.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
476
profile = req.user.profile
477
passport_login
478
database : database
479
strategy : strategy
480
profile : profile # will just get saved in database
481
id : profile.id
482
full_name : profile.displayName
483
req : req
484
res : res
485
base_url : base_url
486
host : host
487
488
cb()
489
490
init_dropbox = (cb) ->
491
dbg("init_dropbox")
492
PassportStrategy = require('passport-dropbox-oauth2').Strategy
493
strategy = 'dropbox'
494
get_conf strategy, (err, conf) ->
495
if err or not conf?
496
cb(err)
497
return
498
# Get these by:
499
# (1) creating a dropbox account, then going to this url: https://www.dropbox.com/developers/apps
500
# (2) make a dropbox api app that only access the datastore (not user files -- for now, since we're just doing auth!).
501
# (3) You'll see an "App key" and an "App secret".
502
# (4) Add the redirect URL on the dropbox page as well, which will be like https://cloud.sagemath.com/auth/dropbox/return
503
# This might (or might not) be relevant when we support dropbox sync: https://github.com/dropbox/dropbox-js
504
#
505
# You must then put them in the database, via
506
# db.set_passport_settings(strategy:'dropbox', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
507
508
opts =
509
clientID : conf.clientID
510
clientSecret : conf.clientSecret
511
callbackURL : "#{auth_url}/#{strategy}/return"
512
513
verify = (accessToken, refreshToken, profile, done) ->
514
done(undefined, {profile:profile})
515
passport.use(new PassportStrategy(opts, verify))
516
517
router.get "/auth/#{strategy}", passport.authenticate("dropbox-oauth2")
518
519
router.get "/auth/#{strategy}/return", passport.authenticate("dropbox-oauth2", {failureRedirect: '/auth/local'}), (req, res) ->
520
profile = req.user.profile
521
passport_login
522
database : database
523
strategy : strategy
524
profile : profile # will just get saved in database
525
id : profile.id
526
first_name : profile._json.name_details.familiar_name
527
last_name : profile._json.name_details.surname
528
full_name : profile.displayName
529
req : req
530
res : res
531
base_url : base_url
532
host : host
533
534
cb()
535
536
init_bitbucket = (cb) ->
537
dbg("init_bitbucket")
538
PassportStrategy = require('passport-bitbucket').Strategy
539
strategy = 'bitbucket'
540
get_conf strategy, (err, conf) ->
541
if err or not conf?
542
cb(err)
543
return
544
# Get these by:
545
# (1) make a bitbucket account
546
# (2) Go to https://bitbucket.org/account/user/[your username]/api
547
# (3) Click add consumer and enter the URL of your CoCalc instance.
548
#
549
# You must then put them in the database, via
550
# db.set_passport_settings(strategy:'bitbucket', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
551
552
opts =
553
consumerKey : conf.clientID
554
consumerSecret : conf.clientSecret
555
callbackURL : "#{auth_url}/#{strategy}/return"
556
557
verify = (accessToken, refreshToken, profile, done) ->
558
done(undefined, {profile:profile})
559
passport.use(new PassportStrategy(opts, verify))
560
561
router.get "/auth/#{strategy}", passport.authenticate(strategy)
562
563
router.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
564
profile = req.user.profile
565
#winston.debug("profile=#{misc.to_json(profile)}")
566
passport_login
567
database : database
568
strategy : strategy
569
profile : profile # will just get saved in database
570
id : profile.username
571
first_name : profile.name.givenName
572
last_name : profile.name.familyName
573
req : req
574
res : res
575
base_url : base_url
576
host : host
577
578
cb()
579
580
###
581
init_wordpress = (cb) ->
582
dbg("init_wordpress")
583
PassportStrategy = require('passport-wordpress').Strategy
584
strategy = 'wordpress'
585
get_conf strategy, (err, conf) ->
586
if err or not conf?
587
cb(err)
588
return
589
# Get these by:
590
# (1) Make a wordpress account
591
# (2) Go to https://developer.wordpress.com/apps/
592
# (3) Click "Create a New Application"
593
# (4) Fill the form as usual and eventual get the id and secret.
594
#
595
# You must then put them in the database, via
596
# db.set_passport_settings(strategy:'wordpress', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
597
opts =
598
clientID : conf.clientID
599
clientSecret : conf.clientSecret
600
callbackURL : "#{auth_url}/#{strategy}/return"
601
verify = (accessToken, refreshToken, profile, done) ->
602
done(undefined, {profile:profile})
603
passport.use(new PassportStrategy(opts, verify))
604
router.get "/auth/#{strategy}", passport.authenticate(strategy)
605
router.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
606
profile = req.user.profile
607
passport_login
608
database : database
609
strategy : strategy
610
profile : profile # will just get saved in database
611
id : profile._json.ID
612
emails : [profile._json.email]
613
full_name : profile.displayName
614
req : req
615
res : res
616
base_url : base_url
617
host : host
618
cb()
619
###
620
621
init_twitter = (cb) ->
622
dbg("init_twitter")
623
PassportStrategy = require('passport-twitter').Strategy
624
strategy = 'twitter'
625
get_conf strategy, (err, conf) ->
626
if err or not conf?
627
cb(err)
628
return
629
# Get these by:
630
# (1) Go to https://apps.twitter.com/ and create a new application.
631
# (2) Click on Keys and Access Tokens
632
#
633
# You must then put them in the database, via
634
# db.set_passport_settings(strategy:'twitter', conf:{clientID:'...',clientSecret:'...'}, cb:console.log)
635
636
opts =
637
consumerKey : conf.clientID
638
consumerSecret : conf.clientSecret
639
callbackURL : "#{auth_url}/#{strategy}/return"
640
641
verify = (accessToken, refreshToken, profile, done) ->
642
done(undefined, {profile:profile})
643
passport.use(new PassportStrategy(opts, verify))
644
645
router.get "/auth/#{strategy}", passport.authenticate(strategy)
646
647
router.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
648
profile = req.user.profile
649
passport_login
650
database : database
651
strategy : strategy
652
profile : profile # will just get saved in database
653
id : profile.id
654
full_name : profile.displayName
655
req : req
656
res : res
657
base_url : base_url
658
host : host
659
660
cb()
661
662
async.series([
663
(cb) ->
664
get_conf 'site_conf', (err, site_conf) ->
665
if err
666
cb(err)
667
else
668
if site_conf?
669
auth_url = site_conf.auth
670
dbg("auth_url='#{auth_url}'")
671
cb()
672
(cb) ->
673
if not auth_url?
674
cb()
675
else
676
async.parallel([init_local, init_google, init_github, init_facebook,
677
init_dropbox, init_bitbucket, init_twitter], cb)
678
], (err) =>
679
strategies.sort()
680
strategies.unshift('email')
681
cb(err)
682
)
683
684
685
686
687
688
# Password checking. opts.cb(undefined, true) if the
689
# password is correct, opts.cb(error) on error (e.g., loading from
690
# database), and opts.cb(undefined, false) if password is wrong. You must
691
# specify exactly one of password_hash, account_id, or email_address.
692
# In case you specify password_hash, in addition to calling the
693
# callback (if specified), this function also returns true if the
694
# password is correct, and false otherwise; it can do this because
695
# there is no async IO when the password_hash is specified.
696
exports.is_password_correct = (opts) ->
697
opts = defaults opts,
698
database : required
699
password : required
700
password_hash : undefined
701
account_id : undefined
702
email_address : undefined
703
allow_empty_password : false # If true and no password set in account, it matches anything.
704
# this is only used when first changing the email address or password
705
# in passport-only accounts.
706
cb : required # cb(err, true or false)
707
708
if opts.password_hash?
709
r = password_hash_library.verify(opts.password, opts.password_hash)
710
opts.cb(undefined, r)
711
else if opts.account_id? or opts.email_address?
712
opts.database.get_account
713
account_id : opts.account_id
714
email_address : opts.email_address
715
columns : ['password_hash']
716
cb : (error, account) ->
717
if error
718
opts.cb(error)
719
else
720
if opts.allow_empty_password and not account.password_hash
721
opts.cb(undefined, true)
722
else
723
opts.cb(undefined, password_hash_library.verify(opts.password, account.password_hash))
724
else
725
opts.cb("One of password_hash, account_id, or email_address must be specified.")
726
727
728
729