snaplet-sqlite-simple-jwt-auth 0.1.1.0 → 0.2.0.0
raw patch · 4 files changed
+50/−28 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Snap.Snaplet.SqliteSimple.JwtAuth: Options :: HashingPolicy -> String -> Int -> Options
+ Snap.Snaplet.SqliteSimple.JwtAuth: [hashingPolicy] :: Options -> HashingPolicy
+ Snap.Snaplet.SqliteSimple.JwtAuth: [maxTokenExpiration] :: Options -> Int
+ Snap.Snaplet.SqliteSimple.JwtAuth: [options] :: SqliteJwt -> Options
+ Snap.Snaplet.SqliteSimple.JwtAuth: [signingKeyFilename] :: Options -> String
+ Snap.Snaplet.SqliteSimple.JwtAuth: data Options
+ Snap.Snaplet.SqliteSimple.JwtAuth: defaults :: Options
- Snap.Snaplet.SqliteSimple.JwtAuth: SqliteJwt :: Secret -> MVar Connection -> SqliteJwt
+ Snap.Snaplet.SqliteSimple.JwtAuth: SqliteJwt :: Secret -> MVar Connection -> Options -> SqliteJwt
- Snap.Snaplet.SqliteSimple.JwtAuth: sqliteJwtInit :: String -> Snaplet Sqlite -> SnapletInit b SqliteJwt
+ Snap.Snaplet.SqliteSimple.JwtAuth: sqliteJwtInit :: Options -> Snaplet Sqlite -> SnapletInit b SqliteJwt
Files
- snaplet-sqlite-simple-jwt-auth.cabal +1/−1
- src/Snap/Snaplet/SqliteSimple/JwtAuth.hs +2/−2
- src/Snap/Snaplet/SqliteSimple/JwtAuth/JwtAuth.hs +28/−23
- src/Snap/Snaplet/SqliteSimple/JwtAuth/Types.hs +19/−2
snaplet-sqlite-simple-jwt-auth.cabal view
@@ -1,5 +1,5 @@ name: snaplet-sqlite-simple-jwt-auth-version: 0.1.1.0+version: 0.2.0.0 synopsis: Snaplet for JWT authentication with snaplet-sqlite-simple description: JWT authentication snaplet for snaplet-sqlite-simple.
src/Snap/Snaplet/SqliteSimple/JwtAuth.hs view
@@ -8,9 +8,11 @@ -- * Types SqliteJwt(..)+ , Options(..) , User(..) , AuthFailure(..) -- * Initialization+ , defaults , sqliteJwtInit -- * High-level handlers @@ -54,8 +56,6 @@ import Snap.Snaplet.SqliteSimple.JwtAuth.JwtAuth -- $intro--- NOTE: This is still very much a work-in-progress project!--- -- A snap middleware for implementing JWT-based authentication with user -- accounts persisted in a SQLite3 database. It's intended use is to protect -- server API routes used in single-page web applications (SPA) and mobile
src/Snap/Snaplet/SqliteSimple/JwtAuth/JwtAuth.hs view
@@ -3,17 +3,11 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} -{---Minimal proof of concept JWT authentication snaplet using snaplet-sqlite--simple.---}- module Snap.Snaplet.SqliteSimple.JwtAuth.JwtAuth ( SqliteJwt(..) , User(..) , AuthFailure(..)+ , defaults , sqliteJwtInit , requireAuth , registerUser@@ -41,6 +35,7 @@ import Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Data.Text.Encoding as LT+import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) import Snap import Snap.Snaplet.SqliteSimple (Sqlite, sqliteConn) import qualified Web.JWT as JWT@@ -49,8 +44,13 @@ import Snap.Snaplet.SqliteSimple.JwtAuth.Types import qualified Snap.Snaplet.SqliteSimple.JwtAuth.Db as Db -bcryptPolicy :: BC.HashingPolicy-bcryptPolicy = BC.fastBcryptHashingPolicy+-- | Default settings for the snaplet+defaults :: Options+defaults = Options {+ hashingPolicy = BC.fastBcryptHashingPolicy+ , signingKeyFilename = "jwt_secret.txt"+ , maxTokenExpiration = 60*60*24*14 -- two weeks+ } ------------------------------------------------------------------------- -- | Initializer for the sqlite-simple JwtAuth snaplet.@@ -63,14 +63,14 @@ -- Initialization will automatically setup SQL tables used to store user -- accounts. It will also automatically upgrade the SQL schema if necessary. sqliteJwtInit- :: String -- ^ JWT secret signing key filename+ :: Options -- ^ Site policy options -> Snaplet Sqlite -- ^ The sqlite-simple snaplet -> SnapletInit b SqliteJwt-sqliteJwtInit jwtSigningKeyFname db = makeSnaplet "sqlite-simple-jwt" description Nothing $ do- k <- liftIO $ (JWT.binarySecret <$> getKey jwtSigningKeyFname)+sqliteJwtInit options db = makeSnaplet "sqlite-simple-jwt" description Nothing $ do+ k <- liftIO $ (JWT.binarySecret <$> getKey (signingKeyFilename options)) let conn = sqliteConn $ db ^# snapletValue liftIO $ Db.createTableIfMissing conn- return $ SqliteJwt k conn+ return $ SqliteJwt k conn options where description = "sqlite-simple jwt auth" @@ -82,9 +82,10 @@ -> Handler b SqliteJwt (Either AuthFailure User) createUser loginName password = do user <- Db.queryUser loginName+ hashPolicy <- hashingPolicy <$> gets options case user of Nothing -> do- hashedPass <- liftIO $ BC.hashPasswordUsingPolicy bcryptPolicy (LT.encodeUtf8 password)+ hashedPass <- liftIO $ BC.hashPasswordUsingPolicy hashPolicy (LT.encodeUtf8 password) -- TODO don't use fromJust Db.insertUser loginName (fromJust hashedPass) u <- Db.queryUser loginName@@ -120,11 +121,12 @@ payload = LT.decodeUtf8 <$> AP.takeWhile1 (AP.inClass base64) base64 = "A-Za-z0-9+/_=.-" -jwtFromUser :: User -> Handler b SqliteJwt JWT.JSON-jwtFromUser (User uid loginName) = do+jwtFromUser :: User -> POSIXTime -> Handler b SqliteJwt JWT.JSON+jwtFromUser (User uid loginName) expiresOn = do key <- gets siteSecret let cs = JWT.def { JWT.unregisteredClaims = M.fromList [("id", Number (fromIntegral uid)), ("login", String loginName)]+ , JWT.exp = JWT.intDate $ expiresOn } return $ JWT.encodeSigned JWT.HS256 key cs @@ -149,14 +151,15 @@ key <- gets siteSecret req <- getRequest res <- runExceptT $ do- authHdr <- getHeader "Authorization" (rqHeaders req) ?? "Missing Authorization header"+ curTime <- liftIO $ getPOSIXTime+ authHdr <- getHeader "Authorization" (rqHeaders req) ?? "missing Authorization header" encPayload <- hoistEither . parseBearerJwt $ authHdr- jwt <- JWT.decode encPayload ?? "Malformed JWT"- verifJwt <- JWT.verify key jwt ?? "JWT verification failed"+ jwt <- JWT.decode encPayload ?? "malformed JWT"+ verifJwt <- JWT.verify key jwt ?? "JWT verification failed"+ exp <- JWT.exp (JWT.claims verifJwt) ?? "exp not set in JWT"+ assertZ (JWT.secondsSinceEpoch exp >= curTime) ?? "token has expired" let unregClaims = JWT.unregisteredClaims (JWT.claims verifJwt)- -- TODO verify expiration too- user <- hoistEither . parseEither parseJSON $ (toObject unregClaims)- return user+ hoistEither . parseEither parseJSON $ (toObject unregClaims) either (finishEarly 401 . BS8.pack) action res where toObject = Object . HM.fromList . M.toList@@ -179,7 +182,9 @@ loginOK :: User -> Handler b SqliteJwt () loginOK user = do- jwt <- jwtFromUser user+ expiresIn <- maxTokenExpiration <$> gets options+ curTime <- liftIO getPOSIXTime+ jwt <- jwtFromUser user (curTime + (fromIntegral expiresIn)) writeJSON $ object [ "token" .= jwt ] -- | Create a new user.
src/Snap/Snaplet/SqliteSimple/JwtAuth/Types.hs view
@@ -4,6 +4,7 @@ import Control.Concurrent import Control.Monad+import qualified Crypto.BCrypt as BC import Data.Aeson import qualified Data.Text as T import Database.SQLite.Simple@@ -13,10 +14,26 @@ data SqliteJwt = SqliteJwt { siteSecret :: JWT.Secret , sqliteJwtConn :: MVar Connection+ , options :: Options } --- | User account--- User ID and login name.+-- | Site configuration options+--+-- Tokens will expire after 'maxTokenExpiration' seconds. The login handler+-- may be extended in the future to allow setting a lower expiration time. In+-- that case, 'maxTokenExpiration' will be used as the upper limit for expiry.+-- Otherwise someone might modify login requests to set an infinitely long+-- expiration time and JWTs would never expire.+--+-- You can use the 'Snap.Snaplet.SqliteSimple.JwtAuth.defaults' value as your+-- basis, overriding individual fields as necessary.+data Options = Options {+ hashingPolicy :: BC.HashingPolicy -- ^ Which bcrypt hashing policy to use+ , signingKeyFilename :: String -- ^ Where to save site JWT key+ , maxTokenExpiration :: Int -- ^ Maximum expiration time for a JWT+ } deriving (Show)++-- | User ID and login name. -- -- If you need to store additional fields for your user accounts, persist them -- in your application SQL tables and key them by 'userId'.