fix: improve jwt errors
This commit is contained in:
committed by
Steve Chavez
parent
4819520e3a
commit
36b6a2c86b
@@ -28,6 +28,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3727, Clarify "listening" logs - @steve-chavez
|
- #3727, Clarify "listening" logs - @steve-chavez
|
||||||
- #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez
|
- #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez
|
||||||
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
|
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
|
||||||
|
- #3600, #3926, Improve JWT errors - @taimoorzaeem
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -43,6 +44,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3013, Drop support for Limited updates/deletes
|
- #3013, Drop support for Limited updates/deletes
|
||||||
+ The feature was complicated and largely unused.
|
+ The feature was complicated and largely unused.
|
||||||
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
|
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
|
||||||
|
- #3600, #3926, Improve JWT errors - @taimoorzaeem
|
||||||
|
+ Return `PGRST301` error when `Bearer` in auth header is sent empty
|
||||||
|
+ Diagnostic error messages instead of exposed internals
|
||||||
|
+ Return new `PGRST303` error when jwt claims decoding fails
|
||||||
|
|
||||||
## [12.2.8] - 2025-02-10
|
## [12.2.8] - 2025-02-10
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ You can specify the literal value as we saw earlier, or reference a filename to
|
|||||||
|
|
||||||
jwt-secret = "@rsa.jwk.pub"
|
jwt-secret = "@rsa.jwk.pub"
|
||||||
|
|
||||||
|
.. _jwt_claims_validation:
|
||||||
|
|
||||||
JWT Claims Validation
|
JWT Claims Validation
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -305,14 +305,18 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1`
|
|||||||
| | | configuration. |
|
| | | configuration. |
|
||||||
| PGRST300 | | |
|
| PGRST300 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst301: | 401 | Any error related to the verification of the JWT, |
|
| .. _pgrst301: | 401 | Provided JWT couldn't be decoded or it is invalid. |
|
||||||
| | | which means that the JWT provided is invalid in some way. |
|
| | | |
|
||||||
| PGRST301 | | |
|
| PGRST301 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst302: | 401 | Attempted to do a request without |
|
| .. _pgrst302: | 401 | Attempted to do a request without |
|
||||||
| | | :ref:`authentication <client_auth>` when the anonymous role |
|
| | | :ref:`authentication <client_auth>` when the anonymous role |
|
||||||
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
|
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
| .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` |
|
||||||
|
| | | or parsing failed. |
|
||||||
|
| PGRST303 | | |
|
||||||
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
|
||||||
.. The Internal Errors Group X** is always at the end
|
.. The Internal Errors Group X** is always at the end
|
||||||
|
|
||||||
|
|||||||
+34
-20
@@ -23,6 +23,7 @@ import qualified Data.Aeson.Key as K
|
|||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.Aeson.Types as JSON
|
import qualified Data.Aeson.Types as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Internal as BS
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
import qualified Data.Scientific as Sci
|
import qualified Data.Scientific as Sci
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
@@ -54,33 +55,42 @@ import Protolude
|
|||||||
|
|
||||||
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||||
-- JSON object of JWT claims.
|
-- JSON object of JWT claims.
|
||||||
parseToken :: AppConfig -> ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
||||||
parseToken _ "" _ = return JSON.emptyObject
|
parseToken _ Nothing _ = return JSON.emptyObject
|
||||||
parseToken AppConfig{..} token time = do
|
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeError "Empty JWT is sent in Authorization header"
|
||||||
secret <- liftEither . maybeToRight (JwtErr JwtTokenMissing) $ configJWKS
|
parseToken AppConfig{..} (Just tkn) time = do
|
||||||
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing token
|
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||||
|
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
||||||
|
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
|
||||||
content <- liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
content <- liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
||||||
liftEither $ mapLeft JwtErr $ verifyClaims content
|
liftEither $ mapLeft JwtErr $ verifyClaims content
|
||||||
where
|
where
|
||||||
-- TODO: Improve errors, those were just taken as-is from hs-jose to avoid
|
hasThreeParts :: ByteString -> Either Error ByteString
|
||||||
-- breaking changes.
|
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||||
|
3 -> Right token
|
||||||
|
n -> Left $ JwtErr $ JwtDecodeError ("Expected 3 parts in JWT; got " <> show n)
|
||||||
jwtDecodeError :: JWT.JwtError -> JwtError
|
jwtDecodeError :: JWT.JwtError -> JwtError
|
||||||
jwtDecodeError (JWT.KeyError _) = JwtTokenInvalid "JWSError JWSInvalidSignature"
|
-- The only errors we can get from JWT.decode function are:
|
||||||
jwtDecodeError JWT.BadCrypto = JwtTokenInvalid "JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)"
|
-- BadAlgorithm
|
||||||
jwtDecodeError (JWT.BadAlgorithm _) = JwtTokenInvalid "JWSError JWSNoSignatures"
|
-- KeyError
|
||||||
jwtDecodeError e = JwtTokenInvalid $ show e
|
-- BadCrypto
|
||||||
|
jwtDecodeError (JWT.KeyError _) = JwtDecodeError "No suitable key or wrong key type"
|
||||||
|
jwtDecodeError (JWT.BadAlgorithm _) = JwtDecodeError "Wrong or unsupported encoding algorithm"
|
||||||
|
jwtDecodeError JWT.BadCrypto = JwtDecodeError "JWT cryptographic operation failed"
|
||||||
|
-- Control never reaches here, the decode function only returns the above three
|
||||||
|
jwtDecodeError _ = JwtDecodeError "JWT couldn't be decoded"
|
||||||
|
|
||||||
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
||||||
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
||||||
Nothing -> Left $ JwtTokenInvalid "Parsing claims failed"
|
Nothing -> Left $ JwtClaimsError "Parsing claims failed"
|
||||||
Just (JSON.Object mclaims)
|
Just (JSON.Object mclaims)
|
||||||
| failedExpClaim mclaims -> Left $ JwtTokenInvalid "JWT expired"
|
| failedExpClaim mclaims -> Left $ JwtClaimsError "JWT expired"
|
||||||
| failedNbfClaim mclaims -> Left $ JwtTokenInvalid "JWTNotYetValid"
|
| failedNbfClaim mclaims -> Left $ JwtClaimsError "JWT not yet valid"
|
||||||
| failedIatClaim mclaims -> Left $ JwtTokenInvalid "JWTIssuedAtFuture"
|
| failedIatClaim mclaims -> Left $ JwtClaimsError "JWT issued at future"
|
||||||
| failedAudClaim mclaims -> Left $ JwtTokenInvalid "JWTNotInAudience"
|
| failedAudClaim mclaims -> Left $ JwtClaimsError "JWT not in audience"
|
||||||
Just jclaims -> Right jclaims
|
Just jclaims -> Right jclaims
|
||||||
-- TODO: We could enable JWE support here (encrypted tokens)
|
-- TODO: We could enable JWE support here (encrypted tokens)
|
||||||
verifyClaims _ = Left $ JwtTokenInvalid "Unsupported token type"
|
verifyClaims _ = Left $ JwtDecodeError "Unsupported token type"
|
||||||
|
|
||||||
allowedSkewSeconds = 30 :: Int64
|
allowedSkewSeconds = 30 :: Int64
|
||||||
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
||||||
@@ -148,7 +158,7 @@ middleware appState app req respond = do
|
|||||||
conf <- getConfig appState
|
conf <- getConfig appState
|
||||||
time <- getTime appState
|
time <- getTime appState
|
||||||
|
|
||||||
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||||
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf
|
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf
|
||||||
jwtCacheState = getJwtCacheState appState
|
jwtCacheState = getJwtCacheState appState
|
||||||
|
|
||||||
@@ -160,7 +170,9 @@ middleware appState app req respond = do
|
|||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||||
|
|
||||||
(True, maxLifetime) -> do
|
(True, maxLifetime) -> do
|
||||||
(dur, authResult) <- timeItT $ lookupJwtCache jwtCacheState token maxLifetime parseJwt time
|
(dur, authResult) <- timeItT $ case token of
|
||||||
|
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
|
||||||
|
Nothing -> parseJwt
|
||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||||
|
|
||||||
(False, 0) -> do
|
(False, 0) -> do
|
||||||
@@ -168,7 +180,9 @@ middleware appState app req respond = do
|
|||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||||
|
|
||||||
(False, maxLifetime) -> do
|
(False, maxLifetime) -> do
|
||||||
authResult <- lookupJwtCache jwtCacheState token maxLifetime parseJwt time
|
authResult <- case token of
|
||||||
|
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
|
||||||
|
Nothing -> parseJwt
|
||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||||
|
|
||||||
app req' respond
|
app req' respond
|
||||||
|
|||||||
+19
-10
@@ -577,9 +577,10 @@ data Error
|
|||||||
| PgErr PgError
|
| PgErr PgError
|
||||||
|
|
||||||
data JwtError
|
data JwtError
|
||||||
= JwtTokenInvalid Text
|
= JwtDecodeError Text
|
||||||
| JwtTokenMissing
|
| JwtSecretMissing
|
||||||
| JwtTokenRequired
|
| JwtTokenRequired
|
||||||
|
| JwtClaimsError Text
|
||||||
|
|
||||||
instance PgrstError Error where
|
instance PgrstError Error where
|
||||||
status (ApiRequestError err) = status err
|
status (ApiRequestError err) = status err
|
||||||
@@ -593,13 +594,15 @@ instance PgrstError Error where
|
|||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
instance PgrstError JwtError where
|
instance PgrstError JwtError where
|
||||||
status JwtTokenInvalid{} = HTTP.unauthorized401
|
status JwtDecodeError{} = HTTP.unauthorized401
|
||||||
status JwtTokenMissing = HTTP.status500
|
status JwtSecretMissing = HTTP.status500
|
||||||
status JwtTokenRequired = HTTP.unauthorized401
|
status JwtTokenRequired = HTTP.unauthorized401
|
||||||
|
status JwtClaimsError{} = HTTP.unauthorized401
|
||||||
|
|
||||||
headers (JwtTokenInvalid m) = [invalidTokenHeader m]
|
headers (JwtDecodeError m) = [invalidTokenHeader m]
|
||||||
headers JwtTokenRequired = [requiredTokenHeader]
|
headers JwtTokenRequired = [requiredTokenHeader]
|
||||||
headers _ = mempty
|
headers (JwtClaimsError m) = [invalidTokenHeader m]
|
||||||
|
headers _ = mempty
|
||||||
|
|
||||||
instance JSON.ToJSON Error where
|
instance JSON.ToJSON Error where
|
||||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||||
@@ -608,16 +611,20 @@ instance JSON.ToJSON Error where
|
|||||||
toJSON NoSchemaCacheError = toJsonPgrstError
|
toJSON NoSchemaCacheError = toJsonPgrstError
|
||||||
ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
|
ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
|
||||||
|
|
||||||
|
-- Should we provide hints and description or explain the error in docs or both?
|
||||||
instance JSON.ToJSON JwtError where
|
instance JSON.ToJSON JwtError where
|
||||||
toJSON JwtTokenMissing = toJsonPgrstError
|
toJSON JwtSecretMissing = toJsonPgrstError
|
||||||
JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
|
JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
|
||||||
|
|
||||||
toJSON (JwtTokenInvalid message) = toJsonPgrstError
|
toJSON (JwtDecodeError message) = toJsonPgrstError
|
||||||
JWTErrorCode01 message Nothing Nothing
|
JWTErrorCode01 message Nothing Nothing
|
||||||
|
|
||||||
toJSON JwtTokenRequired = toJsonPgrstError
|
toJSON JwtTokenRequired = toJsonPgrstError
|
||||||
JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
|
JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
|
||||||
|
|
||||||
|
toJSON (JwtClaimsError message) = toJsonPgrstError
|
||||||
|
JWTErrorCode03 message Nothing Nothing
|
||||||
|
|
||||||
|
|
||||||
invalidTokenHeader :: Text -> Header
|
invalidTokenHeader :: Text -> Header
|
||||||
invalidTokenHeader m =
|
invalidTokenHeader m =
|
||||||
@@ -710,6 +717,7 @@ data ErrorCode
|
|||||||
| JWTErrorCode00
|
| JWTErrorCode00
|
||||||
| JWTErrorCode01
|
| JWTErrorCode01
|
||||||
| JWTErrorCode02
|
| JWTErrorCode02
|
||||||
|
| JWTErrorCode03
|
||||||
-- Internal errors related to the Hasql library
|
-- Internal errors related to the Hasql library
|
||||||
| InternalErrorCode00
|
| InternalErrorCode00
|
||||||
|
|
||||||
@@ -758,5 +766,6 @@ buildErrorCode code = case code of
|
|||||||
JWTErrorCode00 -> "PGRST300"
|
JWTErrorCode00 -> "PGRST300"
|
||||||
JWTErrorCode01 -> "PGRST301"
|
JWTErrorCode01 -> "PGRST301"
|
||||||
JWTErrorCode02 -> "PGRST302"
|
JWTErrorCode02 -> "PGRST302"
|
||||||
|
JWTErrorCode03 -> "PGRST303"
|
||||||
|
|
||||||
InternalErrorCode00 -> "PGRSTX00"
|
InternalErrorCode00 -> "PGRSTX00"
|
||||||
|
|||||||
+38
-9
@@ -92,7 +92,7 @@ def test_jwt_errors(defaultenv):
|
|||||||
headers = jwtauthheader({}, "other secret")
|
headers = jwtauthheader({}, "other secret")
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWSError JWSInvalidSignature"
|
assert response.json()["message"] == "No suitable key or wrong key type"
|
||||||
|
|
||||||
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
@@ -110,27 +110,30 @@ def test_jwt_errors(defaultenv):
|
|||||||
headers = jwtauthheader({"nbf": relativeSeconds(31)}, SECRET)
|
headers = jwtauthheader({"nbf": relativeSeconds(31)}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWTNotYetValid"
|
assert response.json()["message"] == "JWT not yet valid"
|
||||||
|
|
||||||
# 31 seconds, because we allow clock skew of 30 seconds
|
# 31 seconds, because we allow clock skew of 30 seconds
|
||||||
headers = jwtauthheader({"iat": relativeSeconds(31)}, SECRET)
|
headers = jwtauthheader({"iat": relativeSeconds(31)}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWTIssuedAtFuture"
|
assert response.json()["message"] == "JWT issued at future"
|
||||||
|
|
||||||
headers = jwtauthheader({"aud": "not set"}, SECRET)
|
headers = jwtauthheader({"aud": "not set"}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWTNotInAudience"
|
assert response.json()["message"] == "JWT not in audience"
|
||||||
|
|
||||||
# partial token, no signature
|
# partial token, no signature
|
||||||
headers = authheader("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bm90IGFuIG9iamVjdA")
|
headers = authheader("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bm90IGFuIG9iamVjdA")
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert (
|
assert response.json()["message"] == "Expected 3 parts in JWT; got 2"
|
||||||
response.json()["message"]
|
|
||||||
== "JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; 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"
|
# token with algorithm "none"
|
||||||
headers = authheader(
|
headers = authheader(
|
||||||
@@ -138,7 +141,33 @@ def test_jwt_errors(defaultenv):
|
|||||||
)
|
)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json()["message"] == "JWSError JWSNoSignatures"
|
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_SERVER_TIMING_ENABLED": "true",
|
||||||
|
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400",
|
||||||
|
"PGRST_JWT_SECRET": SECRET,
|
||||||
|
}
|
||||||
|
|
||||||
|
# for code coverage with cache enabled and server-timing enabled
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
response = postgrest.session.get("/authors_only")
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json()["message"] == "permission denied for table authors_only"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_SERVER_TIMING_ENABLED": "false",
|
||||||
|
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400",
|
||||||
|
"PGRST_JWT_SECRET": SECRET,
|
||||||
|
}
|
||||||
|
|
||||||
|
# for code coverage with cache enabled and server-timing disabled
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
response = postgrest.session.get("/authors_only")
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json()["message"] == "permission denied for table authors_only"
|
||||||
|
|
||||||
|
|
||||||
def test_fail_with_invalid_password(defaultenv):
|
def test_fail_with_invalid_password(defaultenv):
|
||||||
|
|||||||
@@ -85,10 +85,21 @@ spec = describe "authorization" $ do
|
|||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
|
it "fails when auth header is sent empty" $ do
|
||||||
|
let auth = authHeaderJWT ""
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` [json| {"message":"Empty JWT is sent in Authorization header","code":"PGRST301","hint":null,"details":null} |]
|
||||||
|
{ matchStatus = 401
|
||||||
|
, matchHeaders = [
|
||||||
|
"WWW-Authenticate" <:>
|
||||||
|
"Bearer error=\"invalid_token\", error_description=\"Empty JWT is sent in Authorization header\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
it "fails with an expired token" $ do
|
it "fails with an expired token" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` [json| {"message":"JWT expired","code":"PGRST301","hint":null,"details":null} |]
|
`shouldRespondWith` [json| {"message":"JWT expired","code":"PGRST303","hint":null,"details":null} |]
|
||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
, matchHeaders = [
|
, matchHeaders = [
|
||||||
"WWW-Authenticate" <:>
|
"WWW-Authenticate" <:>
|
||||||
@@ -99,11 +110,11 @@ spec = describe "authorization" $ do
|
|||||||
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` [json| {"message":"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)","code":"PGRST301","hint":null,"details":null} |]
|
`shouldRespondWith` [json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |]
|
||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
, matchHeaders = [
|
, matchHeaders = [
|
||||||
"WWW-Authenticate" <:>
|
"WWW-Authenticate" <:>
|
||||||
"Bearer error=\"invalid_token\", error_description=\"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)\""
|
"Bearer error=\"invalid_token\", error_description=\"Expected 3 parts in JWT; got 2\""
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user