Ensure JWT expires

This commit is contained in:
calebmer
2015-11-05 17:04:54 -05:00
parent 4803d7c828
commit aab2f0d1f1
7 changed files with 68 additions and 34 deletions
+1
View File
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Filter columns, e.g. `?select=col1,col2` - @ruslantalpa - Filter columns, e.g. `?select=col1,col2` - @ruslantalpa
- Does not execute the count total if header "Prefer: count=none" - @diogob - Does not execute the count total if header "Prefer: count=none" - @diogob
- Postgres connection string argument - @calebmer - Postgres connection string argument - @calebmer
- Ensure JWT expires - @calebmer
### Removed ### Removed
- API versioning feature - @calebmer - API versioning feature - @calebmer
+12 -2
View File
@@ -25,6 +25,7 @@ import Data.Map as M (fromList, toList)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text) import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime)
import PostgREST.PgQuery (pgFmtLit, pgFmtIdent, unquoted) import PostgREST.PgQuery (pgFmtLit, pgFmtIdent, unquoted)
import qualified Web.JWT as JWT import qualified Web.JWT as JWT
import qualified Data.HashMap.Lazy as H import qualified Data.HashMap.Lazy as H
@@ -49,10 +50,19 @@ claimsToSQL = map setVar . toList
returns a map of JWT claims returns a map of JWT claims
In case there is any problem decoding the JWT it returns Nothing. In case there is any problem decoding the JWT it returns Nothing.
-} -}
jwtClaims :: Text -> Text -> Maybe JWT.ClaimsMap jwtClaims :: Text -> Text -> NominalDiffTime -> Maybe JWT.ClaimsMap
jwtClaims secret input = JWT.unregisteredClaims . JWT.claims <$> decoded jwtClaims secret input time =
case claim JWT.exp of
Just (Just expires) ->
if JWT.secondsSinceEpoch expires > time
then customClaims
else Nothing
_ -> customClaims
where where
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
claim :: (JWT.JWTClaimsSet -> a) -> Maybe a
claim prop = prop . JWT.claims <$> decoded
customClaims = claim JWT.unregisteredClaims
-- | Receives the name of a role and returns a SET ROLE statement -- | Receives the name of a role and returns a SET ROLE statement
setRole :: Text -> Text setRole :: Text -> Text
+3 -8
View File
@@ -24,6 +24,7 @@ import qualified Hasql.Postgres as P
import Network.Wai import Network.Wai
import Network.Wai.Handler.Warp hiding (Connection) import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.RequestLogger (logStdout) import Network.Wai.Middleware.RequestLogger (logStdout)
import Data.Time.Clock.POSIX (getPOSIXTime)
import System.IO (BufferMode (..), import System.IO (BufferMode (..),
hSetBuffering, stderr, hSetBuffering, stderr,
stdin, stdout) stdin, stdout)
@@ -72,13 +73,6 @@ main = do
<> show minimumPgVersion) <> show minimumPgVersion)
) supportedOrError ) supportedOrError
-- what was this code for?
-- roleOrError <- H.session pool $ do
-- Identity (role :: Text) <- H.tx Nothing $ H.singleEx
-- [H.stmt|SELECT SESSION_USER|]
-- return role
-- authenticator <- either hasqlError return roleOrError
let txSettings = Just (H.ReadCommitted, Just True) let txSettings = Just (H.ReadCommitted, Just True)
metadata <- H.session pool $ H.tx txSettings $ do metadata <- H.session pool $ H.tx txSettings $ do
tabs <- allTables tabs <- allTables
@@ -105,7 +99,8 @@ main = do
-- print $ findRelation (fakeRels ++ allRels) "test" "pg_source" "clients" -- print $ findRelation (fakeRels ++ allRels) "test" "pg_source" "clients"
runSettings appSettings $ middle $ \ req respond -> do runSettings appSettings $ middle $ \ req respond -> do
time <- getPOSIXTime
body <- strictRequestBody req body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx txSettings $ resOrError <- liftIO $ H.session pool $ H.tx txSettings $
runWithClaims conf (app dbstructure conf body) req runWithClaims conf time (app dbstructure conf body) req
either (respond . errResponse) respond resOrError either (respond . errResponse) respond resOrError
+23 -19
View File
@@ -7,6 +7,7 @@ import Data.Maybe (fromMaybe, isNothing)
import Data.Monoid import Data.Monoid
import Data.Text import Data.Text
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Time.Clock (NominalDiffTime)
import qualified Hasql as H import qualified Hasql as H
import qualified Hasql.Postgres as P import qualified Hasql.Postgres as P
@@ -32,27 +33,30 @@ import qualified Data.Vector as V
import qualified Hasql.Backend as B import qualified Hasql.Backend as B
import qualified Data.Map.Lazy as M import qualified Data.Map.Lazy as M
runWithClaims :: forall s. AppConfig -> runWithClaims :: forall s. AppConfig -> NominalDiffTime ->
(Request -> H.Tx P.Postgres s Response) -> (Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response Request -> H.Tx P.Postgres s Response
runWithClaims conf app req = do runWithClaims conf time app req = do
mapM_ H.unitEx $ stmt <$> env _ <- H.unitEx $ stmt setAnon
app req case split (== ' ') (cs auth) of
where ("Bearer" : tokenStr : _) ->
stmt = (flip $ flip B.Stmt V.empty) True case jwtClaims jwtSecret tokenStr time of
hdrs = requestHeaders req Just claims ->
jwtSecret = (cs $ configJwtSecret conf) :: Text if M.member "role" claims
auth = fromMaybe "" $ lookup hAuthorization hdrs then do
anon = cs $ configAnonRole conf mapM_ H.unitEx $ stmt <$> claimsToSQL claims
claims = app req
fromMaybe (M.fromList []) $ else invalidJWT
case split (==' ') (cs auth) of _ -> invalidJWT
("Bearer" : jwt : _) -> jwtClaims jwtSecret jwt _ -> app req
_ -> Nothing where
env = if M.member "role" claims stmt = (flip $ flip B.Stmt V.empty) True
then jwtEnv hdrs = requestHeaders req
else setRole anon : jwtEnv jwtSecret = (cs $ configJwtSecret conf) :: Text
jwtEnv = claimsToSQL claims auth = fromMaybe "" $ lookup hAuthorization hdrs
anon = cs $ configAnonRole conf
setAnon = setRole anon
invalidJWT = return $ responseLBS status400 [] "Invalid JWT"
redirectInsecure :: Application -> Application redirectInsecure :: Application -> Application
redirectInsecure app req respond = do redirectInsecure app req respond = do
+24 -3
View File
@@ -31,15 +31,36 @@ spec = beforeAll
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
it "works with tokens which have extra fields" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.GfydCh-F4wnM379xs0n1zUgalwJIsb6YoBapCo8HlFk"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with an unexpired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "hides tables from users with invalid JWT" $ do it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 404 `shouldRespondWith` 400
it "hides tables from users with JWT that contain no claims about role" $ do it "should fail when jwt contains no claims" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MKYc_lOECtB0LJOiykilAdlHodB-I0_id2qHKq35dmc" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MKYc_lOECtB0LJOiykilAdlHodB-I0_id2qHKq35dmc"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 404 `shouldRespondWith` 400
it "hides tables from users with JWT that contain no claims about role" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.zyohGMnrDy4_8eJTl6I2AUXO3MeCCiwR24aGWRkTE9o"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "recovers after 400 error with logged in user" $ do it "recovers after 400 error with logged in user" $ do
_ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |] _ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]
+4 -2
View File
@@ -11,6 +11,7 @@ import Hasql.Postgres as P
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Monoid import Data.Monoid
import Data.Text hiding (map) import Data.Text hiding (map)
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Data.Vector as V import qualified Data.Vector as V
import Control.Monad (void) import Control.Monad (void)
import Control.Applicative import Control.Applicative
@@ -73,9 +74,10 @@ withApp perform = do
} }
perform $ middle $ \req resp -> do perform $ middle $ \req resp -> do
time <- getPOSIXTime
body <- strictRequestBody req body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx txSettings result <- liftIO $ H.session pool $ H.tx txSettings
$ runWithClaims cfg (app dbstructure cfg body) req $ runWithClaims cfg time (app dbstructure cfg body) req
either (resp . errResponse) resp result either (resp . errResponse) resp result
where middle = defaultMiddle False where middle = defaultMiddle False
@@ -134,7 +136,7 @@ clearProjectsTable :: IO ()
clearProjectsTable = do clearProjectsTable = do
pool <- testPool pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $ void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ B.Stmt ("delete from test.projects where id > 4") V.empty True H.unitEx $ B.Stmt "delete from test.projects where id > 4" V.empty True
createItems :: Int -> IO () createItems :: Int -> IO ()
+1
View File
@@ -291,6 +291,7 @@ $$ LANGUAGE SQL;
CREATE TYPE public.jwt_claims AS (role text, id text); CREATE TYPE public.jwt_claims AS (role text, id text);
CREATE FUNCTION test.login(id text, pass text) CREATE FUNCTION test.login(id text, pass text)
RETURNS public.jwt_claims RETURNS public.jwt_claims
SECURITY DEFINER SECURITY DEFINER