Issue http 401 for expired jwt

Fixes #512
This commit is contained in:
Joe Nelson
2016-09-24 21:28:08 -07:00
parent 35da4809d4
commit fb5fce026d
3 changed files with 34 additions and 27 deletions
+19 -11
View File
@@ -16,6 +16,7 @@ module PostgREST.Auth (
, containsRole , containsRole
, jwtClaims , jwtClaims
, tokenJWT , tokenJWT
, JWTAttempt(..)
) where ) where
import Protolude import Protolude
@@ -47,17 +48,24 @@ claimsToSQL claims = roleStmts <> varStmts
valueToVariable = pgFmtLit . unquoted valueToVariable = pgFmtLit . unquoted
{-| {-|
Receives the JWT secret (from config) and a JWT and Possible situations encountered with client JWTs
returns a map of JWT claims
In case there is any problem decoding the JWT it returns an error Text
-} -}
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Either Text (M.HashMap Text Value) data JWTAttempt = JWTExpired
jwtClaims _ "" _ = Right M.empty | JWTInvalid
| JWTClaims (M.HashMap Text Value)
deriving Eq
{-|
Receives the JWT secret (from config) and a JWT and returns a map
of JWT claims.
-}
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt
jwtClaims _ "" _ = JWTClaims M.empty
jwtClaims secret jwt time = jwtClaims secret jwt time =
case isExpired <$> mClaims of case isExpired <$> mClaims of
Just True -> Left "JWT expired" Just True -> JWTExpired
Nothing -> Left "Invalid JWT" Nothing -> JWTInvalid
Just False -> Right $ value2map $ fromJust mClaims Just False -> JWTClaims $ value2map $ fromJust mClaims
where where
isExpired claims = isExpired claims =
let mExp = claims ^? key "exp" . _Integer let mExp = claims ^? key "exp" . _Integer
@@ -80,6 +88,6 @@ tokenJWT secret _ = tokenJWT secret emptyArray
{-| {-|
Whether a response from jwtClaims contains a role claim Whether a response from jwtClaims contains a role claim
-} -}
containsRole :: Either Text (M.HashMap Text Value) -> Bool containsRole :: JWTAttempt -> Bool
containsRole (Left _) = False containsRole (JWTClaims claims) = M.member "role" claims
containsRole (Right claims) = M.member "role" claims containsRole _ = False
+9 -10
View File
@@ -7,33 +7,32 @@ import Data.Aeson (Value (..))
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import qualified Hasql.Transaction as H import qualified Hasql.Transaction as H
import Network.HTTP.Types.Status (status400) import Network.HTTP.Types.Status (badRequest400, unauthorized401)
import Network.Wai (Application, Response) import Network.Wai (Application, Response)
import Network.Wai.Middleware.Cors (cors) import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip) import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy) import Network.Wai.Middleware.Static (only, staticPolicy)
import PostgREST.ApiRequest (ApiRequest(..)) import PostgREST.ApiRequest (ApiRequest(..))
import PostgREST.Auth (claimsToSQL) import PostgREST.Auth (claimsToSQL, JWTAttempt(..))
import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (errResponse) import PostgREST.Error (errResponse)
import Data.Text
import Protolude hiding (concat, null) import Protolude hiding (concat, null)
runWithClaims :: AppConfig -> Either Text (M.HashMap Text Value) -> runWithClaims :: AppConfig -> JWTAttempt ->
(ApiRequest -> H.Transaction Response) -> (ApiRequest -> H.Transaction Response) ->
ApiRequest -> H.Transaction Response ApiRequest -> H.Transaction Response
runWithClaims conf eClaims app req = runWithClaims conf eClaims app req =
case eClaims of case eClaims of
Left e -> clientErr e JWTExpired -> return $ errResponse unauthorized401 "JWT expired"
Right claims -> do JWTInvalid -> return $ errResponse badRequest400 "JWT invalid"
-- role claim defaults to anon if not specified in jwt JWTClaims claims -> do
H.sql . mconcat . claimsToSQL $ M.union claims (M.singleton "role" anon) -- role claim defaults to anon if not specified in jwt
app req H.sql . mconcat . claimsToSQL $ M.union claims (M.singleton "role" anon)
app req
where where
anon = String . toS $ configAnonRole conf anon = String . toS $ configAnonRole conf
clientErr = return . errResponse status400
defaultMiddle :: Application -> Application defaultMiddle :: Application -> Application
defaultMiddle = defaultMiddle =
+6 -6
View File
@@ -80,7 +80,7 @@ spec = describe "authorization" $ do
it "fails with an expired token" $ do it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400 `shouldRespondWith` 401
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"
@@ -88,16 +88,16 @@ spec = describe "authorization" $ do
`shouldRespondWith` 400 `shouldRespondWith` 400
it "should fail when jwt contains no claims" $ do it "should fail when jwt contains no claims" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MKYc_lOECtB0LJOiykilAdlHodB-I0_id2qHKq35dmc" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.lu-rG8aSCiw-aOlN0IxpRGz5r7Jwq7K9r3tuMPUpytI"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400 `shouldRespondWith` 401
it "hides tables from users with JWT that contain no claims about role" $ do it "hides tables from users with JWT that contain no claims about role" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.zyohGMnrDy4_8eJTl6I2AUXO3MeCCiwR24aGWRkTE9o" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.Jneso9X519Vh0z7i9PbXIu7W1HEoq9RRw9BBbyQKFCQ"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400 `shouldRespondWith` 401
it "recovers after 400 error with logged in user" $ do it "recovers after 401 error with logged in user" $ do
_ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |] _ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
_ <- request methodPost "/rpc/problem" [auth] "" _ <- request methodPost "/rpc/problem" [auth] ""