diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,126 @@
+## 1.7.1.6
+
+* Bump upper bound on `yesod-form` to allow 1.7
+
+## 1.7.1.3
+
+* Support `persistent-2.11` [#8](https://github.com/paul-rouse/yesod-auth-hashdb/pull/8)
+
+## 1.7.1.2
+
+* Fix test to allow use of persistent-template-2.8
+
+## 1.7.1.1
+
+* Fix test and relax upper bound for persistent-2.10 / persistent-template-2.7
+* Replace use of deprecated `requireJsonBody`
+
+## 1.7.1
+
+* Relax upper bounds to allow persistent-2.9 (for GHC 8.6 versions of Stackage nightly)
+* Remove testing of GHC below 8.0.2, and lts below 9
+
+## 1.7
+
+* Update for changes in yesod version 1.6, but retain compatibility with previous versions
+* Remove support for GHC below 7.10, and lts below 6
+
+## 1.6.2
+
+* Use `PasswordStore` from `yesod-auth` instead of `pwstore-fast` (uses `cryptonite` instead of `cryptohash`)
+
+## 1.6.1
+
+* Relax upper bound on persistent
+
+## 1.6.0.1
+
+* Fix serious documentation layout problem caused by typo
+
+## 1.6
+
+This release completes the breaking changes started in 1.5.  For details
+of upgrading, please see
+[Upgrading.md](https://github.com/paul-rouse/yesod-auth-hashdb/blob/master/Upgrading.md).
+
+* Complete removal of compatibility with old databases designed for versions before 1.3
+* Add JSON support
+
+## 1.5.1.3
+
+* Fix test failure with basic-prelude >= 0.6 (#6)
+
+## 1.5.1.2
+
+* Relax upper bound to allow persistent-2.6
+
+## 1.5.1.1
+
+* Minor documentation improvement
+* Reduce external-library dependencies for tests
+
+## 1.5.1
+
+* Include CSRF token in default form
+
+## 1.5
+
+This release can break both old code and old database entries.  For details
+of upgrading, please see
+[Upgrading.md](https://github.com/paul-rouse/yesod-auth-hashdb/blob/master/Upgrading.md).
+
+* First phase of removing compatibility with old databases designed for versions before 1.3
+* Remove deprecated utilities (`getAuthIdHashDB` and pre-defined `User` data type)
+
+## 1.4.3
+
+* Changes to work with persistent-2.5
+
+## 1.4.2.2
+
+* Relax upper bound to allow persistent-2.2.*
+
+## 1.4.2.1
+
+* Add ChangeLog
+
+## 1.4.2
+
+* Deprecate `getAuthIdHashDB` (see [#5](https://github.com/paul-rouse/yesod-auth-hashdb/issues/5))
+
+## 1.4.1.2
+
+* Use internationalized messages
+* Increase `defaultStrength`
+
+## 1.4.1.1
+
+* Minor documentation change
+
+## 1.4.1
+
+* Expose additional validation function which does not need to read the database
+* Deprecate compatibility with old data which includes a salt field
+
+## 1.4.0
+
+* Changes for Yesod 1.4
+
+## 1.3.2
+
+* Documentation improvement
+
+## 1.3.1
+
+* Optional custom login form
+* Deprecate predefined `User` data type
+* Changes for Persistent 2
+
+## 1.3.0.1
+
+* Version bounds
+* Minor documentation changes
+
+## 1.3
+
+* First release as a separate package, not part of yesod-auth
diff --git a/Yesod/Auth/HashDB.hs b/Yesod/Auth/HashDB.hs
--- a/Yesod/Auth/HashDB.hs
+++ b/Yesod/Auth/HashDB.hs
@@ -1,197 +1,219 @@
-{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE CPP                        #-}
 {-# LANGUAGE QuasiQuotes                #-}
 {-# LANGUAGE ConstraintKinds            #-}
 {-# LANGUAGE FlexibleContexts           #-}
 {-# LANGUAGE TypeFamilies               #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE RankNTypes                 #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
 {-# LANGUAGE OverloadedStrings          #-}
-{-# LANGUAGE GADTs                      #-}
-{-# LANGUAGE CPP                        #-}
 -------------------------------------------------------------------------------
 -- |
 -- Module      :  Yesod.Auth.HashDB
--- Copyright   :  (c) Patrick Brisbin 2010, Paul Rouse 2014
+-- Copyright   :  (c) Patrick Brisbin 2010, Paul Rouse 2014-2016
 -- License     :  MIT
 --
 -- Maintainer  :  Paul Rouse <pyr@doynton.org>
 -- Stability   :  Stable
 -- Portability :  Portable
 --
--- A yesod-auth AuthPlugin designed to look users up in Persist where
--- their user id's and a hash of their password is stored.
---
--- This module was removed from @yesod-auth-1.3.0.0@ and is now
--- maintained separately.
--- Versions of this module prior to @yesod-auth-1.3@ used a relatively weak
--- hashing algorithm (a single round of SHA1) which does not provide
--- adequate protection against an attacker who discovers the hashed passwords.
--- See: <https://github.com/yesodweb/yesod/issues/668>.
---
--- It has now been rewritten to use "Crypto.PasswordStore", but this has been
--- done in a way which preserves compatibility both with the API and
--- with databases which have been set up using older versions of this module.
--- There are two levels of database compatibility:
---
---     * The verification code recognises both the old and new hash formats,
---       so passwords can be verified against database entries which still
---       contain old-style hashes.
---
---     * The function 'upgradePasswordHash' can be used to migrate
---       existing user records to use the new format hash.  Unlike
---       freshly created password hashes, entries converted this way
---       must still have the old salt field, since the old hash function
---       remains part of the algorithm needed for verification.  (The
---       new hash is layered on top of the old one.)
---
--- On the other hand, new passwords set up by 'setPassword' or
--- 'setPasswordStrength' no longer use a separate salt field, so new users
--- of this module need only provide a single password field in the user data,
--- and can ignore the salt.
+-- A Yesod authentication plugin designed to look users up in a Persistent
+-- database where the hash of their password is stored.
 --
--- In a system which has been migrated from the old format, passwords
--- which are reset using the new format will have an empty salt field.
--- Once all the entries are of this form, it is safe to change the model
--- to remove the salt, and change the 'HashDBUser' instance accordingly.
+-- __Releases 1.6 finishes the process of removing compatibility with old__
+-- __(pre 1.3) databases.  Please see__
+-- __<https://github.com/paul-rouse/yesod-auth-hashdb/blob/master/Upgrading.md>__
 --
--- To use this in a Yesod application, it must be an instance of
--- YesodPersist, and the username and hashed-passwords should be added
--- to the database.  The followng steps give an outline of what is required.
+-- To use this in a Yesod application, the foundation data type must be an
+-- instance of YesodPersist, and the username and hashed passwords should
+-- be added to the database.  The following steps give an outline of what
+-- is required.
 --
 -- You need a database table to store user records: in a scaffolded site it
 -- might look like:
 --
 -- > User
--- >     name Text             -- user name used by HashDB
+-- >     name Text             -- user name used to uniquely identify users
 -- >     password Text Maybe   -- password hash for HashDB
 -- >     UniqueUser name
 --
 -- Create an instance of 'HashDBUser' for this data type:
 --
+-- > import Yesod.Auth.HashDB (HashDBUser(..))
+-- > ....
 -- > instance HashDBUser User where
 -- >     userPasswordHash = userPassword
--- >     setPasswordHash h p = p { userPassword = Just h }
+-- >     setPasswordHash h u = u { userPassword = Just h }
 --
 -- In the YesodAuth instance declaration for your app, include 'authHashDB'
 -- like so:
 --
+-- > import Yesod.Auth.HashDB (authHashDB)
+-- > ....
 -- > instance YesodAuth App where
 -- >     ....
 -- >     authPlugins _ = [ authHashDB (Just . UniqueUser), .... ]
--- >     getAuthId = getAuthIdHashDB AuthR (Just . UniqueUser)
+-- >     getAuthId creds = ... -- Perhaps modify scaffolding: see below
 --
--- @AuthR@ should be your authentication route, and the function
--- @(Just . UniqueUser)@ supplied to both 'authHashDB' and
--- 'getAuthIdHashDB' takes a 'Text' and produces a 'Unique' value to
--- look up in the User table.  'getAuthIdHashDB' is just a convenience
--- for the case when 'HashDB' is the only plugin, and something else
--- would be needed when other plugins are used as well.
+-- The argument to 'authHashDB' is a function which takes a 'Text' and
+-- produces a 'Maybe' containing a 'Unique' value to look up in the User
+-- table.  The example @(Just . UniqueUser)@ shown here works for the
+-- model outlined above.
 --
--- You can create password hashes manually as follows, if you need to
--- initialise the database:
+-- In the scaffolding, the definition of @getAuthId@ contains code to
+-- add a user who is not already in the database.  Depending on how users
+-- are administered, this may not make sense when using HashDB, so consider
+-- whether it should be removed.
 --
+-- For a real application, the developer should provide some sort of
+-- of administrative interface for setting passwords; it needs to call
+-- 'setPassword' and save the result in the database.  However, if you
+-- need to initialise the database by hand, you can generate the correct
+-- password hash as follows:
+--
 -- > ghci -XOverloadedStrings
--- > > import Crypto.PasswordStore
--- > > makePassword "MyPassword" 14
+-- > > import Yesod.Auth.Util.PasswordStore
+-- > > makePassword "MyPassword" 17
 --
--- where \"14\" is the default strength parameter used in this module.
+-- where \"17\" is the default strength parameter ('defaultStrength') used
+-- in this module.
 --
+-- == Custom Login Form
+--
+-- Instead of using the built-in HTML form, a custom one can be supplied
+-- by using 'authHashDBWithForm' instead of 'authHashDB'.
+--
+-- The custom form needs to be given as a function returning a Widget, since
+-- it has to build in the supplied "action" URL, and it must provide two text
+-- fields called "username" and "password".  For example, the following
+-- modification of the outline code given above would replace the default
+-- form with a very minimal one which has no labels and a simple layout.
+--
+-- > instance YesodAuth App where
+-- >     ....
+-- >     authPlugins _ = [ authHashDBWithForm myform (Just . UniqueUser), .... ]
+-- >
+-- > myform :: Route App -> Widget
+-- > myform action = $(whamletFile "templates/loginform.hamlet")
+--
+-- where templates/loginform.hamlet contains
+--
+-- > <form method="post" action="@{action}">
+-- >     <input name="username">
+-- >     <input type="password" name="password">
+-- >     <input type="submit" value="Login">
+--
+-- If a CSRF token needs to be embedded in a custom form, code must be
+-- included in the widget to add it - see @defaultForm@ in the source
+-- code of this module for an example.
+--
+-- == JSON Interface
+--
+-- This plugin provides sufficient tools to build a complete JSON-based
+-- authentication flow.  We assume that a design goal is to avoid URLs
+-- being built into the client, so all of the URLs needed are passed in
+-- JSON data.
+--
+-- To start the process, Yesod's defaultErrorHandler produces a JSON
+-- response if the HTTP Accept header gives \"application/json\"
+-- precedence over HTML.  For a NotAuthenticated error, the status is
+-- 401 and the response contains the URL to use for authentication: this
+-- is the route which will be handled by the loginHandler method of the
+-- YesodAuth instance, which normally returns a login form.
+--
+-- Leaving the loginHandler aside for a moment, the final step - supported
+-- by this plugin since version 1.6 - is to POST the credentials for
+-- authentication in a JSON object.  This object must include the
+-- properties "username" and "password".  In the HTML case this would be
+-- the form submission, but here we want to use JSON instead.
+--
+-- In a JSON interface, the purpose of the loginHandler is to tell the
+-- client the URL for submitting the credentials.  This requires a
+-- custom loginHandler, since the default one generates HTML only.
+-- It can find the correct URL by using the 'submitRouteHashDB'
+-- function defined in this module.
+--
+-- Writing the loginHandler is made a little messy by the fact that its
+-- type allows only HTML content.  A work-around is to send JSON as a
+-- short-circuit response, but we still make the choice using selectRep
+-- so as to get its matching of content types.  Here is an example which
+-- is geared around using HashDB on its own, supporting both JSON and HTML
+-- clients:
+--
+-- > instance YesodAuth App where
+-- >    ....
+-- >    loginHandler = do
+-- >         submission <- submitRouteHashDB
+-- >         render <- lift getUrlRender
+-- >         typedContent@(TypedContent ct _) <- selectRep $ do
+-- >             provideRepType typeHtml $ return emptyContent
+-- >                            -- Dummy: the real Html version is at the end
+-- >             provideJson $ object [("loginUrl", toJSON $ render submission)]
+-- >         when (ct == typeJson) $
+-- >             sendResponse typedContent   -- Short-circuit JSON response
+-- >         defaultLoginHandler             -- Html response
+--
 -------------------------------------------------------------------------------
 module Yesod.Auth.HashDB
     ( HashDBUser(..)
-    , Unique (..)
     , defaultStrength
     , setPasswordStrength
     , setPassword
+    , validatePass
     , upgradePasswordHash
-      -- * Authentication
+      -- * Interface to database and Yesod.Auth
     , validateUser
     , authHashDB
-    , getAuthIdHashDB
-      -- * Predefined data type
-    , User
-    , UserGeneric (..)
-    , UserId
-    , EntityField (..)
-    , migrateUsers
+    , authHashDBWithForm
+    , submitRouteHashDB
     ) where
 
-import Yesod.Persist
-import Yesod.Form
-import Yesod.Auth
-import Yesod.Core
 
-import Control.Applicative         ((<$>), (<*>))
-import Data.Typeable
-
+import           Yesod.Auth.Util.PasswordStore (makePassword, strengthenPassword,
+                                              verifyPassword, passwordStrength)
+import           Data.Aeson                  ((.:?))
 import qualified Data.ByteString.Char8 as BS (pack, unpack)
-import qualified Crypto.Hash as CH (SHA1, Digest, hash)
-import Data.Text                   (Text, pack, unpack, append)
-import Data.Maybe                  (fromMaybe)
-import Crypto.PasswordStore        (makePassword, verifyPassword,
-                                    passwordStrength, strengthenPassword)
+import           Data.Maybe                  (fromMaybe)
+import           Data.Text                   (Text, pack, unpack)
+import           Yesod.Auth
+import qualified Yesod.Auth.Message    as Msg
+import           Yesod.Core
+import           Yesod.Form
+import           Yesod.Persist
 
--- | Default strength used for passwords (see "Crypto.PasswordStore" for
---   details).
-defaultStrength :: Int
-defaultStrength = 14
+#if !MIN_VERSION_yesod_core(1,6,0)
+type HandlerFor site a = HandlerT site IO a
+type WidgetFor site a  = WidgetT site IO ()
+#define liftHandler lift
+#endif
 
--- | Interface for data type which holds user info. It's just a
---   collection of getters and setters
-class HashDBUser user where
-  -- | Retrieve password hash from user data
-  userPasswordHash :: user -> Maybe Text
-  -- | Retrieve salt for password from user data.  This is needed only for
-  --   compatibility with old database entries, which contain the salt
-  --   as a separate field.  New implementations do not require a separate
-  --   salt field in the user data, and should leave this as the default.
-  userPasswordSalt :: user -> Maybe Text
-  userPasswordSalt _ = Just ""
+#if !MIN_VERSION_yesod_core(1,6,11)
+#define requireInsecureJsonBody requireJsonBody
+#endif
 
-  -- | Callback for 'setPassword' and 'upgradePasswordHash'.  Produces a
-  --   version of the user data with the hash set to the new value.
-  --
-  --   This is the method which you should define for new applications, which
-  --   do not require compatibility with databases containing hashes written
-  --   by previous versions of this module.  If you do need compatibility,
-  --   define 'setSaltAndPasswordHash' instead.
-  setPasswordHash :: Text   -- ^ Password hash
-                     -> user -> user
-  setPasswordHash = setSaltAndPasswordHash ""
+-- | Default strength used for passwords (see "Yesod.Auth.Util.PasswordStore"
+--   for details).
+defaultStrength :: Int
+defaultStrength = 17
 
-  setUserHashAndSalt :: Text    -- ^ Salt
-                     -> Text    -- ^ Password hash
-                     -> user -> user
-  setUserHashAndSalt =
-      error "Define setSaltAndPasswordHash to get old-database compatibility"
+-- | The type representing user information stored in the database should
+--   be an instance of this class.  It just provides the getter and setter
+--   used by the functions in this module.
+class HashDBUser user where
+    -- | Getter used by 'validatePass' and 'upgradePasswordHash' to
+    --   retrieve the password hash from user data
+    --
+    userPasswordHash :: user -> Maybe Text
 
-  -- | Callback used in 'upgradePasswordHash' when compatibility is needed
-  --   with old-style hashes (including ones already upgraded using
-  --   'upgradePasswordHash').  This is not required for new applications,
-  --   which do not have a separate salt field in user data: please define
-  --   'setPasswordHash' instead.
-  --
-  --   The default implementation produces a runtime error, and will only be
-  --   called if a non-empty salt value needs to be set for compatibility
-  --   with an old database.
-  setSaltAndPasswordHash :: Text    -- ^ Salt
-                     -> Text    -- ^ Password hash
-                     -> user -> user
-  setSaltAndPasswordHash = setUserHashAndSalt
+    -- | Setter used by 'setPassword' and 'upgradePasswordHash'.  Produces a
+    --   version of the user data with the hash set to the new value.
+    --
+    setPasswordHash :: Text   -- ^ Password hash
+                       -> user -> user
 
-  {-# MINIMAL userPasswordHash, (setPasswordHash | (userPasswordSalt, setSaltAndPasswordHash)) #-}
-{-# DEPRECATED setUserHashAndSalt "Please use setSaltAndPasswordHash instead" #-}
+    {-# MINIMAL userPasswordHash, setPasswordHash #-}
 
 
--- | Calculate salted hash using SHA1.  Retained for compatibility with
---   hashes in existing databases, but will not be used for new passwords.
-saltedHash :: Text              -- ^ Salt
-           -> Text              -- ^ Password
-           -> Text
-saltedHash salt pw =
-  pack $ show (CH.hash $ BS.pack $ unpack $ append salt pw :: CH.Digest CH.SHA1)
-
--- | Calculate a new-style password hash using "Crypto.PasswordStore".
+-- | Calculate a new-style password hash using "Yesod.Auth.Util.PasswordStore".
 passwordHash :: MonadIO m => Int -> Text -> m Text
 passwordHash strength pwd = do
     h <- liftIO $ makePassword (BS.pack $ unpack pwd) strength
@@ -202,6 +224,9 @@
 --   hashed password.  Unlike previous versions of this module, no separate
 --   salt field is required for new passwords (but it may still be required
 --   for compatibility while old password hashes remain in the database).
+--
+--   This function does not change the database; the calling application
+--   is responsible for saving the data which is returned.
 setPasswordStrength :: (MonadIO m, HashDBUser user) => Int -> Text -> user -> m user
 setPasswordStrength strength pwd u = do
     hashed <- passwordHash strength pwd
@@ -211,81 +236,104 @@
 setPassword :: (MonadIO m, HashDBUser user) => Text -> user -> m user
 setPassword = setPasswordStrength defaultStrength
 
+-- | Validate a plaintext password against the hash in the user data structure.
+--
+--   The result distinguishes two types of validation failure, which may
+--   be useful in an application which supports multiple authentication
+--   methods:
+--
+--   * Just False - the user has a password set up, but the given one does
+--     not match it
+--
+--   * Nothing - the user does not have a password ('userPasswordHash' returns
+--     Nothing)
+--
+--   Since 1.4.1
+--
+validatePass :: HashDBUser u => u -> Text -> Maybe Bool
+validatePass user passwd = do
+    hash <- userPasswordHash user
+    -- NB plaintext password characters are truncated to 8 bits here,
+    -- and also in passwordHash above (the hash is already 8 bit).
+    -- This is for historical compatibility, but in practice it is
+    -- unlikely to reduce the entropy of most users' alphabets by much.
+    let hash' = BS.pack $ unpack hash
+        passwd' = BS.pack $ unpack passwd
+    if passwordStrength hash' > 0
+        -- Will give >0 for valid hash format, else treat as if wrong password
+        then return $ verifyPassword passwd' hash'
+        else return False
+
 -- | Upgrade existing user credentials to a stronger hash.  The existing
---   hash may have been produced either by previous versions of this module,
---   which used a weak algorithm, or from a weaker setting in the current
+--   hash will have been produced from a weaker setting in the current
 --   algorithm.  Use this function to produce an updated user record to
 --   store in the database.
 --
---   To allow transitional use, starting from hashes produced by older
---   versions of this module, and upgrading them to the new format,
---   we have to use the hash alone, without knowledge of the user's
---   plaintext password.  In this case, we apply the new algorithm to the
---   old hash, resulting in both hash functions, old and new, being used
---   one on top of the other; this situation is recognised by the hash
---   having the new format while the separate salt field is non-empty.
+--   As of version 1.5 this function cannot be used to upgrade a hash
+--   which has a non-empty separate salt field.  Such entries would have
+--   been produced originally by versions of this module prior to 1.3,
+--   but may have been upgraded using earlier versions of this function.
 --
 --   Returns Nothing if the user has no password (ie if 'userPasswordHash' u
---   is 'Nothing' and/or 'userPasswordSalt' u is 'Nothing').
+--   is 'Nothing') or if the password hash is not in the correct format.
+--
 upgradePasswordHash :: (MonadIO m, HashDBUser user) => Int -> user -> m (Maybe user)
 upgradePasswordHash strength u = do
-    let old = do h <- userPasswordHash u
-                 s <- userPasswordSalt u
-                 return (h, s)
+    let old = userPasswordHash u
     case old of
-        Just (oldHash, oldSalt) -> do
+        Just oldHash -> do
             let oldHash' = BS.pack $ unpack oldHash
             if passwordStrength oldHash' > 0
               then
-                -- Already a new-style hash, so only strengthen it as needed
+                -- Valid hash format, so strengthen it as needed
                 let newHash = pack $ BS.unpack $ strengthenPassword oldHash' strength
-                in if oldSalt == ""
-                   then return $ Just $ setPasswordHash newHash u
-                   else return $ Just $ setSaltAndPasswordHash oldSalt newHash u
+                in return $ Just $ setPasswordHash newHash u
               else do
-                -- Old-style hash: do extra layer of hash with the new algorithm
-                newHash <- passwordHash strength oldHash
-                return $ Just $ setSaltAndPasswordHash oldSalt newHash u
+                -- Invalid hash format (perhaps from old version of this module)
+                return Nothing
         Nothing -> return Nothing
 
 
 ----------------------------------------------------------------
--- Authentication
+-- Interface to database and Yesod.Auth
 ----------------------------------------------------------------
 
+-- | Constraint for types of interface functions in this module
+--
+type HashDBPersist master user =
+    ( YesodAuthPersist master
+    , PersistUnique (YesodPersistBackend master)
+    , AuthEntity master ~ user
+#if MIN_VERSION_persistent(2,5,0)
+    , PersistEntityBackend user ~ BaseBackend (YesodPersistBackend master)
+#else
+    , PersistEntityBackend user ~ YesodPersistBackend master
+#endif
+    , HashDBUser user
+    , PersistEntity user
+    )
+
+-- Internal data type for receiving JSON encoded username and password
+data UserPass = UserPass (Maybe Text) (Maybe Text)
+
+instance FromJSON UserPass where
+    parseJSON (Object v) = UserPass <$>
+                           v .:? "username" <*>
+                           v .:? "password"
+    parseJSON _          = pure $ UserPass Nothing Nothing
+
 -- | Given a user ID and password in plaintext, validate them against
---   the database values.  This function retains compatibility with
---   databases containing hashes produced by previous versions of this
---   module, although they are less secure and should be upgraded as
---   soon as possible.  They can be upgraded using 'upgradePasswordHash',
---   or by insisting that users set new passwords.
-validateUser :: ( YesodPersist yesod
-                , b ~ YesodPersistBackend yesod
-                , PersistMonadBackend (b (HandlerT yesod IO)) ~ PersistEntityBackend user
-                , PersistUnique (b (HandlerT yesod IO))
-                , PersistEntity user
-                , HashDBUser    user
-                ) => 
+--   the database values.  This function simply looks up the user id in the
+--   database and calls 'validatePass' to do the work.
+--
+validateUser :: HashDBPersist site user =>
                 Unique user     -- ^ User unique identifier
-             -> Text            -- ^ Password in plaint-text
-             -> HandlerT yesod IO Bool
+             -> Text            -- ^ Password in plaintext
+             -> HandlerFor site Bool
 validateUser userID passwd = do
-  -- Checks that hash and password match
-  let validate u = do hash <- userPasswordHash u
-                      salt <- userPasswordSalt u
-                      let hash' = BS.pack $ unpack hash
-                          passwd' = BS.pack $ unpack
-                              $ if salt == "" then passwd
-                                else
-                                    -- Extra layer for an upgraded old hash
-                                    saltedHash salt passwd
-                      if passwordStrength hash' > 0
-                        -- Will give >0 for new-style hash, else fall back
-                        then return $ verifyPassword passwd' hash'
-                        else return $ hash == saltedHash salt passwd
-  -- Get user data
-  user <- runDB $ getBy userID
-  return $ fromMaybe False $ validate . entityVal =<< user
+    -- Get user data
+    user <- runDB $ getBy userID
+    return $ fromMaybe False $ flip validatePass passwd . entityVal =<< user
 
 
 login :: AuthRoute
@@ -294,116 +342,104 @@
 
 -- | Handle the login form. First parameter is function which maps
 --   username (whatever it might be) to unique user ID.
-postLoginR :: ( YesodAuth y, YesodPersist y
-              , HashDBUser user, PersistEntity user
-              , b ~ YesodPersistBackend y
-              , PersistMonadBackend (b (HandlerT y IO)) ~ PersistEntityBackend user
-              , PersistUnique (b (HandlerT y IO))
-              )
-           => (Text -> Maybe (Unique user))
-           -> HandlerT Auth (HandlerT y IO) TypedContent
+--
+--   Since version 1.6, the data may be submitted as a JSON object.
+--   See the \"JSON Interface\" section above for more details.
+postLoginR :: HashDBPersist site user =>
+              (Text -> Maybe (Unique user))
+           -> AuthHandler site TypedContent
 postLoginR uniq = do
-    (mu,mp) <- lift $ runInputPost $ (,)
-        <$> iopt textField "username"
-        <*> iopt textField "password"
+    ct <- lookupHeader "Content-Type"
+    let jsonContent = ((== "application/json") . simpleContentType) <$> ct
+    UserPass mu mp <-
+        case jsonContent of
+          Just True -> requireInsecureJsonBody  -- We already know content type!
+          _         -> liftHandler $ runInputPost $ UserPass
+                       <$> iopt textField "username"
+                       <*> iopt textField "password"
 
-    isValid <- lift $ fromMaybe (return False) 
+    isValid <- liftHandler $ fromMaybe (return False) 
                  (validateUser <$> (uniq =<< mu) <*> mp)
     if isValid 
-       then lift $ setCredsRedirect $ Creds "hashdb" (fromMaybe "" mu) []
-       else do
-           tm <- getRouteToParent
-           lift $ loginErrorMessage (tm LoginR) "Invalid username/password"
+        then liftHandler $ setCredsRedirect $ Creds "hashdb" (fromMaybe "" mu) []
+        else loginErrorMessageI LoginR Msg.InvalidUsernamePass
 
 
--- | A drop in for the getAuthId method of your YesodAuth instance which
---   can be used if authHashDB is the only plugin in use.
-getAuthIdHashDB :: ( YesodAuth master, YesodPersist master
-                   , HashDBUser user, PersistEntity user
-                   , Key user ~ AuthId master
-                   , b ~ YesodPersistBackend master
-                   , PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend user
-                   , PersistUnique (b (HandlerT master IO))
-                   )
-                => (AuthRoute -> Route master)   -- ^ your site's Auth Route
-                -> (Text -> Maybe (Unique user)) -- ^ gets user ID
-                -> Creds master                  -- ^ the creds argument
-                -> HandlerT master IO (Maybe (AuthId master))
-getAuthIdHashDB authR uniq creds = do
-    muid <- maybeAuthId
-    case muid of
-        -- user already authenticated
-        Just uid -> return $ Just uid
-        Nothing       -> do
-            x <- case uniq (credsIdent creds) of
-                   Nothing -> return Nothing
-                   Just u  -> runDB (getBy u)
-            case x of
-                -- user exists
-                Just (Entity uid _) -> return $ Just uid
-                Nothing       -> do
-                                     _ <- loginErrorMessage (authR LoginR) "User not found"
-                                     return Nothing
-
 -- | Prompt for username and password, validate that against a database
 --   which holds the username and a hash of the password
-authHashDB :: ( YesodAuth m, YesodPersist m
-              , HashDBUser user
-              , PersistEntity user
-              , b ~ YesodPersistBackend m
-              , PersistMonadBackend (b (HandlerT m IO)) ~ PersistEntityBackend user
-              , PersistUnique (b (HandlerT m IO)))
-           => (Text -> Maybe (Unique user)) -> AuthPlugin m
-authHashDB uniq = AuthPlugin "hashdb" dispatch $ \tm -> toWidget [hamlet|
-$newline never
-    <div id="header">
-        <h1>Login
+authHashDB :: HashDBPersist site user =>
+              (Text -> Maybe (Unique user)) -> AuthPlugin site
+authHashDB = authHashDBWithForm defaultForm
 
-    <div id="login">
-        <form method="post" action="@{tm login}">
-            <table>
-                <tr>
-                    <th>Username:
-                    <td>
-                        <input id="x" name="username" autofocus="" required>
-                <tr>
-                    <th>Password:
-                    <td>
-                        <input type="password" name="password" required>
-                <tr>
-                    <td>&nbsp;
-                    <td>
-                        <input type="submit" value="Login">
 
-            <script>
-                if (!("autofocus" in document.createElement("input"))) {
-                    document.getElementById("x").focus();
-                }
-
-|]
+-- | Like 'authHashDB', but with an extra parameter to supply a custom HTML
+-- form.
+--
+-- The custom form should be specified as a function which takes a route to
+-- use as the form action, and returns a Widget containing the form.  The
+-- form must use the supplied route as its action URL, and, when submitted,
+-- it must send two text fields called "username" and "password".
+--
+-- Please see the example in the documentation at the head of this module.
+--
+-- Since 1.3.2
+--
+authHashDBWithForm :: forall site user.
+                      HashDBPersist site user =>
+                      (Route site -> WidgetFor site ())
+                   -> (Text -> Maybe (Unique user))
+                   -> AuthPlugin site
+authHashDBWithForm form uniq =
+    AuthPlugin "hashdb" dispatch $ \tm -> form (tm login)
     where
+        dispatch :: Text -> [Text] -> AuthHandler site TypedContent
         dispatch "POST" ["login"] = postLoginR uniq >>= sendResponse
         dispatch _ _              = notFound
 
 
-----------------------------------------------------------------
--- Predefined datatype
-----------------------------------------------------------------
+defaultForm :: Yesod app => Route app -> WidgetFor app ()
+defaultForm loginRoute = do
+    request <- getRequest
+    let mtok = reqToken request
+    toWidget [hamlet|
+      $newline never
+      <div id="header">
+        <h1>Login
 
--- | Generate data base instances for a valid user
-share [mkPersist sqlSettings, mkMigrate "migrateUsers"]
-         [persistUpperCase|
-User
-    username Text Eq
-    password Text
-    salt     Text
-    UniqueUser username
-    deriving Typeable
-|]
+      <div id="login">
+        <form method="post" action="@{loginRoute}">
+          $maybe tok <- mtok
+            <input type=hidden name=#{defaultCsrfParamName} value=#{tok}>
+          <table>
+            <tr>
+              <th>Username:
+              <td>
+                <input id="x" name="username" autofocus="" required>
+            <tr>
+              <th>Password:
+              <td>
+                <input type="password" name="password" required>
+            <tr>
+              <td>&nbsp;
+              <td>
+                <input type="submit" value="Login">
 
-instance HashDBUser (UserGeneric backend) where
-  userPasswordHash = Just . userPassword
-  userPasswordSalt = Just . userSalt
-  setSaltAndPasswordHash s h u = u { userSalt     = s
-                               , userPassword = h
-                               }
+          <script>
+            if (!("autofocus" in document.createElement("input"))) {
+                document.getElementById("x").focus();
+            }
+
+    |]
+
+
+-- | The route, in the parent site, to which the username and password
+--   should be sent in order to log in.  This function is particularly
+--   useful in constructing a 'loginHandler' function which provides a
+--   JSON response.  See the \"JSON Interface\" section above for more
+--   details.
+--
+--   Since 1.6
+submitRouteHashDB :: YesodAuth site => AuthHandler site (Route site)
+submitRouteHashDB = do
+    toParent <- getRouteToParent
+    return $ toParent login
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,4 @@
+resolver: lts-17.9
+packages:
+- .
+extra-deps: []
diff --git a/test/ExampleData.hs b/test/ExampleData.hs
new file mode 100644
--- /dev/null
+++ b/test/ExampleData.hs
@@ -0,0 +1,92 @@
+{-# LANGUAGE OverloadedStrings #-}
+module ExampleData (
+    OldStyleUser (..),
+    NewStyleUser (..),
+    mypassword,
+    changedpw,
+    equivpassword,
+    equivchangedpw,
+    oldStyleValidUser,
+    oldStyleBadUser1,
+    oldStyleBadUser2,
+    oldStyleUpgradedUser,
+    newStyleValidUser,
+    newStyleBadUser,
+    stronger
+) where
+
+import Yesod.Auth.HashDB (HashDBUser(..), defaultStrength)
+import Data.Text (Text)
+
+-- OldStyleUser is retained in the tests so that naive (incorrect)
+-- upgrading can be tested; the naive upgrade consists of removing
+-- the `userPasswordSalt` method without setting new passwords which
+-- have empty salt.
+data OldStyleUser = OldStyleUser {
+                        oldStyleName :: Text,
+                        oldStylePass :: Maybe Text,
+                        oldStyleSalt :: Maybe Text
+                    } deriving (Eq, Show)
+
+instance HashDBUser OldStyleUser where
+    userPasswordHash = oldStylePass
+    setPasswordHash h u = u { oldStyleSalt = Just "",
+                              oldStylePass = Just h
+                            }
+
+data NewStyleUser = NewStyleUser {
+                        newStyleName :: Text,
+                        newStylePass :: Maybe Text
+                    } deriving (Eq, Show)
+
+instance HashDBUser NewStyleUser where
+    userPasswordHash = newStylePass
+    setPasswordHash h u = u { newStylePass = Just h }
+
+mypassword :: Text
+mypassword = "mypassword"
+changedpw :: Text
+changedpw = "changedpw"
+
+-- These are equivalent to the above if each character is truncated to 8 bits
+equivpassword :: Text
+equivpassword = "\x46d\x479\x470\x461\x473\x473\x477\x46f\x472\x464"
+equivchangedpw :: Text
+equivchangedpw = "\xbc63\xbc68\xbc61\xbc6e\xbc67\xbc65\xbc64\xbc70\xbc77"
+
+oldStyleValidUser :: OldStyleUser
+oldStyleValidUser =
+    OldStyleUser "foo"
+                 (Just "8e3e33029e71b4e25ba95a00a88c4bfeb93d766a")
+                 (Just "somesalt")
+
+oldStyleBadUser1 :: OldStyleUser
+oldStyleBadUser1 =
+    OldStyleUser "bar"
+                 Nothing
+                 (Just "pepper")
+
+oldStyleBadUser2 :: OldStyleUser
+oldStyleBadUser2 =
+    OldStyleUser "baz"
+                 (Just "8e3e33029e71b4e25ba95a00a88c4bfeb93d766a")
+                 Nothing
+
+oldStyleUpgradedUser :: OldStyleUser
+oldStyleUpgradedUser =
+    OldStyleUser "foo"
+                 (Just "sha256|17|GkImOI0oV9RyOE3oJpYKRg==|KPPYL9JaP6UQjwLVvRsK3Pw2tl1LWyjqlh11jjKRQVM=")
+                 (Just "somesalt")
+
+newStyleValidUser :: NewStyleUser
+newStyleValidUser =
+    NewStyleUser "fox"
+                 (Just "sha256|14|2hL7cNopkA/dGy/5CQTuSg==|CUTPW6ICMISSjohFep851f9PdqIn7Y4B75/I77BvEYM=")
+
+newStyleBadUser :: NewStyleUser
+newStyleBadUser =
+    NewStyleUser "bad"
+                 Nothing
+
+stronger :: Int
+stronger = defaultStrength + 2
diff --git a/test/IntegrationTest.hs b/test/IntegrationTest.hs
new file mode 100644
--- /dev/null
+++ b/test/IntegrationTest.hs
@@ -0,0 +1,131 @@
+{-# LANGUAGE NoImplicitPrelude          #-}
+{-# LANGUAGE OverloadedStrings          #-}
+
+module IntegrationTest (
+    withApp,
+    integrationSpec
+) where
+
+import BasicPrelude
+import Data.Aeson                   (FromJSON, parseJSON, (.:))
+import qualified Data.Aeson as JSON
+import Network.Wai.Test             (simpleBody)
+import Test.Hspec                   (Spec, SpecWith, before,
+                                     describe, it)
+import qualified Yesod.Test as YT
+
+import TestSite                     (App, Route(..))
+import TestTools
+
+type MyTestApp = YT.TestApp App
+withApp :: App -> SpecWith (YT.TestApp App) -> Spec
+withApp app = before $ return (app, id)
+
+authUrl :: Text
+authUrl = "http://localhost:3000/auth/login"
+
+data AuthUrl = AuthUrl Text deriving (Eq, Show)
+instance FromJSON AuthUrl where
+    parseJSON (JSON.Object v) = AuthUrl <$> v .: "authentication_url"
+    parseJSON _ = mempty
+
+loginUrl :: Text
+loginUrl = "http://localhost:3000/auth/page/hashdb/login"
+
+data LoginUrl = LoginUrl Text deriving (Eq, Show)
+instance FromJSON LoginUrl where
+    parseJSON (JSON.Object v) = LoginUrl <$> v .: "loginUrl"
+    parseJSON _ = mempty
+
+successMsg :: Text
+successMsg = "Login Successful"
+
+data SuccessMsg = SuccessMsg Text deriving (Eq, Show)
+instance FromJSON SuccessMsg where
+    parseJSON (JSON.Object v) = SuccessMsg <$> v .: "message"
+    parseJSON _ = mempty
+
+getBodyJSON :: FromJSON a => YT.YesodExample site (Maybe a)
+getBodyJSON = do
+    resp <- YT.getResponse
+    let body = simpleBody <$> resp
+        result = JSON.decode =<< body
+    return result
+
+integrationSpec :: SpecWith MyTestApp
+integrationSpec = do
+    describe "The home page" $ do
+      it "can be accessed" $ do
+        YT.get HomeR
+        YT.statusIs 200
+
+    describe "The protected page" $ do
+      it "requires login" $ do
+        needsLogin GET ("/prot" :: Text)
+      it "looks right after login by a valid user" $ do
+        _ <- doLogin "paul" "MyPassword"
+        YT.get ProtectedR
+        YT.statusIs 200
+        YT.bodyContains "OK, you are logged in so you are allowed to see this!"
+      it "can't be accessed after login then logout" $ do
+        _ <- doLogin "paul" "MyPassword"
+        YT.get $ AuthR LogoutR
+        -- That `get` will get the form from Yesod.Core.Handler.redirectToPost
+        -- which will not be submitted automatically without javascript
+        YT.bodyContains "please click on the button below to be redirected"
+        -- so we do the redirection ourselves:
+        YT.request $ do
+            YT.setMethod "POST"
+            YT.setUrl $ AuthR LogoutR
+            -- yesod-core-1.4.19 added the CSRF token to the redirectToPost form
+            YT.addToken
+        YT.get HomeR
+        YT.statusIs 200
+        YT.bodyContains "Your current auth ID: Nothing"
+        YT.get ProtectedR
+        YT.statusIs 303
+
+    describe "Login" $ do
+      it "fails when incorrect password given" $ do
+        loc <- doLoginPart1 "paul" "WrongPassword"
+        checkFailedLogin loc
+      it "fails when unknown user name given" $ do
+        loc <- doLoginPart1 "xyzzy" "WrongPassword"
+        checkFailedLogin loc
+
+    describe "JSON Login" $ do
+      it "JSON access to protected page gives JSON object with auth URL" $ do
+        YT.request $ do
+          YT.setMethod "GET"
+          YT.setUrl ProtectedR
+          YT.addRequestHeader ("Accept", "application/json")
+        YT.statusIs 401
+        auth <- getBodyJSON
+        YT.assertEq "Authentication URL" auth (Just $ AuthUrl authUrl)
+      it "Custom loginHandler using submitRouteHashDB has correct URL in JSON" $ do
+        YT.request $ do
+          YT.setMethod "GET"
+          YT.setUrl authUrl
+          YT.addRequestHeader ("Accept", "application/json")
+        YT.statusIs 200
+        login <- getBodyJSON
+        YT.assertEq "Login URL" login (Just $ LoginUrl loginUrl)
+      -- This example needs yesod-test >= 1.5.0.1, since older ones use wrong
+      -- content type for JSON (https://github.com/yesodweb/yesod/issues/1063).
+      it "Sending JSON username and password produces JSON success message" $ do
+        -- This first request is only to get the CSRF token cookie, used below
+        YT.request $ do
+          YT.setMethod "GET"
+          YT.setUrl authUrl
+          YT.addRequestHeader ("Accept", "application/json")
+        YT.request $ do
+          YT.setMethod "POST"
+          YT.setUrl loginUrl
+          YT.addRequestHeader ("Accept", "application/json")
+          YT.addRequestHeader ("Content-Type", "application/json; charset=utf-8")
+          YT.setRequestBody "{\"username\":\"paul\",\"password\":\"MyPassword\"}"
+          -- CSRF token is being checked, since yesod-core >= 1.4.14 is forced
+          YT.addTokenFromCookie
+        YT.statusIs 200
+        msg <- getBodyJSON
+        YT.assertEq "Login success" msg (Just $ SuccessMsg successMsg)
diff --git a/test/NonDBTests.hs b/test/NonDBTests.hs
new file mode 100644
--- /dev/null
+++ b/test/NonDBTests.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE OverloadedStrings #-}
+module NonDBTests (
+    nonDBTests
+) where
+
+import Test.Hspec
+import Yesod.Auth.HashDB
+import Data.Text (pack, isInfixOf)
+
+import ExampleData
+
+-- For consistent layout of "it" items, we sometimes have a redundant "do":
+{-# ANN nonDBTests ("HLint: ignore Redundant do"::String) #-}
+
+nonDBTests :: Spec
+nonDBTests = do
+
+    describe "validatePass" $ do
+
+      context "Naively upgraded old-style valid user" $ do
+        it "no longer checks good password (wrong format, so Just False)" $
+            validatePass oldStyleValidUser mypassword `shouldBe` Just False
+        it "no longer checks bad password (wrong format, so Just False)" $
+            validatePass oldStyleValidUser changedpw `shouldBe` Just False
+
+      context "Naively upgraded old-style user with no password hash" $ do
+        it "fails validation giving Nothing" $
+            validatePass oldStyleBadUser1 mypassword `shouldBe` Nothing
+
+      context "Naively upgraded old-style user with no salt" $ do
+        it "fails validation (wrong format, so Just False)" $
+            validatePass oldStyleBadUser2 mypassword `shouldBe` Just False
+
+      context "Previously upgraded old-style user" $ do
+        it "no longer verifies with correct password as it still needs salt" $
+            validatePass oldStyleUpgradedUser mypassword `shouldBe` Just False
+        it "fails validation with a wrong password giving Just False" $
+            validatePass oldStyleUpgradedUser changedpw `shouldBe` Just False
+
+      context "New style valid user" $ do
+        it "verifies OK with correct password" $
+            validatePass newStyleValidUser mypassword `shouldBe` Just True
+        it "fails validation with a wrong password giving Just False" $
+            validatePass newStyleValidUser changedpw `shouldBe` Just False
+
+      context "New-style user with no password hash" $ do
+        it "fails validation giving Nothing" $
+            validatePass newStyleBadUser mypassword `shouldBe` Nothing
+
+
+    describe "upgradePasswordHash" $ do
+
+      context "Upgrade of naively upgraded old-style password hash" $ do
+        it "is Nothing if the user has a password (format is wrong)" $ do
+            newuser <- upgradePasswordHash defaultStrength oldStyleValidUser
+            newuser `shouldBe` Nothing
+        it "is Nothing if there is no password hash" $ do
+            newuser <- upgradePasswordHash defaultStrength oldStyleBadUser1
+            newuser `shouldBe` Nothing
+
+      context "Upgrade of previously upgraded old-style user" $ do
+        it "silently succeeds producing a password which won't verify as it still needs salt" $ do
+            newuser <- upgradePasswordHash defaultStrength oldStyleUpgradedUser
+            case newuser of
+              Nothing -> expectationFailure "should not produce Nothing"
+              Just u  -> validatePass u mypassword `shouldBe` Just False
+
+      context "Upgrade of new-style password hash to stronger setting" $ do
+        it "really does have a hash containing the new strength" $ do
+            newuser <- upgradePasswordHash stronger newStyleValidUser
+            let s = pack $ "|" ++ show stronger ++ "|"
+                found = fmap (s `isInfixOf`) $ newuser >>= newStylePass
+            found `shouldBe` Just True
+        it "still verifies with the same password" $ do
+            newuser <- upgradePasswordHash stronger newStyleValidUser
+            let valid = newuser >>= flip validatePass mypassword
+            valid `shouldBe` Just True
+        it "is Nothing if there is no password hash" $ do
+            newuser <- upgradePasswordHash stronger newStyleBadUser
+            newuser `shouldBe` Nothing
+
+
+    describe "setPassword" $ do
+        it "produces hash which verifies OK starting from old-style user" $ do
+            newuser <- setPassword changedpw oldStyleValidUser
+            validatePass newuser changedpw `shouldBe` Just True
+        it "produces empty salt (in the case of old-style user)" $ do
+            newuser <- setPassword changedpw oldStyleValidUser
+            oldStyleSalt newuser `shouldBe` Just ""
+        it "produces hash which verifies OK starting from new-style user" $ do
+            newuser <- setPassword changedpw newStyleValidUser
+            validatePass newuser changedpw `shouldBe` Just True
+
+
+    describe "Only the bottom 8 bits of password characters are used" $ do
+        it "when verifying new-style password hash" $
+            validatePass newStyleValidUser equivpassword `shouldBe` Just True
+        it "when setting a new password" $ do
+            newuser <- setPassword equivchangedpw oldStyleValidUser
+            validatePass newuser changedpw `shouldBe` Just True
diff --git a/test/TestSite.hs b/test/TestSite.hs
new file mode 100644
--- /dev/null
+++ b/test/TestSite.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE UndecidableInstances       #-}
+#if __GLASGOW_HASKELL__ >= 802
+-- Retain support for older compilers, assuming dependency versions which do too
+{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE StandaloneDeriving         #-}
+#endif
+
+module TestSite (
+    User(..),
+    migrateAll,
+    App(..),
+    Route(..),
+    Handler,
+    runDB
+) where
+
+import Control.Monad                (when)
+import Data.Text
+import Database.Persist.Sqlite
+import Network.HTTP.Client.Conduit  (Manager)
+import Yesod
+import Yesod.Auth
+import Yesod.Auth.HashDB            (HashDBUser(..), authHashDB,
+                                     submitRouteHashDB)
+import Yesod.Auth.Message           (AuthMessage (InvalidLogin))
+
+
+-- Trivial example site needing authentication
+--
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+User
+    name       Text
+    password   Text Maybe
+    UniqueUser name
+    deriving Show
+|]
+
+instance HashDBUser User where
+    userPasswordHash = userPassword
+    setPasswordHash h u = u { userPassword = Just h }
+
+data App = App
+    { appHttpManager  :: Manager
+    , appDBConnection :: SqlBackend
+    }
+
+#if !MIN_VERSION_yesod_core(1,6,0)
+#define liftHandler lift
+#define doRunDB runDB
+getAppHttpManager :: App -> Manager
+getAppHttpManager = appHttpManager
+#else
+#define doRunDB liftHandler $ runDB
+getAppHttpManager :: (MonadHandler m, HandlerSite m ~ App) => m Manager
+getAppHttpManager = appHttpManager <$> getYesod
+#endif
+
+mkYesod "App" [parseRoutes|
+/       HomeR GET
+/prot   ProtectedR GET
+/auth   AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    approot = ApprootStatic "http://localhost:3000"
+
+    authRoute _ = Just $ AuthR LoginR
+
+    isAuthorized ProtectedR _ = do
+        mu <- maybeAuthId
+        return $ case mu of
+            Nothing -> AuthenticationRequired
+            Just _  -> Authorized
+    -- Other pages (HomeR and AuthR _) do not require login
+    isAuthorized _ _ = return Authorized
+
+    -- CSRF middleware requires yesod-core-1.4.14, yesod-test >= 1.5 is
+    -- required so that tests can get at the token.
+    yesodMiddleware = defaultCsrfMiddleware . defaultYesodMiddleware
+
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB action = do
+        master <- getYesod
+        runSqlConn action $ appDBConnection master
+
+instance YesodAuth App where
+    type AuthId App = UserId
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authenticate creds = doRunDB $ do
+        x <- getBy $ UniqueUser $ credsIdent creds
+        case x of
+            Just (Entity uid _) -> return $ Authenticated uid
+            Nothing             -> return $ UserError InvalidLogin
+
+    authPlugins _ = [ authHashDB (Just . UniqueUser) ]
+
+    authHttpManager = getAppHttpManager
+
+    loginHandler = do
+        submission <- submitRouteHashDB
+        render <- liftHandler getUrlRender
+        typedContent@(TypedContent ct _) <- selectRep $ do
+            provideRepType typeHtml $ return emptyContent
+                           -- Dummy: the real Html version is at the end
+            provideJson $ object [("loginUrl", toJSON $ render submission)]
+        when (ct == typeJson) $
+            sendResponse typedContent   -- Short-circuit JSON response
+        defaultLoginHandler             -- Html response
+
+instance YesodAuthPersist App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    mauth <- maybeAuth
+    let mname = userName . entityVal <$> mauth
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show mname}
+            $maybe _ <- mname
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+            <p><a href=@{ProtectedR}>Go to protected page
+        |]
+
+-- This page requires a valid login
+getProtectedR :: Handler Html
+getProtectedR = defaultLayout
+        [whamlet|
+            <p>OK, you are logged in so you are allowed to see this!
+            <p><a href=@{HomeR}>Go to home page
+        |]
diff --git a/test/TestTools.hs b/test/TestTools.hs
new file mode 100644
--- /dev/null
+++ b/test/TestTools.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE NoImplicitPrelude          #-}
+module TestTools (
+    assertFailure,
+    urlPath,
+    needsLogin,
+    doLogin,
+    doLoginPart1,
+    doLoginPart2,
+    checkFailedLogin,
+    StdMethod(..)
+) where
+
+import TestSite           (App(..))
+import BasicPrelude
+import qualified Prelude  (show)
+import Data.Text          (pack, unpack)
+import Data.ByteString.Lazy (toStrict)
+import Yesod.Core         (RedirectUrl)
+import Yesod.Test
+import qualified Data.ByteString.Char8 as BC
+import Network.URI        (URI(uriPath), parseURI)
+import Network.HTTP.Types (StdMethod(..), renderStdMethod, Status(..))
+import Network.Wai.Test   (SResponse(..))
+
+-- Adjust as necessary to the url prefix in the Testing configuration
+testRoot :: ByteString
+testRoot = "http://localhost:3000"
+
+-- Adjust as necessary for the path part of the url for a page to force login
+forceLogin :: ByteString
+forceLogin = "/prot"
+
+-- Adjust as necessary for the expected path part of the URL after login
+afterLogin :: ByteString
+afterLogin = "/prot"
+
+-- Force failure by swearing that black is white, and pigs can fly...
+assertFailure :: String -> YesodExample App ()
+assertFailure msg = assertEq msg True False
+
+-- Convert an absolute URL (eg extracted from responses) to just the path
+-- for use in test requests.
+urlPath :: Text -> Text
+urlPath = pack . maybe "" uriPath . parseURI . unpack
+
+-- Internal use only - actual urls are ascii, so exact encoding is irrelevant
+urlPathB :: ByteString -> Text
+urlPathB = urlPath . decodeUtf8
+
+-- Stages in login process, used below
+firstRedirect :: RedirectUrl App url =>
+                 StdMethod -> url -> YesodExample App (Maybe ByteString)
+firstRedirect method url = do
+    request $ do
+        setMethod $ renderStdMethod method
+        setUrl url
+    extractLocation  -- We should get redirected to the login page
+
+assertLoginPage :: ByteString -> YesodExample App ()
+assertLoginPage loc = do
+    assertEq "correct login redirection location"
+                (testRoot ++ "/auth/login") loc
+    get $ urlPathB loc
+    statusIs 200
+    bodyContains "Login"
+
+submitLogin :: Text -> Text -> YesodExample App (Maybe ByteString)
+submitLogin user pass = do
+    -- Ideally we would extract this url from the login form on the current page
+    request $ do
+        setMethod "POST"
+        setUrl $ urlPathB $ testRoot ++ "/auth/page/hashdb/login"
+        addPostParam "username" user
+        addPostParam "password" pass
+        addToken
+    extractLocation  -- Successful login should redirect to the home page
+
+extractLocation :: YesodExample App (Maybe ByteString)
+extractLocation = do
+    withResponse ( \ SResponse { simpleStatus = s, simpleHeaders = h } -> do
+                        let code = statusCode s
+                        assertEq ("Expected a 302 or 303 redirection status "
+                                     ++ "but received " ++ Prelude.show code)
+                                    (code `elem` [302,303])
+                                    True
+                        return $ lookup "Location" h
+                 )
+
+-- Check that accessing the url with the given method requires login, and
+-- that it redirects us to what looks like the login page.  Note that this is
+-- *not* an ajax request, whatever the method, so the redirection *should*
+-- result in the HTML login page.
+--
+needsLogin :: RedirectUrl App url => StdMethod -> url -> YesodExample App ()
+needsLogin method url = do
+    mbloc <- firstRedirect method url
+    maybe (assertFailure "Should have location header") assertLoginPage mbloc
+
+-- Do a login (using hashdb auth).  This just attempts to go to the home
+-- url, and follows through the login process.  It should probably be the
+-- first thing in each "it" spec.
+--
+-- To allow testing of the login process itself, doLogin is split into two
+-- parts.
+--
+doLogin :: Text -> Text -> YesodExample App (Maybe ByteString)
+doLogin user pass = do
+    redir <- doLoginPart1 user pass
+    doLoginPart2 redir
+
+doLoginPart1 :: Text -> Text -> YesodExample App (Maybe ByteString)
+doLoginPart1 user pass = do
+    mbloc <- firstRedirect GET $ urlPathB $ testRoot ++ forceLogin
+    maybe (assertFailure "Should have location header") assertLoginPage mbloc
+    submitLogin user pass
+
+doLoginPart2 :: Maybe ByteString -> YesodExample App (Maybe ByteString)
+doLoginPart2 mbloc2 = do
+    maybe (assertFailure "Should have second location header")
+          (assertEq "Check after-login redirection" $ testRoot ++ afterLogin)
+          mbloc2
+    -- Now get the home page to obtain the sessAuth value
+    get ("/" :: Text)
+    statusIs 200
+    resp <- getResponse
+    let sessAuth = (fmap simpleBody resp) >>= findSessAuth
+    return sessAuth
+  where
+    findSessAuth body =
+        let stmt = snd $ BC.breakSubstring "var sessAuth =" $ toStrict body
+            parts = BC.split '"' stmt
+        in case parts of
+              (_:sa:_) -> Just sa
+              _        -> Nothing
+
+-- Use this instead of doLoginPart2 if the login is expected to fail
+--
+checkFailedLogin :: Maybe ByteString -> YesodExample App ()
+checkFailedLogin mbloc2 = do
+    maybe (assertFailure "Should have second location header")
+          assertLoginPage
+          mbloc2
+    bodyContains "Invalid username/password combination"
diff --git a/test/integration.hs b/test/integration.hs
new file mode 100644
--- /dev/null
+++ b/test/integration.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE OverloadedStrings          #-}
+
+import Control.Monad.Trans.Resource (runResourceT)
+import Control.Monad.Logger         (runStderrLoggingT)
+import Database.Persist.Sqlite
+import Network.HTTP.Client.Conduit  (newManager)
+import Test.Hspec                   (hspec)
+import Yesod
+import Yesod.Auth.HashDB            (setPassword)
+
+#ifndef STANDALONE
+import IntegrationTest
+#endif
+import TestSite
+
+main :: IO ()
+main = runStderrLoggingT $ withSqliteConn ":memory:" $ \conn -> liftIO $ do
+    runResourceT $ flip runSqlConn conn $ do
+        runMigration migrateAll
+        validUser <- setPassword "MyPassword" $ User "paul" Nothing
+        insert_ validUser
+    mgr <- newManager
+#ifdef STANDALONE
+    -- Run as a stand-alone server - see the cabal file in this directory
+    warp 3000 $ App mgr conn
+#else
+    -- Otherwise run the tests
+    hspec $ withApp (App mgr conn) integrationSpec
+#endif
diff --git a/test/main.hs b/test/main.hs
new file mode 100644
--- /dev/null
+++ b/test/main.hs
@@ -0,0 +1,7 @@
+{-# LANGUAGE OverloadedStrings #-}
+import Test.Hspec
+
+import NonDBTests
+
+main :: IO ()
+main = hspec nonDBTests
diff --git a/test/stack.yaml b/test/stack.yaml
new file mode 100644
--- /dev/null
+++ b/test/stack.yaml
@@ -0,0 +1,7 @@
+# Used for building standalone-testsite
+packages:
+- ..
+- .
+extra-deps: []
+resolver: lts-17.9
+compiler-check: newer-minor
diff --git a/test/standalone-testsite.cabal b/test/standalone-testsite.cabal
new file mode 100644
--- /dev/null
+++ b/test/standalone-testsite.cabal
@@ -0,0 +1,38 @@
+name:            standalone-testsite
+version:         0.1.2
+license:         MIT
+author:          Paul Rouse
+maintainer:      Paul Rouse <pyr@doynton.org>
+synopsis:        Stand-alone version of test for Yesod.Auth.HashDB
+category:        Web, Yesod
+stability:       Stable
+cabal-version:   >= 1.10
+build-type:      Simple
+homepage:        https://github.com/paul-rouse/yesod-auth-hashdb
+bug-reports:     https://github.com/paul-rouse/yesod-auth-hashdb/issues
+description:
+    Stand-alone integration test for Yesod.Auth.HashDB, run as a server.
+    .
+    Normally the integration test is run using Yesod.Test.  However, it
+    may be handy to build the example server as a stand-alone application
+    and debug it.  To do so, use this cabal file and the accompanying
+    stack.yaml.  STANDALONE is used in integration.hs to replace
+    the tests with code which uses warp to make a server.
+
+executable integration
+    main-is:         integration.hs
+    hs-source-dirs:  .
+    ghc-options:     -Wall
+    cpp-options:     -DSTANDALONE
+    other-modules:   TestSite
+    build-depends:   base >= 4 && < 5
+                   , hspec
+                   , http-conduit
+                   , monad-logger
+                   , persistent-sqlite
+                   , resourcet
+                   , text
+                   , yesod
+                   , yesod-auth
+                   , yesod-auth-hashdb
+                   , yesod-core
diff --git a/yesod-auth-hashdb.cabal b/yesod-auth-hashdb.cabal
--- a/yesod-auth-hashdb.cabal
+++ b/yesod-auth-hashdb.cabal
@@ -1,5 +1,5 @@
 name:            yesod-auth-hashdb
-version:         1.3.0.1
+version:         1.7.1.7
 license:         MIT
 license-file:    LICENSE
 author:          Patrick Brisbin, later changes Paul Rouse
@@ -7,24 +7,87 @@
 synopsis:        Authentication plugin for Yesod.
 category:        Web, Yesod
 stability:       Stable
-cabal-version:   >= 1.6.0
+cabal-version:   >= 1.10
 build-type:      Simple
-homepage:        http://www.yesodweb.com/
-description:     This package is the Yesod.Auth.HashDB plugin, originally included in yesod-auth, but now modified to be more secure and placed in a separate package.
+homepage:        https://github.com/paul-rouse/yesod-auth-hashdb
+bug-reports:     https://github.com/paul-rouse/yesod-auth-hashdb/issues
+description:
+    This package is the Yesod.Auth.HashDB plugin, originally included as part
+    of yesod-auth, but now modified to be more secure and placed in a separate
+    package.
+    .
+    It provides authentication using hashed passwords stored in a database,
+    and works best in situations where an administrator is involved in
+    setting up a user with an initial password.
+    .
+    The complete login process, including a default form, is implemented by
+    this plugin, but the application developer must design the interfaces
+    for setting up users and allowing them to change their own passwords,
+    since only the low-level password-setting functions are provided by this
+    package.  (Note that other authentication plugins may be more appropriate
+    if you wish to use email verification to set up accounts).
+extra-source-files:  ChangeLog.md
+                     stack.yaml
+                     test/stack.yaml
+                     test/standalone-testsite.cabal
 
 library
-    build-depends:   base                    >= 4          && < 5
-                   , bytestring              >= 0.9.1.4    && < 0.11
-                   , yesod-core              >= 1.2        && < 1.3
-                   , yesod-auth              >= 1.3        && < 1.4
-                   , text                    >= 0.7        && < 2.0
-                   , yesod-persistent        >= 1.2        && < 1.3
-                   , yesod-form              >= 1.3        && < 1.4
-                   , pwstore-fast            >= 2.2        && < 2.5
-                   , cryptohash              >= 0.8        && < 0.12
+    build-depends:   base                    >= 4.8        && < 5
+                   , bytestring              >= 0.9.1.4
+                   , yesod-core              >= 1.4.19     && < 1.7
+                   , yesod-auth              >= 1.4.18     && < 1.7
+                   , text                    >= 0.7
+                   , yesod-persistent        >= 1.2
+                   , persistent              >= 2.1
+                   , yesod-form              >= 1.4        && < 1.8
+                   , aeson
 
     exposed-modules: Yesod.Auth.HashDB
     ghc-options:     -Wall
+    default-language: Haskell2010
+
+test-suite test
+    type:            exitcode-stdio-1.0
+    main-is:         main.hs
+    hs-source-dirs:  test
+    ghc-options:     -Wall
+    default-language: Haskell2010
+    other-modules:   ExampleData
+                     NonDBTests
+    build-depends:   base               >= 4.8 && < 5
+                   , yesod-auth-hashdb
+                   , hspec
+                   , text
+
+test-suite integration
+    type:            exitcode-stdio-1.0
+    main-is:         integration.hs
+    hs-source-dirs:  test
+    ghc-options:     -Wall
+    default-language: Haskell2010
+    other-modules:   IntegrationTest
+                   , TestSite
+                   , TestTools
+    build-depends:   base               >= 4.8 && < 5
+                   , aeson
+                   , bytestring
+                   , basic-prelude
+                   , containers
+                   , hspec              >= 2.0.0
+                   , http-conduit
+                   , http-types
+                   , monad-logger
+                   , network-uri
+                   , persistent-sqlite  >= 2.1
+                   , resourcet
+                   , text
+                   , unordered-containers
+                   , wai-extra
+                   , yesod
+                   , yesod-auth         >= 1.4.18 && < 1.5 || >= 1.6.1 && < 1.7
+                   , yesod-auth-hashdb
+                   , yesod-core
+                   , yesod-test         >= 1.5.0.1
 
 source-repository head
   type:     git
