diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,7 @@
+## 1.5.1
+
+* Include CSRF token in default form
+
 ## 1.5
 
 This release can break both old code and old database entries.  For details
diff --git a/Yesod/Auth/HashDB.hs b/Yesod/Auth/HashDB.hs
--- a/Yesod/Auth/HashDB.hs
+++ b/Yesod/Auth/HashDB.hs
@@ -131,6 +131,11 @@
 import           Yesod.Form
 import           Yesod.Persist
 
+#if !MIN_VERSION_yesod_core(1,4,14)
+defaultCsrfParamName :: Text
+defaultCsrfParamName = "_token"
+#endif
+
 -- | Default strength used for passwords (see "Crypto.PasswordStore" for
 --   details).
 defaultStrength :: Int
@@ -345,30 +350,35 @@
 
 
 defaultForm :: Yesod app => Route app -> WidgetT app IO ()
-defaultForm loginRoute = toWidget [hamlet|
-$newline never
-    <div id="header">
+defaultForm loginRoute = do
+    request <- getRequest
+    let mtok = reqToken request
+    toWidget [hamlet|
+      $newline never
+      <div id="header">
         <h1>Login
 
-    <div id="login">
+      <div id="login">
         <form method="post" action="@{loginRoute}">
-            <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">
+          $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">
 
-            <script>
-                if (!("autofocus" in document.createElement("input"))) {
-                    document.getElementById("x").focus();
-                }
+          <script>
+            if (!("autofocus" in document.createElement("input"))) {
+                document.getElementById("x").focus();
+            }
 
-|]
+    |]
diff --git a/test/IntegrationTest.hs b/test/IntegrationTest.hs
new file mode 100644
--- /dev/null
+++ b/test/IntegrationTest.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE NoImplicitPrelude          #-}
+{-# LANGUAGE OverloadedStrings          #-}
+
+module IntegrationTest (
+    withApp,
+    integrationSpec
+) where
+
+import ClassyPrelude
+import Test.Hspec                   (Spec, SpecWith, before,
+                                     describe, context, it)
+import qualified Yesod.Test as YT
+
+import TestSite                     (App, Route(..), Handler, runDB)
+import TestTools
+
+#if MIN_VERSION_yesod_test(1,5,0)
+type MyTestApp = YT.TestApp App
+withApp :: App -> SpecWith (YT.TestApp App) -> Spec
+withApp app = before $ return (app, id)
+#else
+type MyTestApp = App
+withApp :: App -> SpecWith App -> Spec
+withApp app = before $ return app
+#endif
+
+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
+#if MIN_VERSION_yesod_core(1,4,19)
+            -- yesod-core-1.4.19 added the CSRF token to the redirectToPost form
+            YT.addToken
+#elif MIN_VERSION_yesod_core(1,4,14) && MIN_VERSION_yesod_test(1,4,4)
+            -- Otherwise we still use CSRF middleware (see TestSite.hs) from
+            -- yesod-core 1.4.14 onwards as long as we can get the token
+            -- from the cookie, which was implemented in yesod-test-1.4.4
+            YT.addTokenFromCookie
+#endif
+        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 "paul" "WrongPassword"
+        checkFailedLogin loc
diff --git a/test/TestSite.hs b/test/TestSite.hs
new file mode 100644
--- /dev/null
+++ b/test/TestSite.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE CPP                        #-}
+#if __GLASGOW_HASKELL__ < 710
+{-# LANGUAGE DeriveDataTypeable         #-}
+#endif
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+
+module TestSite (
+    User(..),
+    migrateAll,
+    App(..),
+    Route(..),
+    Handler,
+    runDB
+) where
+
+#if __GLASGOW_HASKELL__ < 710
+import Control.Applicative          ((<$>))
+import Data.Typeable                (Typeable)
+#endif
+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.Message           (AuthMessage (InvalidLogin))
+#if MIN_VERSION_yesod_core(1,4,14)
+import Yesod.Core                   (defaultCsrfMiddleware,
+                                     defaultYesodMiddleware)
+#endif
+
+
+-- Trivial example site needing authentication
+--
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+User
+    name       Text
+    password   Text Maybe
+    UniqueUser name
+    deriving Show
+#if __GLASGOW_HASKELL__ < 710
+    deriving Typeable
+#endif
+|]
+
+instance HashDBUser User where
+    userPasswordHash = userPassword
+    setPasswordHash h u = u { userPassword = Just h }
+
+data App = App
+    { appHttpManager  :: Manager
+    , appDBConnection :: SqlBackend
+    }
+
+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
+
+#if MIN_VERSION_yesod_core(1,4,19) || (MIN_VERSION_yesod_core(1,4,14) && MIN_VERSION_yesod_test(1,4,4))
+    -- CSRF middleware requires yesod-core-1.4.14, but only include it if tests
+    -- can get at the token (see IntegrationTest.hs for additional comment).
+    yesodMiddleware = defaultCsrfMiddleware . defaultYesodMiddleware
+#endif
+
+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 = runDB $ do
+        x <- getBy $ UniqueUser $ credsIdent creds
+        case x of
+            Just (Entity uid _) -> return $ Authenticated uid
+            Nothing             -> return $ UserError InvalidLogin
+
+    authPlugins _ = [ authHashDB (Just . UniqueUser) ]
+
+    authHttpManager = appHttpManager
+
+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,142 @@
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE NoImplicitPrelude          #-}
+module TestTools (
+    assertFailure,
+    urlPath,
+    needsLogin,
+    doLogin,
+    doLoginPart1,
+    doLoginPart2,
+    checkFailedLogin,
+    StdMethod(..)
+) where
+
+import TestSite           (App(..))
+import ClassyPrelude
+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 = assertEqual 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
+    assertEqual "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
+                        assertEqual ("Expected a 302 or 303 redirection status "
+                                     ++ "but received " ++ show code)
+                                    (code `oelem` [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")
+          (assertEqual "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,25 @@
+{-# 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)
+
+import IntegrationTest
+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
+    -- 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
+    hspec $ withApp (App mgr conn) integrationSpec
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
+version:         1.5.1
 license:         MIT
 license-file:    LICENSE
 author:          Patrick Brisbin, later changes Paul Rouse
@@ -54,6 +54,33 @@
                    , yesod-auth-hashdb
                    , hspec
                    , text
+
+test-suite integration
+    type:            exitcode-stdio-1.0
+    main-is:         integration.hs
+    hs-source-dirs:  test
+    ghc-options:     -Wall
+    other-modules:   IntegrationTest
+                   , TestSite
+                   , TestTools
+    build-depends:   base >= 4 && < 5
+                   , bytestring
+                   , classy-prelude
+                   , containers
+                   , hspec                   >= 2.0.0
+                   , http-conduit
+                   , http-types
+                   , monad-logger
+                   , network-uri
+                   , persistent-sqlite
+                   , resourcet
+                   , text
+                   , wai-extra
+                   , yesod
+                   , yesod-auth
+                   , yesod-auth-hashdb
+                   , yesod-core
+                   , yesod-test              >= 1.4.3
 
 source-repository head
   type:     git
