Merge pull request #1317 from psibi/json-auth

JSON endpoints for Auth.Email, haddock, and i18n fix
This commit is contained in:
Michael Snoyman 2016-12-07 09:23:00 -05:00 committed by GitHub
commit d7be78f82e
3 changed files with 165 additions and 89 deletions

View File

@ -1,6 +1,13 @@
## 1.4.14.1
* Add JSON endpoints to Yesod.Auth.Email module
* Export croatianMessage from Message module
* Minor Haddock rendering fixes at Auth.Email module
## 1.4.14
* Remove Google OpenID link [#1309](https://github.com/yesodweb/yesod/pull/1309)
* Add CSRF Security check in `registerHelperFunction` [#1302](https://github.com/yesodweb/yesod/pull/1302)
## 1.4.13.5

View File

@ -4,23 +4,30 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables#-}
{-# LANGUAGE TypeFamilies #-}
-- | A Yesod plugin for Authentication via e-mail
--
-- This plugin works out of the box by only setting a few methods on the type class
-- that tell the plugin how to interoprate with your user data storage (your database).
-- However, almost everything is customizeable by setting more methods on the type class.
-- In addition, you can send all the form submissions via JSON and completely control the user's flow.
-- This plugin works out of the box by only setting a few methods on
-- the type class that tell the plugin how to interoperate with your
-- user data storage (your database). However, almost everything is
-- customizeable by setting more methods on the type class. In
-- addition, you can send all the form submissions via JSON and
-- completely control the user's flow.
--
-- This is a standard registration e-mail flow
--
-- 1) A user registers a new e-mail address, and an e-mail is sent there
-- 2) The user clicks on the registration link in the e-mail
-- Note that at this point they are actually logged in (without a password)
-- That means that when they log out they will need to reset their password
-- 3) The user sets their password and is redirected to the site.
-- 4) The user can now
-- * logout and sign in
-- * reset their password
-- 1. A user registers a new e-mail address, and an e-mail is sent there
-- 2. The user clicks on the registration link in the e-mail. Note that
-- at this point they are actually logged in (without a
-- password). That means that when they log out they will need to
-- reset their password.
-- 3. The user sets their password and is redirected to the site.
-- 4. The user can now
--
-- * logout and sign in
-- * reset their password
--
module Yesod.Auth.Email
( -- * Plugin
authEmail
@ -70,6 +77,9 @@ import Data.Time (addUTCTime, getCurrentTime)
import Safe (readMay)
import System.IO.Unsafe (unsafePerformIO)
import qualified Text.Email.Validate
import Network.HTTP.Types.Status (status400)
import Data.Aeson.Types (Parser(..), Result(..), parseMaybe, withObject, (.:?))
import Data.Maybe (isJust, isNothing, fromJust)
loginR, registerR, forgotPasswordR, setpassR :: AuthRoute
loginR = PluginR "email" ["login"]
@ -79,7 +89,7 @@ setpassR = PluginR "email" ["set-password"]
-- |
--
-- Since 1.4.5
-- @since 1.4.5
verifyR :: Text -> Text -> AuthRoute -- FIXME
verifyR eid verkey = PluginR "email" ["verify", eid, verkey]
@ -94,7 +104,7 @@ type VerStatus = Bool
--
-- Note that any of these other identifiers must not be valid email addresses.
--
-- Since 1.2.0
-- @since 1.2.0
type Identifier = Text
-- | Data stored in a database for each e-mail address.
@ -121,22 +131,22 @@ class ( YesodAuth site
-- | Add a new email address to the database, but indicate that the address
-- has not yet been verified.
--
-- Since 1.1.0
-- @since 1.1.0
addUnverified :: Email -> VerKey -> HandlerT site IO (AuthEmailId site)
-- | Send an email to the given address to verify ownership.
--
-- Since 1.1.0
-- @since 1.1.0
sendVerifyEmail :: Email -> VerKey -> VerUrl -> HandlerT site IO ()
-- | Get the verification key for the given email ID.
--
-- Since 1.1.0
-- @since 1.1.0
getVerifyKey :: AuthEmailId site -> HandlerT site IO (Maybe VerKey)
-- | Set the verification key for the given email ID.
--
-- Since 1.1.0
-- @since 1.1.0
setVerifyKey :: AuthEmailId site -> VerKey -> HandlerT site IO ()
-- | Verify the email address on the given account.
@ -147,39 +157,39 @@ class ( YesodAuth site
--
-- See <https://github.com/yesodweb/yesod/issues/1222>.
--
-- Since 1.1.0
-- @since 1.1.0
verifyAccount :: AuthEmailId site -> HandlerT site IO (Maybe (AuthId site))
-- | Get the salted password for the given account.
--
-- Since 1.1.0
-- @since 1.1.0
getPassword :: AuthId site -> HandlerT site IO (Maybe SaltedPass)
-- | Set the salted password for the given account.
--
-- Since 1.1.0
-- @since 1.1.0
setPassword :: AuthId site -> SaltedPass -> HandlerT site IO ()
-- | Get the credentials for the given @Identifier@, which may be either an
-- email address or some other identification (e.g., username).
--
-- Since 1.2.0
-- @since 1.2.0
getEmailCreds :: Identifier -> HandlerT site IO (Maybe (EmailCreds site))
-- | Get the email address for the given email ID.
--
-- Since 1.1.0
-- @since 1.1.0
getEmail :: AuthEmailId site -> HandlerT site IO (Maybe Email)
-- | Generate a random alphanumeric string.
--
-- Since 1.1.0
-- @since 1.1.0
randomKey :: site -> IO Text
randomKey _ = Nonce.nonce128urlT defaultNonceGen
-- | Route to send user to after password has been set correctly.
--
-- Since 1.2.0
-- @since 1.2.0
afterPasswordRoute :: site -> Route site
-- | Does the user need to provide the current password in order to set a
@ -187,7 +197,7 @@ class ( YesodAuth site
--
-- Default: if the user logged in via an email link do not require a password.
--
-- Since 1.2.1
-- @since 1.2.1
needOldPassword :: AuthId site -> HandlerT site IO Bool
needOldPassword aid' = do
mkey <- lookupSession loginLinkKey
@ -207,7 +217,7 @@ class ( YesodAuth site
-- | Response after sending a confirmation email.
--
-- Since 1.2.2
-- @since 1.2.2
confirmationEmailSentResponse :: Text -> HandlerT site IO TypedContent
confirmationEmailSentResponse identifier = do
mr <- getMessageRender
@ -223,7 +233,7 @@ class ( YesodAuth site
--
-- Default: Lower case the email address.
--
-- Since 1.2.3
-- @since 1.2.3
normalizeEmailAddress :: site -> Text -> Text
normalizeEmailAddress _ = TS.toLower
@ -233,7 +243,7 @@ class ( YesodAuth site
--
-- Default: 'defaultRegisterHandler'.
--
-- Since: 1.2.6.
-- @since: 1.2.6
registerHandler :: AuthHandler site Html
registerHandler = defaultRegisterHandler
@ -243,7 +253,7 @@ class ( YesodAuth site
--
-- Default: 'defaultForgotPasswordHandler'.
--
-- Since: 1.2.6.
-- @since: 1.2.6
forgotPasswordHandler :: AuthHandler site Html
forgotPasswordHandler = defaultForgotPasswordHandler
@ -253,7 +263,7 @@ class ( YesodAuth site
--
-- Default: 'defaultSetPasswordHandler'.
--
-- Since: 1.2.6.
-- @since: 1.2.6
setPasswordHandler ::
Bool
-- ^ Whether the old password is needed. If @True@, a
@ -340,7 +350,7 @@ emailLoginHandler toParent = do
return $ renderAuthMessage master langs msg
-- | Default implementation of 'registerHandler'.
--
-- Since: 1.2.6
-- @since 1.2.6
defaultRegisterHandler :: YesodAuthEmail master => AuthHandler master Html
defaultRegisterHandler = do
(widget, enctype) <- lift $ generateFormPost registrationForm
@ -376,6 +386,11 @@ defaultRegisterHandler = do
return (userRes, widget)
parseEmail :: Value -> Parser Text
parseEmail = withObject "email" (\obj -> do
email' <- obj .: "email"
return email')
registerHelper :: YesodAuthEmail master
=> Bool -- ^ allow usernames?
-> Route Auth
@ -383,7 +398,15 @@ registerHelper :: YesodAuthEmail master
registerHelper allowUsername dest = do
y <- lift getYesod
checkCsrfHeaderOrParam defaultCsrfHeaderName defaultCsrfParamName
midentifier <- lookupPostParam "email"
pidentifier <- lookupPostParam "email"
midentifier <- case pidentifier of
Nothing -> do
(jidentifier :: Result Value) <- lift parseJsonBody
case jidentifier of
Error _ -> return Nothing
Success val -> return $ parseMaybe parseEmail val
Just _ -> return pidentifier
let eidentifier = case midentifier of
Nothing -> Left Msg.NoIdentifierProvided
Just x
@ -425,7 +448,7 @@ getForgotPasswordR = forgotPasswordHandler
-- | Default implementation of 'forgotPasswordHandler'.
--
-- Since: 1.2.6
-- @since 1.2.6
defaultForgotPasswordHandler :: YesodAuthEmail master => AuthHandler master Html
defaultForgotPasswordHandler = do
(widget, enctype) <- lift $ generateFormPost forgotPasswordForm
@ -497,38 +520,56 @@ $newline never
|]
parseCreds :: Value -> Parser (Text, Text)
parseCreds = withObject "creds" (\obj -> do
email' <- obj .: "email"
pass <- obj .: "password"
return (email', pass))
postLoginR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
postLoginR = do
(identifier, pass) <- lift $ runInputPost $ (,)
result <- lift $ runInputPostResult $ (,)
<$> ireq textField "email"
<*> ireq textField "password"
mecreds <- lift $ getEmailCreds identifier
maid <-
case ( mecreds >>= emailCredsAuthId
, emailCredsEmail <$> mecreds
, emailCredsStatus <$> mecreds
) of
(Just aid, Just email, Just True) -> do
mrealpass <- lift $ getPassword aid
case mrealpass of
Nothing -> return Nothing
Just realpass -> return $
if isValidPass pass realpass
then Just email
else Nothing
_ -> return Nothing
let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier
case maid of
Just email ->
lift $ setCredsRedirect $ Creds
(if isEmail then "email" else "username")
email
[("verifiedEmail", email)]
Nothing ->
loginErrorMessageI LoginR $
if isEmail
then Msg.InvalidEmailPass
else Msg.InvalidUsernamePass
midentifier <- case result of
FormSuccess (iden, pass) -> return $ Just (iden, pass)
_ -> do
(creds :: Result Value) <- lift parseJsonBody
case creds of
Error _ -> return Nothing
Success val -> return $ parseMaybe parseCreds val
case midentifier of
Nothing -> loginErrorMessageI LoginR Msg.NoIdentifierProvided
Just (identifier, pass) -> do
mecreds <- lift $ getEmailCreds identifier
maid <-
case ( mecreds >>= emailCredsAuthId
, emailCredsEmail <$> mecreds
, emailCredsStatus <$> mecreds
) of
(Just aid, Just email, Just True) -> do
mrealpass <- lift $ getPassword aid
case mrealpass of
Nothing -> return Nothing
Just realpass -> return $ if isValidPass pass realpass
then Just email
else Nothing
_ -> return Nothing
let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier
case maid of
Just email ->
lift $ setCredsRedirect $ Creds
(if isEmail then "email" else "username")
email
[("verifiedEmail", email)]
Nothing ->
loginErrorMessageI LoginR $
if isEmail
then Msg.InvalidEmailPass
else Msg.InvalidUsernamePass
getPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
getPasswordR = do
@ -541,7 +582,7 @@ getPasswordR = do
-- | Default implementation of 'setPasswordHandler'.
--
-- Since: 1.2.6
-- @since 1.2.6
defaultSetPasswordHandler :: YesodAuthEmail master => Bool -> AuthHandler master TypedContent
defaultSetPasswordHandler needOld = do
messageRender <- lift getMessageRender
@ -614,54 +655,81 @@ defaultSetPasswordHandler needOld = do
fsAttrs = [("autofocus", "")]
}
parsePassword :: Value -> Parser (Text, Text, Maybe Text)
parsePassword = withObject "password" (\obj -> do
email' <- obj .: "new"
pass <- obj .: "confirm"
curr <- obj .:? "current"
return (email', pass, curr))
postPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
postPasswordR = do
maid <- lift maybeAuthId
(creds :: Result Value) <- lift parseJsonBody
let jcreds = case creds of
Error _ -> Nothing
Success val -> parseMaybe parsePassword val
let doJsonParsing = isJust jcreds
case maid of
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
Just aid -> do
tm <- getRouteToParent
needOld <- lift $ needOldPassword aid
if not needOld then confirmPassword aid tm else do
current <- lift $ runInputPost $ ireq textField "current"
if not needOld then confirmPassword aid tm jcreds else do
res <- lift $ runInputPostResult $ ireq textField "current"
let fcurrent = case res of
FormSuccess currentPass -> Just currentPass
_ -> Nothing
let current = if doJsonParsing
then getThird jcreds
else fcurrent
mrealpass <- lift $ getPassword aid
case mrealpass of
Nothing ->
lift $ loginErrorMessage (tm setpassR) "You do not currently have a password set on your account"
Just realpass
| isValidPass current realpass -> confirmPassword aid tm
| isNothing current -> loginErrorMessageI LoginR Msg.BadSetPass
| isValidPass (fromJust current) realpass -> confirmPassword aid tm jcreds
| otherwise ->
lift $ loginErrorMessage (tm setpassR) "Invalid current password, please try again"
where
msgOk = Msg.PassUpdated
confirmPassword aid tm = do
(new, confirm) <- lift $ runInputPost $ (,)
getThird (Just (_,_,t)) = t
getThird Nothing = Nothing
getNewConfirm (Just (a,b,_)) = Just (a,b)
getNewConfirm _ = Nothing
confirmPassword aid tm jcreds = do
res <- lift $ runInputPostResult $ (,)
<$> ireq textField "new"
<*> ireq textField "confirm"
if new /= confirm
then loginErrorMessageI setpassR Msg.PassMismatch
else do
isSecure <- lift $ checkPasswordSecurity aid new
case isSecure of
let creds = if (isJust jcreds)
then getNewConfirm jcreds
else case res of
FormSuccess res' -> Just res'
_ -> Nothing
case creds of
Nothing -> loginErrorMessageI setpassR Msg.PassMismatch
Just (new, confirm) ->
if new /= confirm
then loginErrorMessageI setpassR Msg.PassMismatch
else do
isSecure <- lift $ checkPasswordSecurity aid new
case isSecure of
Left e -> lift $ loginErrorMessage (tm setpassR) e
Right () -> do
salted <- liftIO $ saltPass new
y <- lift $ do
setPassword aid salted
deleteSession loginLinkKey
addMessageI "success" msgOk
getYesod
salted <- liftIO $ saltPass new
y <- lift $ do
setPassword aid salted
deleteSession loginLinkKey
addMessageI "success" msgOk
getYesod
mr <- lift getMessageRender
selectRep $ do
provideRep $
fmap asHtml $ lift $ redirect $ afterPasswordRoute y
provideJsonMessage (mr msgOk)
mr <- lift getMessageRender
selectRep $ do
provideRep $
fmap asHtml $ lift $ redirect $ afterPasswordRoute y
provideJsonMessage (mr msgOk)
saltLength :: Int
saltLength = 5
@ -695,13 +763,13 @@ isValidPass' clear' salted' =
-- | Session variable set when user logged in via a login link. See
-- 'needOldPassword'.
--
-- Since 1.2.1
-- @since 1.2.1
loginLinkKey :: Text
loginLinkKey = "_AUTH_EMAIL_LOGIN_LINK"
-- | Set 'loginLinkKey' to the current time.
--
-- Since 1.2.1
-- @since 1.2.1
setLoginLinkKey :: (YesodAuthEmail site, MonadHandler m, HandlerSite m ~ site) => AuthId site -> m ()
setLoginLinkKey aid = do
now <- liftIO getCurrentTime

View File

@ -13,6 +13,7 @@ module Yesod.Auth.Message
, japaneseMessage
, finnishMessage
, chineseMessage
, croatianMessage
, spanishMessage
, czechMessage
, russianMessage