/
Handlers.hs
542 lines (452 loc) · 19.6 KB
/
Handlers.hs
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
------------------------------------------------------------------------------
-- | Pre-packaged Handlers that deal with form submissions and standard
-- use-cases involving authentication.
module Snap.Snaplet.Auth.Handlers where
------------------------------------------------------------------------------
import Control.Applicative
import Control.Error
import Control.Monad.State
import Data.ByteString (ByteString)
import Data.Serialize hiding (get)
import Data.Time
import Data.Text.Encoding (decodeUtf8)
import Data.Text (Text, null, strip)
import Prelude hiding (null)
import Web.ClientSession
------------------------------------------------------------------------------
import Snap.Core
import Snap.Snaplet
import Snap.Snaplet.Auth.AuthManager
import Snap.Snaplet.Auth.Types
import Snap.Snaplet.Session
------------------------------------------------------------------------------
----------------------------
-- Higher level functions --
----------------------------
------------------------------------------------------------------------------
-- | Create a new user from just a username and password
--
createUser :: Text -- ^ Username
-> ByteString -- ^ Password
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
createUser unm pwd
| null $ strip unm = return $ Left UsernameMissing
| otherwise = do
uExists <- usernameExists unm
if uExists then return $ Left DuplicateLogin
else withBackend $ \r -> liftIO $ buildAuthUser r unm pwd
------------------------------------------------------------------------------
-- | Check whether a user with the given username exists.
--
usernameExists :: Text -- ^ The username to be checked
-> Handler b (AuthManager b) Bool
usernameExists username =
withBackend $ \r -> liftIO $ isJust <$> lookupByLogin r username
------------------------------------------------------------------------------
-- | Lookup a user by her username, check given password and perform login
--
loginByUsername :: ByteString -- ^ Username/login for user
-> Password -- ^ Should be ClearText
-> Bool -- ^ Set remember token?
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
loginByUsername _ (Encrypted _) _ = return $ Left EncryptedPassword
loginByUsername unm pwd shouldRemember = do
sk <- gets siteKey
cn <- gets rememberCookieName
rp <- gets rememberPeriod
withBackend $ loginByUsername' sk cn rp
where
--------------------------------------------------------------------------
loginByUsername' :: (IAuthBackend t) =>
Key
-> ByteString
-> Maybe Int
-> t
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
loginByUsername' sk cn rp r =
liftIO (lookupByLogin r $ decodeUtf8 unm) >>=
maybe (return $! Left UserNotFound) found
where
----------------------------------------------------------------------
found user = checkPasswordAndLogin user pwd >>=
either (return . Left) matched
----------------------------------------------------------------------
matched user
| shouldRemember = do
token <- gets randomNumberGenerator >>=
liftIO . randomToken 64
setRememberToken sk cn rp token
let user' = user {
userRememberToken = Just (decodeUtf8 token)
}
saveUser user'
return $! Right user'
| otherwise = return $ Right user
------------------------------------------------------------------------------
-- | Remember user from the remember token if possible and perform login
--
loginByRememberToken :: Handler b (AuthManager b) (Maybe AuthUser)
loginByRememberToken = withBackend $ \impl -> do
key <- gets siteKey
cookieName_ <- gets rememberCookieName
period <- gets rememberPeriod
runMaybeT $ do
token <- MaybeT $ getRememberToken key cookieName_ period
user <- MaybeT $ liftIO $ lookupByRememberToken impl
$ decodeUtf8 token
lift $ forceLogin user
return user
------------------------------------------------------------------------------
-- | Logout the active user
--
logout :: Handler b (AuthManager b) ()
logout = do
s <- gets session
withTop s $ withSession s removeSessionUserId
rc <- gets rememberCookieName
forgetRememberToken rc
modify $ \mgr -> mgr { activeUser = Nothing }
------------------------------------------------------------------------------
-- | Return the current user; trying to remember from cookie if possible.
--
currentUser :: Handler b (AuthManager b) (Maybe AuthUser)
currentUser = cacheOrLookup $ withBackend $ \r -> do
s <- gets session
uid <- withTop s getSessionUserId
case uid of
Nothing -> loginByRememberToken
Just uid' -> liftIO $ lookupByUserId r uid'
------------------------------------------------------------------------------
-- | Convenience wrapper around 'rememberUser' that returns a bool result
--
isLoggedIn :: Handler b (AuthManager b) Bool
isLoggedIn = isJust <$> currentUser
------------------------------------------------------------------------------
-- | Create or update a given user
--
saveUser :: AuthUser -> Handler b (AuthManager b) (Either AuthFailure AuthUser)
saveUser u
| null $ userLogin u = return $ Left UsernameMissing
| otherwise = withBackend $ \r -> liftIO $ save r u
------------------------------------------------------------------------------
-- | Destroy the given user
--
destroyUser :: AuthUser -> Handler b (AuthManager b) ()
destroyUser u = withBackend $ liftIO . flip destroy u
-----------------------------------
-- Lower level helper functions --
-----------------------------------
------------------------------------------------------------------------------
-- | Mutate an 'AuthUser', marking failed authentication
--
-- This will save the user to the backend.
--
markAuthFail :: AuthUser
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
markAuthFail u = withBackend $ \r -> do
lo <- gets lockout
incFailCtr u >>= checkLockout lo >>= liftIO . save r
where
--------------------------------------------------------------------------
incFailCtr u' = return $ u' {
userFailedLoginCount = userFailedLoginCount u' + 1
}
--------------------------------------------------------------------------
checkLockout lo u' =
case lo of
Nothing -> return u'
Just (mx, wait) ->
if userFailedLoginCount u' >= mx
then do
now <- liftIO getCurrentTime
let reopen = addUTCTime wait now
return $! u' { userLockedOutUntil = Just reopen }
else return u'
------------------------------------------------------------------------------
-- | Mutate an 'AuthUser', marking successful authentication
--
-- This will save the user to the backend.
--
markAuthSuccess :: AuthUser
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
markAuthSuccess u = withBackend $ \r ->
incLoginCtr u >>=
updateIp >>=
updateLoginTS >>=
resetFailCtr >>=
liftIO . save r
where
--------------------------------------------------------------------------
incLoginCtr u' = return $ u' { userLoginCount = userLoginCount u' + 1 }
--------------------------------------------------------------------------
updateIp u' = do
ip <- rqRemoteAddr <$> getRequest
return $ u' { userLastLoginIp = userCurrentLoginIp u'
, userCurrentLoginIp = Just ip }
--------------------------------------------------------------------------
updateLoginTS u' = do
now <- liftIO getCurrentTime
return $
u' { userCurrentLoginAt = Just now
, userLastLoginAt = userCurrentLoginAt u' }
--------------------------------------------------------------------------
resetFailCtr u' = return $ u' { userFailedLoginCount = 0
, userLockedOutUntil = Nothing }
------------------------------------------------------------------------------
-- | Authenticate and log the user into the current session if successful.
--
-- This is a mid-level function exposed to allow roll-your-own ways of looking
-- up a user from the database.
--
-- This function will:
--
-- 1. Check the password
--
-- 2. Login the user into the current session
--
-- 3. Mark success/failure of the authentication trial on the user record
--
checkPasswordAndLogin
:: AuthUser -- ^ An existing user, somehow looked up from db
-> Password -- ^ A ClearText password
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
checkPasswordAndLogin u pw =
case userLockedOutUntil u of
Just x -> do
now <- liftIO getCurrentTime
if now > x
then auth u
else return . Left $ LockedOut x
Nothing -> auth u
where
auth :: AuthUser -> Handler b (AuthManager b) (Either AuthFailure AuthUser)
auth user =
case authenticatePassword user pw of
Just e -> do
markAuthFail user
return $ Left e
Nothing -> do
forceLogin user
modify (\mgr -> mgr { activeUser = Just user })
markAuthSuccess user
------------------------------------------------------------------------------
-- | Login and persist the given 'AuthUser' in the active session
--
-- Meant to be used if you have other means of being sure that the person is
-- who she says she is.
--
forceLogin :: AuthUser -- ^ An existing user, somehow looked up from db
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
forceLogin u = do
s <- gets session
withSession s $
case userId u of
Just x -> do
withTop s (setSessionUserId x)
return $ Right u
Nothing -> return . Left $
AuthError $ "forceLogin: Can't force the login of a user "
++ "without userId"
------------------------------------
-- Internal, non-exported helpers --
------------------------------------
------------------------------------------------------------------------------
getRememberToken :: (Serialize t, MonadSnap m)
=> Key
-> ByteString
-> Maybe Int
-> m (Maybe t)
getRememberToken sk rc rp = getSecureCookie rc sk rp
------------------------------------------------------------------------------
setRememberToken :: (Serialize t, MonadSnap m)
=> Key
-> ByteString
-> Maybe Int
-> t
-> m ()
setRememberToken sk rc rp token = setSecureCookie rc sk rp token
------------------------------------------------------------------------------
forgetRememberToken :: MonadSnap m => ByteString -> m ()
forgetRememberToken rc = expireCookie rc (Just "/")
------------------------------------------------------------------------------
-- | Set the current user's 'UserId' in the active session
--
setSessionUserId :: UserId -> Handler b SessionManager ()
setSessionUserId (UserId t) = setInSession "__user_id" t
------------------------------------------------------------------------------
-- | Remove 'UserId' from active session, effectively logging the user out.
removeSessionUserId :: Handler b SessionManager ()
removeSessionUserId = deleteFromSession "__user_id"
------------------------------------------------------------------------------
-- | Get the current user's 'UserId' from the active session
--
getSessionUserId :: Handler b SessionManager (Maybe UserId)
getSessionUserId = do
uid <- getFromSession "__user_id"
return $ liftM UserId uid
------------------------------------------------------------------------------
-- | Check password for a given user.
--
-- Returns "Nothing" if check is successful and an "IncorrectPassword" error
-- otherwise
--
authenticatePassword :: AuthUser -- ^ Looked up from the back-end
-> Password -- ^ Check against this password
-> Maybe AuthFailure
authenticatePassword u pw = auth
where
auth = case userPassword u of
Nothing -> Just PasswordMissing
Just upw -> check $ checkPassword pw upw
check b = if b then Nothing else Just IncorrectPassword
------------------------------------------------------------------------------
-- | Wrap lookups around request-local cache
--
cacheOrLookup
:: Handler b (AuthManager b) (Maybe AuthUser)
-- ^ Lookup action to perform if request local cache is empty
-> Handler b (AuthManager b) (Maybe AuthUser)
cacheOrLookup f = do
au <- gets activeUser
if isJust au
then return au
else do
au' <- f
modify (\mgr -> mgr { activeUser = au' })
return au'
------------------------------------------------------------------------------
-- | Register a new user by specifying login and password 'Param' fields
--
registerUser
:: ByteString -- ^ Login field
-> ByteString -- ^ Password field
-> Handler b (AuthManager b) (Either AuthFailure AuthUser)
registerUser lf pf = do
l <- fmap decodeUtf8 <$> getParam lf
p <- getParam pf
let l' = note UsernameMissing l
let p' = note PasswordMissing p
-- In case of multiple AuthFailure, the first available one
-- will be propagated.
case liftM2 (,) l' p' of
Left e -> return $ Left e
Right (lgn, pwd) -> createUser lgn pwd
------------------------------------------------------------------------------
-- | A 'MonadSnap' handler that processes a login form.
--
-- The request paremeters are passed to 'performLogin'
--
-- To make your users stay logged in for longer than the session replay
-- prevention timeout, you must pass a field name as the third parameter and
-- that field must be set to a value of \"1\" by the submitting form. This
-- lets you use a user selectable check box. Or if you want user remembering
-- always turned on, you can use a hidden form field.
loginUser
:: ByteString
-- ^ Username field
-> ByteString
-- ^ Password field
-> Maybe ByteString
-- ^ Remember field; Nothing if you want no remember function.
-> (AuthFailure -> Handler b (AuthManager b) ())
-- ^ Upon failure
-> Handler b (AuthManager b) ()
-- ^ Upon success
-> Handler b (AuthManager b) ()
loginUser unf pwdf remf loginFail loginSucc =
runEitherT (loginUser' unf pwdf remf)
>>= either loginFail (const loginSucc)
------------------------------------------------------------------------------
loginUser' :: ByteString
-> ByteString
-> Maybe ByteString
-> EitherT AuthFailure (Handler b (AuthManager b)) AuthUser
loginUser' unf pwdf remf = do
mbUsername <- lift $ getParam unf
mbPassword <- lift $ getParam pwdf
remember <- lift $ liftM (fromMaybe False)
(runMaybeT $
do field <- MaybeT $ return remf
value <- MaybeT $ getParam field
return $ value == "1")
password <- noteT PasswordMissing $ hoistMaybe mbPassword
username <- noteT UsernameMissing $ hoistMaybe mbUsername
EitherT $ loginByUsername username (ClearText password) remember
------------------------------------------------------------------------------
-- | Simple handler to log the user out. Deletes user from session.
--
logoutUser :: Handler b (AuthManager b) () -- ^ What to do after logging out
-> Handler b (AuthManager b) ()
logoutUser target = logout >> target
------------------------------------------------------------------------------
-- | Require that an authenticated 'AuthUser' is present in the current
-- session.
--
-- This function has no DB cost - only checks to see if a user_id is present
-- in the current session.
--
requireUser :: SnapletLens b (AuthManager b)
-- ^ Lens reference to an "AuthManager"
-> Handler b v a
-- ^ Do this if no authenticated user is present.
-> Handler b v a
-- ^ Do this if an authenticated user is present.
-> Handler b v a
requireUser auth bad good = do
loggedIn <- withTop auth isLoggedIn
if loggedIn then good else bad
------------------------------------------------------------------------------
-- | Run a function on the backend, and return the result.
--
-- This uses an existential type so that the backend type doesn't
-- 'escape' AuthManager. The reason that the type is Handler b
-- (AuthManager v) a and not a is because anything that uses the
-- backend will return an IO something, which you can liftIO, or a
-- Handler b (AuthManager v) a if it uses other handler things.
--
withBackend ::
(forall r. (IAuthBackend r) => r -> Handler b (AuthManager v) a)
-- ^ The function to run with the handler.
-> Handler b (AuthManager v) a
withBackend f = join $ do
(AuthManager backend_ _ _ _ _ _ _ _ _) <- get
return $ f backend_
------------------------------------------------------------------------------
-- | This function generates a random password reset token and stores it in
-- the database for the user. Call this function when a user forgets their
-- password. Then use the token to autogenerate a link that the user can
-- visit to reset their password. This function also sets a timestamp so the
-- reset token can be expired.
setPasswordResetToken :: Text -> Handler b (AuthManager b) (Maybe Text)
setPasswordResetToken login = do
tokBS <- liftIO . randomToken 40 =<< gets randomNumberGenerator
let token = decodeUtf8 tokBS
now <- liftIO getCurrentTime
success <- modPasswordResetToken login (Just token) (Just now)
return $ if success then Just token else Nothing
------------------------------------------------------------------------------
-- | Clears a user's password reset token. Call this when the user
-- successfully changes their password to ensure that the password reset link
-- cannot be used again.
clearPasswordResetToken :: Text -> Handler b (AuthManager b) Bool
clearPasswordResetToken login = modPasswordResetToken login Nothing Nothing
------------------------------------------------------------------------------
-- | Helper function used for setting and clearing the password reset token
-- and associated timestamp.
modPasswordResetToken :: Text
-> Maybe Text
-> Maybe UTCTime
-> Handler v (AuthManager v) Bool
modPasswordResetToken login token timestamp = do
res <- runMaybeT $ do
u <- MaybeT $ withBackend $ \b -> liftIO $ lookupByLogin b login
lift $ saveUser $ u
{ userResetToken = token
, userResetRequestedAt = timestamp
}
return ()
return $ maybe False (\_ -> True) res