test: move jwt error tests from io tests to spec tests
Towards #4946. Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
This commit is contained in:
@@ -298,6 +298,7 @@ test-suite spec
|
|||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
, scientific >= 0.3.4 && < 0.4
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, text >= 1.2.2 && < 2.2
|
, text >= 1.2.2 && < 2.2
|
||||||
|
, time >= 1.6 && < 1.15
|
||||||
, transformers-base >= 0.4.4 && < 0.5
|
, transformers-base >= 0.4.4 && < 0.5
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
, wai-extra >= 3.0.19 && < 3.2
|
, wai-extra >= 3.0.19 && < 3.2
|
||||||
|
|||||||
+1
-68
@@ -7,7 +7,7 @@ import time
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from config import BASEDIR, CONFIGSDIR, FIXTURES, SECRET
|
from config import BASEDIR, CONFIGSDIR, FIXTURES, SECRET
|
||||||
from util import authheader, jwtauthheader, parse_server_timings_header, relativeSeconds
|
from util import authheader, jwtauthheader, parse_server_timings_header
|
||||||
from postgrest import (
|
from postgrest import (
|
||||||
run,
|
run,
|
||||||
sleep_until_postgrest_config_reload,
|
sleep_until_postgrest_config_reload,
|
||||||
@@ -67,73 +67,6 @@ def test_read_secret_from_stdin_dbconfig(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_jwt_errors(defaultenv):
|
|
||||||
"invalid JWT should throw error"
|
|
||||||
|
|
||||||
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET, "PGRST_JWT_AUD": "io tests"}
|
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
|
||||||
headers = jwtauthheader({}, "other secret")
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "No suitable key or wrong key type"
|
|
||||||
assert (
|
|
||||||
response.json()["details"] == "None of the keys was able to decode the JWT"
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == 'role "not_existing" does not exist'
|
|
||||||
|
|
||||||
# -35 seconds, because we allow clock skew of 30 seconds
|
|
||||||
headers = jwtauthheader({"exp": relativeSeconds(-35)}, SECRET)
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "JWT expired"
|
|
||||||
|
|
||||||
# 35 seconds, because we allow clock skew of 30 seconds
|
|
||||||
headers = jwtauthheader({"nbf": relativeSeconds(35)}, SECRET)
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "JWT not yet valid"
|
|
||||||
|
|
||||||
# 35 seconds, because we allow clock skew of 35 seconds
|
|
||||||
headers = jwtauthheader({"iat": relativeSeconds(35)}, SECRET)
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "JWT issued at future"
|
|
||||||
|
|
||||||
headers = jwtauthheader({"aud": "not set"}, SECRET)
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "JWT not in audience"
|
|
||||||
|
|
||||||
# partial token, no signature
|
|
||||||
headers = authheader("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bm90IGFuIG9iamVjdA")
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "Expected 3 parts in JWT; got 2"
|
|
||||||
|
|
||||||
# complete token but random characters
|
|
||||||
headers = authheader("quifquirndsjagnrgniur.fonvoienqhhdj.iuqvnvhojah")
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "JWT cryptographic operation failed"
|
|
||||||
|
|
||||||
# token with algorithm "none"
|
|
||||||
headers = authheader(
|
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.e30.yOBhlOIqn56T-4NvyEXCjfi3UmyQZ-BzXtePMO2NgRI"
|
|
||||||
)
|
|
||||||
response = postgrest.session.get("/", headers=headers)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
|
||||||
assert (
|
|
||||||
response.json()["details"]
|
|
||||||
== "JWT is unsecured but expected 'alg' was not 'none'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_fail_with_invalid_password(defaultenv):
|
def test_fail_with_invalid_password(defaultenv):
|
||||||
"Connecting with an invalid password should fail without retries."
|
"Connecting with an invalid password should fail without retries."
|
||||||
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
|
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import PostgREST.Config (AppConfig (..))
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWithConfig
|
spec :: SpecWithConfig
|
||||||
spec withConfig = withConfig baseCfg $ do
|
spec withConfig = do
|
||||||
describe "Test PostgreSQL and PostgREST errors" $ do
|
withConfig baseCfg $ describe "Test PostgreSQL and PostgREST errors" $ do
|
||||||
it "should return 500 for cardinality_violation" $
|
it "should return 500 for cardinality_violation" $
|
||||||
get "/bad_subquery" `shouldRespondWith` 500
|
get "/bad_subquery" `shouldRespondWith` 500
|
||||||
|
|
||||||
@@ -100,3 +102,124 @@ spec withConfig = withConfig baseCfg $ do
|
|||||||
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
|
||||||
, "Content-Length" <:> "119" ]
|
, "Content-Length" <:> "119" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "JWT Errors" $ do
|
||||||
|
it "error on jwt encoded with wrong secret" $ do
|
||||||
|
let jwtPayload = [json|{}|]
|
||||||
|
auth = authHeaderJWT $ generateJWTWithSecret jwtPayload "wrong secret"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST301",
|
||||||
|
"details":"None of the keys was able to decode the JWT",
|
||||||
|
"hint":null,
|
||||||
|
"message":"No suitable key or wrong key type"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
it "when role does not exist" $ do
|
||||||
|
let jwtPayload = [json|{ "role": "not existing" }|]
|
||||||
|
auth = authHeaderJWT $ generateJWT jwtPayload
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"22023",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"role \"not existing\" does not exist"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
context "we allow 30 seconds clock skew" $ do
|
||||||
|
it "it should return error if expired" $ do
|
||||||
|
currentTime <- liftIO $ relativeSeconds (-35)
|
||||||
|
let jwtPayload = [json|{ "exp": #{currentTime} }|]
|
||||||
|
auth = authHeaderJWT $ generateJWT jwtPayload
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST303",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"JWT expired"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
it "it should return error if used before it is valid" $ do
|
||||||
|
currentTime <- liftIO $ relativeSeconds 35
|
||||||
|
let jwtPayload = [json|{ "nbf": #{currentTime} }|]
|
||||||
|
auth = authHeaderJWT $ generateJWT jwtPayload
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST303",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"JWT not yet valid"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
it "it should return error if issued at future" $ do
|
||||||
|
currentTime <- liftIO $ relativeSeconds 35
|
||||||
|
let jwtPayload = [json|{ "iat": #{currentTime} }|]
|
||||||
|
auth = authHeaderJWT $ generateJWT jwtPayload
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST303",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"JWT issued at future"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
withConfig baseCfg { configJwtAudience = Just "spec tests" } $ describe "Test JWT Audience error" $ do
|
||||||
|
it "it should return error if JWT not in audience" $ do
|
||||||
|
let jwtPayload = [json|{ "aud": "not set" }|]
|
||||||
|
auth = authHeaderJWT $ generateJWT jwtPayload
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST303",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"JWT not in audience"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
withConfig baseCfg $ describe "Test JWT Token format errors" $ do
|
||||||
|
it "when partial token is provided" $ do
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bm90IGFuIG9iamVjdA"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST301",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"Expected 3 parts in JWT; got 2"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
it "when token is complete but random characters" $ do
|
||||||
|
let auth = authHeaderJWT "quifquirndsjagnrgniur.fonvoienqhhdj.iuqvnvhojah"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST301",
|
||||||
|
"details":null,
|
||||||
|
"hint":null,
|
||||||
|
"message":"JWT cryptographic operation failed"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|
||||||
|
it "when token with algorithm 'none' is used" $ do
|
||||||
|
let auth = authHeaderJWT "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.e30.yOBhlOIqn56T-4NvyEXCjfi3UmyQZ-BzXtePMO2NgRI"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"code":"PGRST301",
|
||||||
|
"details":"JWT is unsecured but expected 'alg' was not 'none'",
|
||||||
|
"hint":null,
|
||||||
|
"message":"Wrong or unsupported encoding algorithm"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 401 }
|
||||||
|
|||||||
+18
-8
@@ -13,14 +13,15 @@ import qualified Jose.Jwa as JWT
|
|||||||
import qualified Jose.Jws as JWT
|
import qualified Jose.Jws as JWT
|
||||||
import qualified Jose.Jwt as JWT
|
import qualified Jose.Jwt as JWT
|
||||||
|
|
||||||
import Data.Aeson ((.=))
|
import Data.Aeson ((.=))
|
||||||
import Data.CaseInsensitive (CI (..), original)
|
import Data.CaseInsensitive (CI (..), original)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.List.NonEmpty (fromList)
|
import Data.List.NonEmpty (fromList)
|
||||||
import Network.Wai (Application)
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
import Network.Wai (Application)
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||||
import Text.Regex.TDFA ((=~))
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
import Text.Regex.TDFA ((=~))
|
||||||
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
@@ -207,6 +208,10 @@ generateJWT :: BL.ByteString -> ByteString
|
|||||||
generateJWT claims =
|
generateJWT claims =
|
||||||
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)
|
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)
|
||||||
|
|
||||||
|
generateJWTWithSecret :: BL.ByteString -> ByteString -> ByteString
|
||||||
|
generateJWTWithSecret claims secret =
|
||||||
|
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 secret (BL.toStrict claims)
|
||||||
|
|
||||||
-- | Tests whether the text can be parsed as a json object containing
|
-- | Tests whether the text can be parsed as a json object containing
|
||||||
-- the key "message", and optional keys "details", "hint", "code",
|
-- the key "message", and optional keys "details", "hint", "code",
|
||||||
-- and no extraneous keys
|
-- and no extraneous keys
|
||||||
@@ -245,3 +250,8 @@ getInsertDataForTiobePlsTable rows =
|
|||||||
|
|
||||||
readFixtureFile :: FilePath -> BL.ByteString
|
readFixtureFile :: FilePath -> BL.ByteString
|
||||||
readFixtureFile file = unsafePerformIO $ BL.readFile $ "test/spec/fixtures/" <> file
|
readFixtureFile file = unsafePerformIO $ BL.readFile $ "test/spec/fixtures/" <> file
|
||||||
|
|
||||||
|
relativeSeconds :: Integer -> IO Integer
|
||||||
|
relativeSeconds s = do
|
||||||
|
currTime <- getPOSIXTime
|
||||||
|
return $ floor currTime + s
|
||||||
|
|||||||
Reference in New Issue
Block a user