diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,20 @@
+## 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
diff --git a/Yesod/Auth/HashDB.hs b/Yesod/Auth/HashDB.hs
--- a/Yesod/Auth/HashDB.hs
+++ b/Yesod/Auth/HashDB.hs
@@ -17,9 +17,8 @@
 -- A Yesod authentication plugin designed to look users up in a Persistent
 -- database where the hash of their password is stored.
 --
--- __Release 1.5 is the first of two which remove compatibility with old__
--- __(pre 1.3) databases.  Some other deprecated functionality is removed__
--- __too.  Please see__
+-- __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, the foundation data type must be an
@@ -105,6 +104,52 @@
 -- 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(..)
@@ -117,18 +162,19 @@
     , validateUser
     , authHashDB
     , authHashDBWithForm
+    , submitRouteHashDB
     ) where
 
 
 #if __GLASGOW_HASKELL__ < 710
-import           Control.Applicative         ((<$>), (<*>))
+import           Control.Applicative         ((<$>), (<*>), pure)
 #endif
-import qualified Crypto.Hash           as CH (SHA1, Digest, hash)
 import           Crypto.PasswordStore        (makePassword, strengthenPassword,
                                               verifyPassword, passwordStrength)
+import           Data.Aeson                  ((.:?))
 import qualified Data.ByteString.Char8 as BS (pack, unpack)
 import           Data.Maybe                  (fromMaybe)
-import           Data.Text                   (Text, pack, unpack, append)
+import           Data.Text                   (Text, pack, unpack)
 import           Yesod.Auth
 import qualified Yesod.Auth.Message    as Msg
 import           Yesod.Core
@@ -146,36 +192,23 @@
 defaultStrength = 17
 
 -- | The type representing user information stored in the database should
---   be an instance of this class.  It just provides the getters and setters
+--   be an instance of this class.  It just provides the getter and setter
 --   used by the functions in this module.
 class HashDBUser user where
-    -- | Retrieve password hash from user data
+    -- | Getter used by 'validatePass' and 'upgradePasswordHash' to
+    --   retrieve the 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 ""
 
-    -- | Callback for 'setPassword' and 'upgradePasswordHash'.  Produces a
+    -- | 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 #-}
-{-# DEPRECATED userPasswordSalt "Verification against old data containing a separate salt field will be removed in version 1.6" #-}
 
 
--- | 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".
 passwordHash :: MonadIO m => Int -> Text -> m Text
 passwordHash strength pwd = do
@@ -200,11 +233,6 @@
 setPassword = setPasswordStrength defaultStrength
 
 -- | Validate a plaintext password against the hash in the user data structure.
---   This function retains compatibility with user data produced by old
---   versions of this module (prior to 1.3), although the hashes 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.
 --
 --   The result distinguishes two types of validation failure, which may
 --   be useful in an application which supports multiple authentication
@@ -213,27 +241,24 @@
 --   * 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 (the hash is Nothing)
+--   * 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
-    salt <- userPasswordSalt user
-    -- NB plaintext password characters are truncated to 8 bits here, and also
-    -- in saltedHash and passwordHash above (the hash and old salt are already
-    -- 8 bit).  This is for historical compatibility, but in practice it is
+    -- 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 $ if salt == "" then passwd
-                                     else
-                                         -- Extra layer for an upgraded old hash
-                                         saltedHash salt passwd
+        passwd' = BS.pack $ unpack passwd
     if passwordStrength hash' > 0
-        -- Will give >0 for new-style hash, else fall back
+        -- Will give >0 for valid hash format, else treat as if wrong password
         then return $ verifyPassword passwd' hash'
-        else return $ hash == saltedHash salt passwd
+        else return False
 
 -- | Upgrade existing user credentials to a stronger hash.  The existing
 --   hash will have been produced from a weaker setting in the current
@@ -246,25 +271,21 @@
 --   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'), if the password hash is not in the new format, or if the
---   user has a non-empty salt field resulting from the old-style hashing
---   algorithm.
+--   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 = userPasswordHash u
-        oldSalt = fromMaybe "" $ userPasswordSalt u
     case old of
         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 Nothing   -- Can't handle non-empty salt
+                in return $ Just $ setPasswordHash newHash u
               else do
-                -- Old-style hash: cannot upgrade
+                -- Invalid hash format (perhaps from old version of this module)
                 return Nothing
         Nothing -> return Nothing
 
@@ -288,6 +309,15 @@
     , 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 simply looks up the user id in the
 --   database and calls 'validatePass' to do the work.
@@ -308,13 +338,21 @@
 
 -- | Handle the login form. First parameter is function which maps
 --   username (whatever it might be) to unique user ID.
+--
+--   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))
            -> HandlerT Auth (HandlerT site IO) 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 -> requireJsonBody
+          _         -> lift $ runInputPost $ UserPass
+                       <$> iopt textField "username"
+                       <*> iopt textField "password"
 
     isValid <- lift $ fromMaybe (return False) 
                  (validateUser <$> (uniq =<< mu) <*> mp)
@@ -386,3 +424,17 @@
             }
 
     |]
+
+
+-- | 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 =>
+                  HandlerT Auth (HandlerT site IO) (Route site)
+submitRouteHashDB = do
+    toParent <- getRouteToParent
+    return $ toParent login
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
-resolver: lts-5.4
+resolver: lts-6.6
 packages: 
 - .
 extra-deps: []
diff --git a/test/ExampleData.hs b/test/ExampleData.hs
--- a/test/ExampleData.hs
+++ b/test/ExampleData.hs
@@ -12,13 +12,16 @@
     oldStyleUpgradedUser,
     newStyleValidUser,
     newStyleBadUser,
-    newStyleInOldFormat,
     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,
@@ -27,7 +30,6 @@
 
 instance HashDBUser OldStyleUser where
     userPasswordHash = oldStylePass
-    userPasswordSalt = oldStyleSalt
     setPasswordHash h u = u { oldStyleSalt = Just "",
                               oldStylePass = Just h
                             }
@@ -85,12 +87,6 @@
 newStyleBadUser =
     NewStyleUser "bad"
                  Nothing
-
-newStyleInOldFormat :: OldStyleUser
-newStyleInOldFormat =
-    OldStyleUser "fox"
-                 (Just "sha256|14|2hL7cNopkA/dGy/5CQTuSg==|CUTPW6ICMISSjohFep851f9PdqIn7Y4B75/I77BvEYM=")
-                 (Just "")
 
 stronger :: Int
 stronger = defaultStrength + 2
diff --git a/test/IntegrationTest.hs b/test/IntegrationTest.hs
--- a/test/IntegrationTest.hs
+++ b/test/IntegrationTest.hs
@@ -8,6 +8,9 @@
 ) 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, context, it)
 import qualified Yesod.Test as YT
@@ -25,6 +28,37 @@
 withApp app = before $ return app
 #endif
 
+authUrl :: Text
+authUrl = "http://localhost:3000/auth/login"
+
+data AuthUrl = AuthUrl Text deriving Eq
+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
+instance FromJSON LoginUrl where
+    parseJSON (JSON.Object v) = LoginUrl <$> v .: "loginUrl"
+    parseJSON _ = mempty
+
+successMsg :: Text
+successMsg = "Login Successful"
+
+data SuccessMsg = SuccessMsg Text deriving Eq
+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
@@ -70,5 +104,45 @@
         loc <- doLoginPart1 "paul" "WrongPassword"
         checkFailedLogin loc
       it "fails when unknown user name given" $ do
-        loc <- doLoginPart1 "paul" "WrongPassword"
+        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.assertEqual "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.assertEqual "Login URL" login (Just $ LoginUrl loginUrl)
+#if MIN_VERSION_yesod_test(1,5,0)
+      -- Disable this example for yesod-test < 1.5.0.1, since it uses the 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 a
+          -- dependency of yesod-test >= 1.5 on which this item is conditional.
+          YT.addTokenFromCookie
+        YT.statusIs 200
+        msg <- getBodyJSON
+        YT.assertEqual "Login success" msg (Just $ SuccessMsg successMsg)
+#endif
diff --git a/test/NonDBTests.hs b/test/NonDBTests.hs
--- a/test/NonDBTests.hs
+++ b/test/NonDBTests.hs
@@ -17,23 +17,23 @@
 
     describe "validatePass" $ do
 
-      context "Old-style valid user" $ do
-        it "verifies OK with correct password" $
-            validatePass oldStyleValidUser mypassword `shouldBe` Just True
-        it "fails validation with a wrong password giving Just False" $
+      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 "Old-style user with no password hash" $ do
+      context "Naively upgraded old-style user with no password hash" $ do
         it "fails validation giving Nothing" $
             validatePass oldStyleBadUser1 mypassword `shouldBe` Nothing
 
-      context "Old-style user with no salt" $ do
-        it "fails validation giving Nothing" $
-            validatePass oldStyleBadUser2 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 "verifies OK with correct password" $
-            validatePass oldStyleUpgradedUser mypassword `shouldBe` Just True
+        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
 
@@ -47,27 +47,23 @@
         it "fails validation giving Nothing" $
             validatePass newStyleBadUser mypassword `shouldBe` Nothing
 
-      context "New-style hash in old-style format with blank salt" $ do
-        it "verifies OK with correct password" $
-            validatePass newStyleInOldFormat mypassword `shouldBe` Just True
-        it "fails validation with a wrong password giving Just False" $
-            validatePass newStyleInOldFormat changedpw `shouldBe` Just False
 
-
     describe "upgradePasswordHash" $ do
 
-      context "Upgrade of old-style password hash" $ do
-        it "is Nothing if the user has a valid password hash" $ 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 (but salt non-empty)" $ do
+        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 "is Nothing since salt is non-empty" $ do
+        it "silently succeeds producing a password which won't verify as it still needs salt" $ do
             newuser <- upgradePasswordHash defaultStrength oldStyleUpgradedUser
-            newuser `shouldBe` Nothing
+            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
@@ -83,33 +79,20 @@
             newuser <- upgradePasswordHash stronger newStyleBadUser
             newuser `shouldBe` Nothing
 
-      context "Upgrade of new-style hash in old-style format with blank salt" $ do
-        it "really does have a hash containing the new strength" $ do
-            newuser <- upgradePasswordHash stronger newStyleInOldFormat
-            let s = pack $ "|" ++ show stronger ++ "|"
-                found = fmap (s `isInfixOf`) $ newuser >>= oldStylePass
-            found `shouldBe` Just True
-        it "still verifies with the same password" $ do
-            newuser <- upgradePasswordHash stronger newStyleInOldFormat
-            let valid = newuser >>= flip validatePass mypassword
-            valid `shouldBe` Just True
 
-
     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
-            userPasswordSalt newuser `shouldBe` Just ""
+            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 old-style password hash" $
-            validatePass oldStyleValidUser equivpassword `shouldBe` Just True
         it "when verifying new-style password hash" $
             validatePass newStyleValidUser equivpassword `shouldBe` Just True
         it "when setting a new password" $ do
diff --git a/test/TestSite.hs b/test/TestSite.hs
--- a/test/TestSite.hs
+++ b/test/TestSite.hs
@@ -23,16 +23,25 @@
 import Control.Applicative          ((<$>))
 import Data.Typeable                (Typeable)
 #endif
+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)
+import Yesod.Auth.HashDB            (HashDBUser(..), authHashDB,
+                                     submitRouteHashDB)
 import Yesod.Auth.Message           (AuthMessage (InvalidLogin))
-#if MIN_VERSION_yesod_core(1,4,14)
-import Yesod.Core                   (defaultCsrfMiddleware,
-                                     defaultYesodMiddleware)
+
+#if ! MIN_VERSION_yesod_auth(1,4,9)
+import Yesod.Auth.Message           (AuthMessage (LoginTitle))
+defaultLoginHandler :: AuthHandler master Html
+defaultLoginHandler = do
+    tp <- getRouteToParent
+    lift $ authLayout $ do
+        setTitleI LoginTitle
+        master <- getYesod
+        mapM_ (flip apLogin tp) (authPlugins master)
 #endif
 
 
@@ -104,6 +113,17 @@
     authPlugins _ = [ authHashDB (Just . UniqueUser) ]
 
     authHttpManager = appHttpManager
+
+    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
 
 instance YesodAuthPersist App
 
diff --git a/test/integration.hs b/test/integration.hs
--- a/test/integration.hs
+++ b/test/integration.hs
@@ -9,7 +9,9 @@
 import Yesod
 import Yesod.Auth.HashDB            (setPassword)
 
+#ifndef STANDALONE
 import IntegrationTest
+#endif
 import TestSite
 
 main :: IO ()
@@ -19,7 +21,10 @@
         validUser <- setPassword "MyPassword" $ User "paul" Nothing
         insert_ validUser
     mgr <- newManager
-    -- To try this as a server, replace the `hspec` line below with this one,
-    -- and remove the import of IntegrationTest above.
-    -- warp 3000 $ App mgr conn
+#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/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-6.6
+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,39 @@
+name:            standalone-testsite
+version:         0.1.1
+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.8.0
+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.  TestSite.hs needs MIN_VERSION_yesod_test, so a dummy
+    is provided here.  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:     -DMIN_VERSION_yesod_test(a,b,c)=1 -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.5.1.3
+version:         1.6
 license:         MIT
 license-file:    LICENSE
 author:          Patrick Brisbin, later changes Paul Rouse
@@ -28,6 +28,8 @@
     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
@@ -39,7 +41,7 @@
                    , persistent              >= 2.1        && < 2.7
                    , yesod-form              >= 1.4        && < 1.5
                    , pwstore-fast            >= 2.2
-                   , cryptohash              >= 0.8
+                   , aeson
 
     exposed-modules: Yesod.Auth.HashDB
     ghc-options:     -Wall
@@ -65,6 +67,7 @@
                    , TestSite
                    , TestTools
     build-depends:   base >= 4 && < 5
+                   , aeson
                    , bytestring
                    , basic-prelude
                    , containers
@@ -76,6 +79,7 @@
                    , persistent-sqlite
                    , resourcet
                    , text
+                   , unordered-containers
                    , wai-extra
                    , yesod
                    , yesod-auth
