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:
Taimoor Zaeem
2026-06-22 23:52:45 +05:00
parent ee30bd03ac
commit f5fd2e71b6
4 changed files with 145 additions and 78 deletions
+1
View File
@@ -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
View File
@@ -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'
+125 -2
View File
@@ -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 }
+10
View File
@@ -17,6 +17,7 @@ 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 Data.Time.Clock.POSIX (getPOSIXTime)
import Network.Wai (Application) import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus)) import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
@@ -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