packages feed

yesod-auth-nopassword (empty) → 0.1.0.1

raw patch · 5 files changed

+318/−0 lines, 5 filesdep +basedep +blaze-markupdep +http-typessetup-changed

Dependencies added: base, blaze-markup, http-types, pwstore-fast, text, uuid, yesod-auth, yesod-core, yesod-form

Files

+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2016 Dan Palmer++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,3 @@+### yesod-auth-nopassword++A plugin for Yesod to provide email-only authentication.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Yesod/Auth/NoPassword.hs view
@@ -0,0 +1,259 @@+-- | This auth plugin for Yesod enabled simple passwordless authentication.+--+-- The only detail required from a user is an email address, and accounts are+-- either updated or created, depending on whether the account exists or not.+-- To actually log in, users are sent an email containing a link that+-- authenticates them and logs them in.+--+-- This plugin provides:+--+-- * Token generation+-- * Orchestration of the login process and email sending+-- * Receiving of the login form data via HTTP POST.+-- * Authentication of users once they return to the site from an email+--+-- This plugin /does not/ provide:+--+-- * A login form+-- * Email rendering or sending+-- * An account model+-- * A viewable interface (i.e. via HTTP GET) for the login form+--+-- These are left for the user of the plugin to implement so that they can+-- retain control over form functionality, account models, email design and+-- email service provider.+--+-- Implementation checklist:+--+-- 1. Implement an instance of 'NoPasswordAuth' for your Yesod application.+-- 2. Implement a Yesod form that resolves to an 'EmailForm'.+-- 3. Add `authNoPassword` to your authentication plugins in your instance of+--    `YesodAuth`, passing the form you wish to use for authentication. This+--    typeclass provides a number of methods for customisation of behaviour,+--    but the minimal implementation is:+--+--     * 'loginRoute'+--     * 'emailSentRoute'+--     * 'sendLoginEmail'+--     * 'getUserByEmail'+--     * 'getEmailAndHashByTokenId'+--     * 'updateLoginHashForUser'+--     * 'newUserWithLoginHash'++module Yesod.Auth.NoPassword (+    -- * Plugin+      authNoPassword+    -- * Form Type+    , EmailForm(..)+    -- * Typeclass+    , NoPasswordAuth(..)+    -- * Types+    , Email+    , Token+    , TokenId+    , Hash+    -- ** Utility+    , loginPostR+) where++import Prelude++import Data.Monoid ((<>))+import Data.Text+import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import qualified Text.Blaze as B++import qualified Data.UUID as U+import qualified Data.UUID.V4 as U++import Network.HTTP.Types.URI (urlEncode, urlDecode)++import Yesod.Core+import Yesod.Form+import Yesod.Auth+import Crypto.PasswordStore++-- Constants++pluginName :: Text+pluginName = "email"++-- | Route to which the site should POST the email form form.+loginPostR :: AuthRoute+loginPostR = PluginR pluginName ["login"]+++type Email = Text+type Token = Text+type TokenId = Text+type Hash = Text+++-- | Data type required for the Yesod form.+newtype EmailForm = EmailForm+    { efEmail :: Email+    }+++-- Convenience alias for forms+type Form m a = (Html -> MForm (HandlerT m IO) (FormResult a, WidgetT m IO ()))+++-- | Function to create the Yesod Auth plugin. Must be used by a type with an+-- instance for 'NoPasswordAuth', and must be given a form to use.+authNoPassword :: NoPasswordAuth m+               => Form m EmailForm+               -> AuthPlugin m+authNoPassword form = AuthPlugin pluginName dispatch login+    where+        login _ = error "NoPasswordAuth does not provide a login widget"++        dispatch "POST" ["login"] = postEmailR form+        dispatch "GET"  ["login"] = getLoginR+        dispatch _ _ = notFound+++postEmailR :: NoPasswordAuth m+           => Form m EmailForm+           -> HandlerT Auth (HandlerT m IO) TypedContent+postEmailR form = do+    ((result, _), _) <- lift $ runFormPost form+    master <- lift getYesod+    case result of+        FormMissing -> do+            setMessage "Something went wrong, please try again"+            lift $ redirect (emailSentRoute master)+        FormFailure as -> do+            mapM_ (setMessage . B.text) as+            lift $ redirect (emailSentRoute master)+        FormSuccess e -> do+            let email = efEmail e+            strength <- lift $ tokenStrength+            (hash, token) <- liftIO $ genToken strength+            muser <- lift $ getUserByEmail (efEmail e)+            tid <- liftIO genTokenId+            case muser of+                Just user ->+                    lift $ updateLoginHashForUser user (Just hash) tid+                Nothing ->+                    lift $ newUserWithLoginHash email hash tid+            setMessage $ B.text "Check your email for a login link"+            url <- genUrl token tid+            lift $ sendLoginEmail email url+            lift $ redirect (emailSentRoute master)+++getLoginR :: NoPasswordAuth m => HandlerT Auth (HandlerT m IO) TypedContent+getLoginR = do+    paramName <- lift tokenParamName+    loginParam <- lookupGetParam paramName+    case (unpackTokenParam loginParam) of+        Nothing -> permissionDenied "Missing login token"+        Just (tid, loginToken) -> do+            muser <- lift $ getEmailAndHashByTokenId tid+            case muser of+                Nothing -> permissionDenied "No login token sent"+                Just (email, hash) ->+                    if (verifyToken hash loginToken)+                        then lift $ setCredsRedirect (Creds pluginName email [])+                        else permissionDenied "Incorrect login token"+++unpackTokenParam :: Maybe Text -> Maybe (TokenId, Token)+unpackTokenParam param = do+    p <- param+    case (splitOn ":" p) of+        (tid:tkn:[]) -> Just (tid, tkn)+        _ -> Nothing+++genToken :: Int -> IO (Hash, Token)+genToken strength = do+    tokenSalt <- genSaltIO+    let token = exportSalt tokenSalt+    hash <- makePassword token strength+    return (decodeUtf8 hash, decodeUtf8 (urlEncode False token))+++verifyToken :: Hash -> Token -> Bool+verifyToken hash token = verifyPassword t h+    where+        h = encodeUtf8 hash+        t = urlDecode False (encodeUtf8 token)+++genTokenId :: IO TokenId+genTokenId = U.toText <$> U.nextRandom+++genUrl :: NoPasswordAuth m => Token -> TokenId -> HandlerT Auth (HandlerT m IO) Text+genUrl token tid = do+    tm <- getRouteToParent+    render <- lift getUrlRender+    paramName <- lift tokenParamName+    let query = "?" <> paramName <> "=" <> tid <> ":" <> token+    return $ (render $ tm loginPostR) <> query+++class YesodAuthPersist master => NoPasswordAuth master where+    -- | Route to a page that dispays a login form. This is not provided by+    -- the plugin.+    loginRoute :: master -> Route master++    -- | Route to which the user should be sent after entering an email+    -- address. This is not provided by the plugin.+    --+    -- __Note__: the user will not be authenticated when they reach the page.+    emailSentRoute :: master -> Route master++    -- | Send a login email.+    sendLoginEmail :: Email -- ^ The email to send to+                   -> Text  -- ^ The URL that will log the user in+                   -> HandlerT master IO ()++    -- | Get a user by their email address. Used to determine if the user exists or not.+    getUserByEmail :: Email -> HandlerT master IO (Maybe (AuthId master))++    -- | Get a Hash by a TokenId.+    --+    -- Invoked when the user returns to the site from an email. We don't know+    -- who the user is at this point as they may open the link from the email+    -- on another device or in another browser, so session data can't be used.+    -- Equally we do not want to pass the user's ID or email address in a URL+    -- if we don't have to, so instead we look up users by the 'TokenId' that+    -- we issued them earlier in the process.+    getEmailAndHashByTokenId :: TokenId -> HandlerT master IO (Maybe (Email, Hash))++    -- | Update a user's login hash+    --+    -- This is also used to blank out the hash once the user has logged in, or+    -- can be used to prevent the user from logging in, so must accept a value+    -- of `Nothing`.+    --+    -- /It is recommended that the/ 'TokenId' /storage be enforced as unique/.+    -- For this reason, the token is not passed as a maybe, as some storage+    -- backends treat `NULL` values as the same.+    updateLoginHashForUser :: (AuthId master) -> Maybe Hash -> TokenId -> HandlerT master IO ()++    -- | Create a new user with an email address and hash.+    newUserWithLoginHash :: Email -> Hash -> TokenId -> HandlerT master IO ()++    -- | __Optional__ – return a custom token strength.+    --+    -- A token strength of @x@ equates to @2^x@ hash rounds.+    tokenStrength :: HandlerT master IO Int+    tokenStrength = return 17++    -- | __Optional__ – return a custom token param name.+    tokenParamName :: HandlerT master IO Text+    tokenParamName = return "tkn"++    {-+        MINIMAL loginRoute+              , emailSentRoute+              , sendLoginEmail+              , getUserByEmail+              , getEmailAndHashByTokenId+              , updateLoginHashForUser+              , newUserWithLoginHash+    -}
+ yesod-auth-nopassword.cabal view
@@ -0,0 +1,33 @@+name:                yesod-auth-nopassword+version:             0.1.0.1+synopsis:            A plugin for Yesod to provide email-only authentication.+description:         Please see README.md+homepage:            https://github.com/danpalmer/yesod-auth-nopassword#readme+license:             MIT+license-file:        LICENSE+author:              Dan Palmer+maintainer:          dan@danpalmer.me+copyright:           2016 Dan Palmer+category:            Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Yesod.Auth.NoPassword+  default-language:    Haskell2010+  default-extensions:  OverloadedStrings+  build-depends:       base >= 4.7 && < 5+                     , text >= 0.11 && < 2.0+                     , blaze-markup >= 0.7 && < 1.0+                     , pwstore-fast >= 2.4 && < 3.0+                     , uuid >= 1.3 && < 1.4+                     , http-types >= 0.9 && < 1.0+                     , yesod-core >= 1.4.17 && < 1.5+                     , yesod-auth >= 1.4.0 && < 1.5+                     , yesod-form >= 1.4.0 && < 1.5++source-repository head+  type:     git+  location: https://github.com/danpalmer/yesod-auth-nopassword