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 ## 1.4.14
* Remove Google OpenID link [#1309](https://github.com/yesodweb/yesod/pull/1309) * 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 ## 1.4.13.5

View File

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

View File

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