Support asymmetric JWK (#919)
This commit is contained in:
@@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- #567, Support more JWT signing algorithms, including RSA - @begriffs
|
||||||
- #889, Allow more than two conditions in a single and/or - @steve-chavez
|
- #889, Allow more than two conditions in a single and/or - @steve-chavez
|
||||||
- #883, Binary output support for RPC - @steve-chavez
|
- #883, Binary output support for RPC - @steve-chavez
|
||||||
- #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui
|
- #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui
|
||||||
|
|||||||
+1
-8
@@ -13,8 +13,6 @@ import PostgREST.OpenAPI (isMalformedProxyUri)
|
|||||||
import PostgREST.Types (DbStructure, Schema)
|
import PostgREST.Types (DbStructure, Schema)
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings,
|
|
||||||
mkAutoUpdate, updateAction)
|
|
||||||
import Control.Retry (RetryStatus, capDelay,
|
import Control.Retry (RetryStatus, capDelay,
|
||||||
exponentialBackoff,
|
exponentialBackoff,
|
||||||
retrying, rsPreviousDelay)
|
retrying, rsPreviousDelay)
|
||||||
@@ -25,7 +23,6 @@ import Data.String (IsString (..))
|
|||||||
import Data.Text (pack, replace, stripPrefix, strip)
|
import Data.Text (pack, replace, stripPrefix, strip)
|
||||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||||
import Data.Text.IO (hPutStrLn, readFile)
|
import Data.Text.IO (hPutStrLn, readFile)
|
||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as P
|
||||||
@@ -218,10 +215,7 @@ main = do
|
|||||||
refIsWorkerOn
|
refIsWorkerOn
|
||||||
) Nothing
|
) Nothing
|
||||||
#endif
|
#endif
|
||||||
--
|
|
||||||
-- ask for the OS time at most once per second
|
|
||||||
getTime <-
|
|
||||||
mkAutoUpdate defaultUpdateSettings {updateAction = getPOSIXTime}
|
|
||||||
--
|
--
|
||||||
-- run the postgrest application
|
-- run the postgrest application
|
||||||
runSettings appSettings $
|
runSettings appSettings $
|
||||||
@@ -229,7 +223,6 @@ main = do
|
|||||||
conf
|
conf
|
||||||
refDbStructure
|
refDbStructure
|
||||||
pool
|
pool
|
||||||
getTime
|
|
||||||
(connectionWorker
|
(connectionWorker
|
||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
|
|||||||
+4
-7
@@ -29,14 +29,12 @@ executable postgrest
|
|||||||
-rtsopts
|
-rtsopts
|
||||||
"-with-rtsopts=-N -I2"
|
"-with-rtsopts=-N -I2"
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
build-depends: auto-update
|
build-depends: base
|
||||||
, base
|
|
||||||
, hasql
|
, hasql
|
||||||
, hasql-pool
|
, hasql-pool
|
||||||
, postgrest
|
, postgrest
|
||||||
, protolude
|
, protolude
|
||||||
, text
|
, text
|
||||||
, time
|
|
||||||
, warp
|
, warp
|
||||||
, bytestring
|
, bytestring
|
||||||
, base64-bytestring
|
, base64-bytestring
|
||||||
@@ -52,6 +50,7 @@ library
|
|||||||
build-depends: aeson
|
build-depends: aeson
|
||||||
, ansi-wl-pprint
|
, ansi-wl-pprint
|
||||||
, base >= 4.8 && < 6
|
, base >= 4.8 && < 6
|
||||||
|
, base64-bytestring
|
||||||
, bytestring
|
, bytestring
|
||||||
, case-insensitive
|
, case-insensitive
|
||||||
, cassava
|
, cassava
|
||||||
@@ -67,7 +66,7 @@ library
|
|||||||
, http-types
|
, http-types
|
||||||
, insert-ordered-containers
|
, insert-ordered-containers
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jose
|
||||||
, lens
|
, lens
|
||||||
, lens-aeson
|
, lens-aeson
|
||||||
, network-uri
|
, network-uri
|
||||||
@@ -80,7 +79,6 @@ library
|
|||||||
, scientific
|
, scientific
|
||||||
, swagger2
|
, swagger2
|
||||||
, text
|
, text
|
||||||
, time
|
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, vector
|
, vector
|
||||||
, wai
|
, wai
|
||||||
@@ -113,6 +111,7 @@ Test-Suite spec
|
|||||||
Hs-Source-Dirs: test
|
Hs-Source-Dirs: test
|
||||||
Main-Is: Main.hs
|
Main-Is: Main.hs
|
||||||
Other-Modules: Feature.AuthSpec
|
Other-Modules: Feature.AuthSpec
|
||||||
|
, Feature.AsymmetricJwtSpec
|
||||||
, Feature.BinaryJwtSecretSpec
|
, Feature.BinaryJwtSecretSpec
|
||||||
, Feature.ConcurrentSpec
|
, Feature.ConcurrentSpec
|
||||||
, Feature.CorsSpec
|
, Feature.CorsSpec
|
||||||
@@ -132,7 +131,6 @@ Test-Suite spec
|
|||||||
Build-Depends: aeson
|
Build-Depends: aeson
|
||||||
, aeson-qq
|
, aeson-qq
|
||||||
, async
|
, async
|
||||||
, auto-update
|
|
||||||
, base
|
, base
|
||||||
, bytestring
|
, bytestring
|
||||||
, base64-bytestring
|
, base64-bytestring
|
||||||
@@ -156,7 +154,6 @@ Test-Suite spec
|
|||||||
, process
|
, process
|
||||||
, protolude
|
, protolude
|
||||||
, regex-tdfa
|
, regex-tdfa
|
||||||
, time
|
|
||||||
, transformers-base
|
, transformers-base
|
||||||
, wai
|
, wai
|
||||||
, wai-extra
|
, wai-extra
|
||||||
|
|||||||
+9
-12
@@ -11,7 +11,6 @@ import qualified Data.ByteString.Char8 as BS
|
|||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.IORef (IORef, readIORef)
|
import Data.IORef (IORef, readIORef)
|
||||||
import Data.Text (intercalate)
|
import Data.Text (intercalate)
|
||||||
import Data.Time.Clock.POSIX (POSIXTime)
|
|
||||||
|
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as P
|
||||||
import qualified Hasql.Transaction as HT
|
import qualified Hasql.Transaction as HT
|
||||||
@@ -22,7 +21,6 @@ import Network.HTTP.Types.Status
|
|||||||
import Network.HTTP.Types.URI (renderSimpleQuery)
|
import Network.HTTP.Types.URI (renderSimpleQuery)
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Network.Wai.Middleware.RequestLogger (logStdout)
|
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||||
import Web.JWT (binarySecret)
|
|
||||||
|
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import qualified Hasql.Transaction as H
|
import qualified Hasql.Transaction as H
|
||||||
@@ -35,7 +33,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..)
|
|||||||
, mutuallyAgreeable
|
, mutuallyAgreeable
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
)
|
)
|
||||||
import PostgREST.Auth (jwtClaims, containsRole)
|
import PostgREST.Auth (jwtClaims, containsRole, parseJWK)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
import PostgREST.DbRequestBuilder( readRequest
|
import PostgREST.DbRequestBuilder( readRequest
|
||||||
@@ -63,13 +61,12 @@ import Data.Function (id)
|
|||||||
import Protolude hiding (intercalate, Proxy)
|
import Protolude hiding (intercalate, Proxy)
|
||||||
import Safe (headMay)
|
import Safe (headMay)
|
||||||
|
|
||||||
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO POSIXTime ->
|
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO () -> Application
|
||||||
IO () -> Application
|
postgrest conf refDbStructure pool worker =
|
||||||
postgrest conf refDbStructure pool getTime worker =
|
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
|
||||||
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
|
jwtSecret = parseJWK <$> configJwtSecret conf in
|
||||||
|
|
||||||
middle $ \ req respond -> do
|
middle $ \ req respond -> do
|
||||||
time <- getTime
|
|
||||||
body <- strictRequestBody req
|
body <- strictRequestBody req
|
||||||
maybeDbStructure <- readIORef refDbStructure
|
maybeDbStructure <- readIORef refDbStructure
|
||||||
case maybeDbStructure of
|
case maybeDbStructure of
|
||||||
@@ -78,9 +75,9 @@ postgrest conf refDbStructure pool getTime worker =
|
|||||||
response <- case userApiRequest (configSchema conf) req body of
|
response <- case userApiRequest (configSchema conf) req body of
|
||||||
Left err -> return $ apiRequestError err
|
Left err -> return $ apiRequestError err
|
||||||
Right apiRequest -> do
|
Right apiRequest -> do
|
||||||
let jwtSecret = binarySecret <$> configJwtSecret conf
|
eClaims <- jwtClaims jwtSecret (toS $ iJWT apiRequest)
|
||||||
eClaims = jwtClaims jwtSecret (iJWT apiRequest) time
|
|
||||||
authed = containsRole eClaims
|
let authed = containsRole eClaims
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest
|
||||||
txMode = transactionMode dbStructure
|
txMode = transactionMode dbStructure
|
||||||
(iTarget apiRequest) (iAction apiRequest)
|
(iTarget apiRequest) (iAction apiRequest)
|
||||||
@@ -324,7 +321,7 @@ responseContentTypeOrError accepts action = serves contentTypesForRequest accept
|
|||||||
case mutuallyAgreeable sProduces cAccepts of
|
case mutuallyAgreeable sProduces cAccepts of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
let failed = intercalate ", " $ map (toS . toMime) cAccepts
|
let failed = intercalate ", " $ map (toS . toMime) cAccepts
|
||||||
Left $ simpleError status415 $
|
Left $ simpleError status415 [] $
|
||||||
"None of these Content-Types are available: " <> failed
|
"None of these Content-Types are available: " <> failed
|
||||||
Just ct -> Right ct
|
Just ct -> Right ct
|
||||||
|
|
||||||
|
|||||||
+51
-39
@@ -14,63 +14,48 @@ very simple authentication system inside the PostgreSQL database.
|
|||||||
module PostgREST.Auth (
|
module PostgREST.Auth (
|
||||||
containsRole
|
containsRole
|
||||||
, jwtClaims
|
, jwtClaims
|
||||||
, tokenJWT
|
|
||||||
, JWTAttempt(..)
|
, JWTAttempt(..)
|
||||||
|
, parseJWK
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Protolude
|
import Protolude hiding ((&))
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Data.Aeson (Value (..), parseJSON, toJSON)
|
import Data.Aeson (Value (..), decode, toJSON)
|
||||||
import Data.Aeson.Lens
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
|
|
||||||
import qualified Data.Vector as V
|
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.Maybe (fromJust)
|
|
||||||
import Data.Time.Clock (NominalDiffTime)
|
|
||||||
import qualified Web.JWT as JWT
|
|
||||||
|
|
||||||
|
import Crypto.JOSE.Compact
|
||||||
|
import Crypto.JOSE.JWK
|
||||||
|
import Crypto.JOSE.JWS
|
||||||
|
import Crypto.JOSE.Types
|
||||||
|
import Crypto.JWT
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Possible situations encountered with client JWTs
|
Possible situations encountered with client JWTs
|
||||||
-}
|
-}
|
||||||
data JWTAttempt = JWTExpired
|
data JWTAttempt = JWTInvalid JWTError
|
||||||
| JWTInvalid
|
|
||||||
| JWTMissingSecret
|
| JWTMissingSecret
|
||||||
| JWTClaims (M.HashMap Text Value)
|
| JWTClaims (M.HashMap Text Value)
|
||||||
deriving Eq
|
deriving (Eq, Show)
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Receives the JWT secret (from config) and a JWT and returns a map
|
Receives the JWT secret (from config) and a JWT and returns a map
|
||||||
of JWT claims.
|
of JWT claims.
|
||||||
-}
|
-}
|
||||||
jwtClaims :: Maybe JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt
|
jwtClaims :: Maybe JWK -> BL.ByteString -> IO JWTAttempt
|
||||||
jwtClaims _ "" _ = JWTClaims M.empty
|
jwtClaims _ "" = return $ JWTClaims M.empty
|
||||||
jwtClaims secret jwt time =
|
jwtClaims secret payload =
|
||||||
case secret of
|
case secret of
|
||||||
Nothing -> JWTMissingSecret
|
Nothing -> return JWTMissingSecret
|
||||||
Just s ->
|
Just jwk -> do
|
||||||
let mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature s jwt in
|
let validation = defaultJWTValidationSettings
|
||||||
case isExpired <$> mClaims of
|
eJwt <- runExceptT $ do
|
||||||
Just True -> JWTExpired
|
jwt <- decodeCompact payload
|
||||||
Nothing -> JWTInvalid
|
validateJWSJWT validation jwk jwt
|
||||||
Just False -> JWTClaims $ value2map $ fromJust mClaims
|
return jwt
|
||||||
where
|
return $ case eJwt of
|
||||||
isExpired claims =
|
Left e -> JWTInvalid e
|
||||||
let mExp = claims ^? key "exp" . _Integer
|
Right jwt -> JWTClaims . claims2map . jwtClaimsSet $ jwt
|
||||||
in fromMaybe False $ (<= time) . fromInteger <$> mExp
|
|
||||||
value2map (Object o) = o
|
|
||||||
value2map _ = M.empty
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Receives the JWT secret (from config) and a JWT and a JSON value
|
|
||||||
and returns a signed JWT.
|
|
||||||
-}
|
|
||||||
tokenJWT :: JWT.Secret -> Value -> Text
|
|
||||||
tokenJWT secret (Array arr) =
|
|
||||||
let obj = if V.null arr then emptyObject else V.head arr
|
|
||||||
jcs = parseMaybe parseJSON obj :: Maybe JWT.JWTClaimsSet in
|
|
||||||
JWT.encodeSigned JWT.HS256 secret $ fromMaybe JWT.def jcs
|
|
||||||
tokenJWT secret _ = tokenJWT secret emptyArray
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Whether a response from jwtClaims contains a role claim
|
Whether a response from jwtClaims contains a role claim
|
||||||
@@ -78,3 +63,30 @@ tokenJWT secret _ = tokenJWT secret emptyArray
|
|||||||
containsRole :: JWTAttempt -> Bool
|
containsRole :: JWTAttempt -> Bool
|
||||||
containsRole (JWTClaims claims) = M.member "role" claims
|
containsRole (JWTClaims claims) = M.member "role" claims
|
||||||
containsRole _ = False
|
containsRole _ = False
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Internal helper used to turn JWT ClaimSet into something
|
||||||
|
easier to work with
|
||||||
|
-}
|
||||||
|
claims2map :: ClaimsSet -> M.HashMap Text Value
|
||||||
|
claims2map = val2map . toJSON
|
||||||
|
where
|
||||||
|
val2map (Object o) = o
|
||||||
|
val2map _ = M.empty
|
||||||
|
|
||||||
|
parseJWK :: ByteString -> JWK
|
||||||
|
parseJWK str =
|
||||||
|
fromMaybe (hs256jwk str) (decode (toS str) :: Maybe JWK)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Internal helper to generate HMAC-SHA256. When the jwt key in the
|
||||||
|
config file is a simple string rather than a JWK object, we'll
|
||||||
|
apply this function to it.
|
||||||
|
-}
|
||||||
|
hs256jwk :: ByteString -> JWK
|
||||||
|
hs256jwk key =
|
||||||
|
fromKeyMaterial km
|
||||||
|
& jwkUse .~ Just Sig
|
||||||
|
& jwkAlg .~ (Just $ JWSAlg HS256)
|
||||||
|
where
|
||||||
|
km = OctKeyMaterial (OctKeyParameters Oct (Base64Octets key))
|
||||||
|
|||||||
+13
-9
@@ -18,12 +18,15 @@ import qualified Data.Aeson as JSON
|
|||||||
import Data.Text (unwords)
|
import Data.Text (unwords)
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as P
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
|
import Network.HTTP.Types.Header
|
||||||
import qualified Network.HTTP.Types.Status as HT
|
import qualified Network.HTTP.Types.Status as HT
|
||||||
import Network.Wai (Response, responseLBS)
|
import Network.Wai (Response, responseLBS)
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
|
|
||||||
apiRequestError :: ApiRequestError -> Response
|
apiRequestError :: ApiRequestError -> Response
|
||||||
apiRequestError err = errorResponse status err
|
apiRequestError err =
|
||||||
|
errorResponse status
|
||||||
|
[toHeader CTApplicationJSON] err
|
||||||
where
|
where
|
||||||
status =
|
status =
|
||||||
case err of
|
case err of
|
||||||
@@ -35,13 +38,14 @@ apiRequestError err = errorResponse status err
|
|||||||
InvalidRange -> HT.status416
|
InvalidRange -> HT.status416
|
||||||
UnknownRelation -> HT.status404
|
UnknownRelation -> HT.status404
|
||||||
|
|
||||||
simpleError :: HT.Status -> Text -> Response
|
simpleError :: HT.Status -> [Header] -> Text -> Response
|
||||||
simpleError status message =
|
simpleError status hdrs message =
|
||||||
errorResponse status $ JSON.object ["message" .= message]
|
errorResponse status (toHeader CTApplicationJSON : hdrs) $
|
||||||
|
JSON.object ["message" .= message]
|
||||||
|
|
||||||
errorResponse :: JSON.ToJSON a => HT.Status -> a -> Response
|
errorResponse :: JSON.ToJSON a => HT.Status -> [Header] -> a -> Response
|
||||||
errorResponse status e =
|
errorResponse status hdrs e =
|
||||||
responseLBS status [toHeader CTApplicationJSON] $ encodeError e
|
responseLBS status hdrs $ encodeError e
|
||||||
|
|
||||||
pgError :: Bool -> P.UsageError -> Response
|
pgError :: Bool -> P.UsageError -> Response
|
||||||
pgError authed e =
|
pgError authed e =
|
||||||
@@ -71,12 +75,12 @@ singularityError numRows =
|
|||||||
|
|
||||||
binaryFieldError :: Response
|
binaryFieldError :: Response
|
||||||
binaryFieldError =
|
binaryFieldError =
|
||||||
simpleError HT.status406 (toS (toMime CTOctetStream) <>
|
simpleError HT.status406 [] (toS (toMime CTOctetStream) <>
|
||||||
" requested but a single column was not selected")
|
" requested but a single column was not selected")
|
||||||
|
|
||||||
connectionLostError :: Response
|
connectionLostError :: Response
|
||||||
connectionLostError =
|
connectionLostError =
|
||||||
simpleError HT.status503 "Database connection lost, retrying the connection."
|
simpleError HT.status503 [] "Database connection lost, retrying the connection."
|
||||||
|
|
||||||
encodeError :: JSON.ToJSON a => a -> LByteString
|
encodeError :: JSON.ToJSON a => a -> LByteString
|
||||||
encodeError = JSON.encode
|
encodeError = JSON.encode
|
||||||
|
|||||||
+11
-13
@@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
module PostgREST.Middleware where
|
module PostgREST.Middleware where
|
||||||
|
|
||||||
|
import Crypto.JWT
|
||||||
import Data.Aeson (Value (..))
|
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 (unauthorized401, status500)
|
import Network.HTTP.Types.Status (unauthorized401, status500)
|
||||||
import Network.Wai (Application, Response,
|
import Network.Wai (Application, Response)
|
||||||
responseLBS)
|
|
||||||
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)
|
||||||
@@ -19,7 +19,6 @@ import PostgREST.ApiRequest (ApiRequest(..))
|
|||||||
import PostgREST.Auth (JWTAttempt(..))
|
import PostgREST.Auth (JWTAttempt(..))
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
import PostgREST.Error (simpleError)
|
import PostgREST.Error (simpleError)
|
||||||
import PostgREST.Types (ContentType (..), toHeader)
|
|
||||||
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar)
|
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar)
|
||||||
|
|
||||||
import Protolude hiding (concat, null)
|
import Protolude hiding (concat, null)
|
||||||
@@ -29,9 +28,9 @@ runWithClaims :: AppConfig -> JWTAttempt ->
|
|||||||
ApiRequest -> H.Transaction Response
|
ApiRequest -> H.Transaction Response
|
||||||
runWithClaims conf eClaims app req =
|
runWithClaims conf eClaims app req =
|
||||||
case eClaims of
|
case eClaims of
|
||||||
JWTExpired -> return $ unauthed "JWT expired"
|
JWTInvalid JWTExpired -> return $ unauthed "JWT expired"
|
||||||
JWTInvalid -> return $ unauthed "JWT invalid"
|
JWTInvalid e -> return $ unauthed $ show e
|
||||||
JWTMissingSecret -> return $ simpleError status500 "Server lacks JWT secret"
|
JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
|
||||||
JWTClaims claims -> do
|
JWTClaims claims -> do
|
||||||
H.sql $ toS.mconcat $ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql
|
H.sql $ toS.mconcat $ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql
|
||||||
mapM_ H.sql customReqCheck
|
mapM_ H.sql customReqCheck
|
||||||
@@ -47,14 +46,13 @@ runWithClaims conf eClaims app req =
|
|||||||
anon = String . toS $ configAnonRole conf
|
anon = String . toS $ configAnonRole conf
|
||||||
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
|
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
|
||||||
where
|
where
|
||||||
unauthed message = responseLBS unauthorized401
|
unauthed message = simpleError
|
||||||
[ toHeader CTApplicationJSON
|
unauthorized401
|
||||||
, ( "WWW-Authenticate"
|
[( "WWW-Authenticate"
|
||||||
, "Bearer error=\"invalid_token\", " <>
|
, "Bearer error=\"invalid_token\", " <>
|
||||||
"error_description=\"" <> message <> "\""
|
"error_description=" <> show message
|
||||||
)
|
)]
|
||||||
]
|
message
|
||||||
(toS $ "{\"message\":\""<>message<>"\"}")
|
|
||||||
|
|
||||||
defaultMiddle :: Application -> Application
|
defaultMiddle :: Application -> Application
|
||||||
defaultMiddle =
|
defaultMiddle =
|
||||||
|
|||||||
+3
-2
@@ -1,9 +1,10 @@
|
|||||||
resolver: lts-8.5
|
resolver: lts-8.5
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- Ranged-sets-0.3.0
|
|
||||||
- hasql-pool-0.4.1
|
|
||||||
- configurator-ng-0.0.0.1
|
- configurator-ng-0.0.0.1
|
||||||
- critbit-0.2.0.0
|
- critbit-0.2.0.0
|
||||||
|
- hasql-pool-0.4.1
|
||||||
|
- jose-0.5.0.3
|
||||||
|
- Ranged-sets-0.3.0
|
||||||
ghc-options:
|
ghc-options:
|
||||||
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
|
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
|
||||||
nix:
|
nix:
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module Feature.AsymmetricJwtSpec where
|
||||||
|
|
||||||
|
-- {{{ Imports
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
-- }}}
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = describe "server started with asymmetric JWK" $
|
||||||
|
|
||||||
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
|
it "succeeds with jwt token signed with an asymmetric key" $ do
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJSUzI1NiJ9.eyJyb2xlIjogInBvc3RncmVzdF90ZXN0X2F1dGhvciJ9Cg.CBOYWDvqgAR0YYnZnyDGTQi6AJLc2Pds6_eV3YuBG6I36mj_h05eLhkEKNEDA5ZteMzCiY83P60rC_xtxVd7B6vo3BeF5uoanPS3rrbuHzKPwzsrgrD_CqvEuJ4n7Q9epkQiLsNkcexneENZDRqFjbwZx3DrXiCWwlK3Ytr5NAIGxmy0od-0xNpb2U1nXQyO_Q3mumWFViRt4tmFn_3goDHNKG3Ha_AzImfUNvHnWL78kAc4rbn15vLtWXD8PwtSnZaB4lY4V6RfsaW937srQsmRetvytM1i_bHBnjkjQLAqGbXPyItjtlXPs0uGNBadE8-wgkLtfmSCC4v2DjUthw"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith` 200
|
||||||
+17
-17
@@ -28,7 +28,7 @@ spec = describe "authorization" $ do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "denies access to tables that postgrest_test_author does not own" $
|
it "denies access to tables that postgrest_test_author does not own" $
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" in
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in
|
||||||
request methodGet "/private_table" [auth] ""
|
request methodGet "/private_table" [auth] ""
|
||||||
`shouldRespondWith` [json| {
|
`shouldRespondWith` [json| {
|
||||||
"hint":null,
|
"hint":null,
|
||||||
@@ -42,41 +42,41 @@ spec = describe "authorization" $ do
|
|||||||
it "returns jwt functions as jwt tokens" $
|
it "returns jwt functions as jwt tokens" $
|
||||||
request methodPost "/rpc/login" [single]
|
request methodPost "/rpc/login" [single]
|
||||||
[json| { "id": "jdoe", "pass": "1234" } |]
|
[json| { "id": "jdoe", "pass": "1234" } |]
|
||||||
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |]
|
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs"} |]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [matchContentTypeSingular]
|
, matchHeaders = [matchContentTypeSingular]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "sql functions can encode custom and standard claims" $
|
it "sql functions can encode custom and standard claims" $
|
||||||
request methodPost "/rpc/jwt_test" [single] "{}"
|
request methodPost "/rpc/jwt_test" [single] "{}"
|
||||||
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |]
|
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek"} |]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [matchContentTypeSingular]
|
, matchHeaders = [matchContentTypeSingular]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "sql functions can read custom and standard claims variables" $ do
|
it "sql functions can read custom and standard claims variables" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwLCJhdWQiOiJldmVyeW9uZSJ9.AQmCA7CMScvfaDRMqRPeUY6eNf--69gpW-kxaWfq9X0"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwfQ.V5fEpXfpb7feqwVqlcDleFdKu86bdwU2cBRT4fcMhXg"
|
||||||
request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
|
request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
|
||||||
`shouldRespondWith` [str|[{"iss":"joe","sub":"fun","aud":"everyone","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|]
|
`shouldRespondWith` [str|[{"iss":"joe","sub":"fun","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|]
|
||||||
|
|
||||||
it "allows users with permissions to see their tables" $ do
|
it "allows users with permissions to see their tables" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
it "works with tokens which have extra fields" $ do
|
it "works with tokens which have extra fields" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.GfydCh-F4wnM379xs0n1zUgalwJIsb6YoBapCo8HlFk"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.b0eglDKYEmGi-hCvD-ddSqFl7vnDO5qkUaviaHXm3es"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
-- this test will stop working 9999999999s after the UNIX EPOCH
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
it "succeeds with an unexpired token" $ do
|
it "succeeds with an unexpired token" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
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.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` [json| {"message":"JWT expired"} |]
|
`shouldRespondWith` [json| {"message":"JWT expired"} |]
|
||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
@@ -89,27 +89,27 @@ 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":"JWT invalid"} |]
|
`shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError \"expected 3 parts, got 2\")"} |]
|
||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
, matchHeaders = [
|
, matchHeaders = [
|
||||||
"WWW-Authenticate" <:>
|
"WWW-Authenticate" <:>
|
||||||
"Bearer error=\"invalid_token\", error_description=\"JWT invalid\""
|
"Bearer error=\"invalid_token\", error_description=\"JWSError (CompactDecodeError \\\"expected 3 parts, got 2\\\")\""
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "should fail when jwt contains no claims" $ do
|
it "should fail when jwt contains no claims" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.lu-rG8aSCiw-aOlN0IxpRGz5r7Jwq7K9r3tuMPUpytI"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 401
|
`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.Jneso9X519Vh0z7i9PbXIu7W1HEoq9RRw9BBbyQKFCQ"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.RVlZDaSyKbFPvxUf3V_NQXybfRB4dlBIkAUQXVXLUAI"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 401
|
`shouldRespondWith` 401
|
||||||
|
|
||||||
it "recovers after 401 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.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
|
||||||
_ <- request methodPost "/rpc/problem" [auth] ""
|
_ <- request methodPost "/rpc/problem" [auth] ""
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
@@ -117,7 +117,7 @@ spec = describe "authorization" $ do
|
|||||||
describe "custom pre-request proc acting on id claim" $ do
|
describe "custom pre-request proc acting on id claim" $ do
|
||||||
|
|
||||||
it "able to switch to postgrest_test_author role (id=1)" $
|
it "able to switch to postgrest_test_author role (id=1)" $
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.mI2HNoOum6xM3sc4oHLxU4yLv-_WV5W1kqBfY_wEvLw" in
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.gKw7qI50i9hMrSJW8BlTpdMEVmMXJYxlAqueGqpa_mE" in
|
||||||
request methodPost "/rpc/get_current_user" [auth]
|
request methodPost "/rpc/get_current_user" [auth]
|
||||||
[json| {} |]
|
[json| {} |]
|
||||||
`shouldRespondWith` [str|"postgrest_test_author"|]
|
`shouldRespondWith` [str|"postgrest_test_author"|]
|
||||||
@@ -126,7 +126,7 @@ spec = describe "authorization" $ do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "able to switch to postgrest_test_default_role (id=2)" $
|
it "able to switch to postgrest_test_default_role (id=2)" $
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.W7jLsG-zswM91AJkCvZeIMHrnz7_6ceY2jnscVl3Yhk" in
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.nwzjMI0YLvVGJQTeoCPEBsK983b__gxdpLXisBNaO2A" in
|
||||||
request methodPost "/rpc/get_current_user" [auth]
|
request methodPost "/rpc/get_current_user" [auth]
|
||||||
[json| {} |]
|
[json| {} |]
|
||||||
`shouldRespondWith` [str|"postgrest_test_default_role"|]
|
`shouldRespondWith` [str|"postgrest_test_default_role"|]
|
||||||
@@ -135,7 +135,7 @@ spec = describe "authorization" $ do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "raises error (id=3)" $
|
it "raises error (id=3)" $
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.15Gy8PezQhJIaHYDJVLa-Gmz9T3sJnW66EKAYIsXc7c" in
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.OGxEJAf60NKZiTn-tIb2jy4rqKs_ZruLGWZ40TjrJsM" in
|
||||||
request methodPost "/rpc/get_current_user" [auth]
|
request methodPost "/rpc/get_current_user" [auth]
|
||||||
[json| {} |]
|
[json| {} |]
|
||||||
`shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]
|
`shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ spec = describe "server started with binary JWT secret" $
|
|||||||
|
|
||||||
-- this test will stop working 9999999999s after the UNIX EPOCH
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
it "succeeds with jwt token encoded with a binary secret" $ do
|
it "succeeds with jwt token encoded with a binary secret" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.l_EcSRWeNtL4OKUTIplrHyioNrff9Rd0MV7RXNCxCyk"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ spec = do
|
|||||||
|
|
||||||
describe "Row level permission" $
|
describe "Row level permission" $
|
||||||
it "set user_id when inserting rows" $ do
|
it "set user_id when inserting rows" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
|
||||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||||
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
|
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||||
|
|
||||||
@@ -464,7 +464,7 @@ spec = do
|
|||||||
|
|
||||||
p2 <- request methodPost "/authors_only"
|
p2 <- request methodPost "/authors_only"
|
||||||
-- jwt token for jroe
|
-- jwt token for jroe
|
||||||
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ]
|
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ]
|
||||||
[json| { "secret": "lolcat", "owner": "hacker" } |]
|
[json| { "secret": "lolcat", "owner": "hacker" } |]
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|]
|
simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ spec = describe "server started without JWT secret" $ do
|
|||||||
|
|
||||||
-- this test will stop working 9999999999s after the UNIX EPOCH
|
-- this test will stop working 9999999999s after the UNIX EPOCH
|
||||||
it "responds with error on attempted auth" $ do
|
it "responds with error on attempted auth" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 500
|
`shouldRespondWith` 500
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -7,12 +7,11 @@ import qualified Hasql.Pool as P
|
|||||||
|
|
||||||
import PostgREST.DbStructure (getDbStructure)
|
import PostgREST.DbStructure (getDbStructure)
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import Control.AutoUpdate
|
|
||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
|
||||||
|
|
||||||
import qualified Feature.AuthSpec
|
import qualified Feature.AuthSpec
|
||||||
|
import qualified Feature.AsymmetricJwtSpec
|
||||||
import qualified Feature.BinaryJwtSecretSpec
|
import qualified Feature.BinaryJwtSecretSpec
|
||||||
import qualified Feature.ConcurrentSpec
|
import qualified Feature.ConcurrentSpec
|
||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
@@ -36,19 +35,16 @@ main = do
|
|||||||
setupDb testDbConn
|
setupDb testDbConn
|
||||||
|
|
||||||
pool <- P.acquire (3, 10, toS testDbConn)
|
pool <- P.acquire (3, 10, toS testDbConn)
|
||||||
-- ask for the OS time at most once per second
|
|
||||||
getTime <- mkAutoUpdate
|
|
||||||
defaultUpdateSettings { updateAction = getPOSIXTime }
|
|
||||||
|
|
||||||
|
|
||||||
result <- P.use pool $ getDbStructure "test"
|
result <- P.use pool $ getDbStructure "test"
|
||||||
refDbStructure <- newIORef $ Just $ either (panic.show) id result
|
refDbStructure <- newIORef $ Just $ either (panic.show) id result
|
||||||
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
|
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
|
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
|
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
|
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
|
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool $ pure ()
|
||||||
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
|
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool $ pure ()
|
||||||
|
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure ()
|
||||||
|
|
||||||
let reset = resetDb testDbConn
|
let reset = resetDb testDbConn
|
||||||
hspec $ do
|
hspec $ do
|
||||||
@@ -74,6 +70,10 @@ main = do
|
|||||||
beforeAll_ reset . before binaryJwtApp $
|
beforeAll_ reset . before binaryJwtApp $
|
||||||
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
|
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
|
||||||
|
|
||||||
|
-- this test runs with asymmetric JWK
|
||||||
|
beforeAll_ reset . before asymJwkApp $
|
||||||
|
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
specs = map (uncurry describe) [
|
specs = map (uncurry describe) [
|
||||||
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
||||||
|
|||||||
+12
-4
@@ -13,7 +13,8 @@ import Data.List (lookup)
|
|||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import System.Process (readProcess)
|
import System.Process (readProcess)
|
||||||
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig(..))
|
import PostgREST.Config (AppConfig(..))
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ _baseCfg :: AppConfig
|
|||||||
_baseCfg = -- Connection Settings
|
_baseCfg = -- Connection Settings
|
||||||
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
|
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
|
||||||
-- Jwt settings
|
-- Jwt settings
|
||||||
(Just $ encodeUtf8 "safe") False
|
(Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False
|
||||||
-- Connection Modifiers
|
-- Connection Modifiers
|
||||||
10 Nothing (Just "test.switch_role")
|
10 Nothing (Just "test.switch_role")
|
||||||
-- Debug Settings
|
-- Debug Settings
|
||||||
@@ -89,9 +90,16 @@ testProxyCfg :: Text -> AppConfig
|
|||||||
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
|
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
|
||||||
|
|
||||||
testCfgBinaryJWT :: Text -> AppConfig
|
testCfgBinaryJWT :: Text -> AppConfig
|
||||||
testCfgBinaryJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Just secretBs }
|
testCfgBinaryJWT testDbConn = (testCfg testDbConn) {
|
||||||
where secretBs = B64.decodeLenient "h2CGB1FoBd51aQooCS2g+UmRgYQfTPQ6v3+9ALbaqM4="
|
configJwtSecret = Just . B64.decodeLenient $
|
||||||
|
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
|
||||||
|
}
|
||||||
|
|
||||||
|
testCfgAsymJWK :: Text -> AppConfig
|
||||||
|
testCfgAsymJWK testDbConn = (testCfg testDbConn) {
|
||||||
|
configJwtSecret = Just $ encodeUtf8
|
||||||
|
[str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]
|
||||||
|
}
|
||||||
|
|
||||||
setupDb :: Text -> IO ()
|
setupDb :: Text -> IO ()
|
||||||
setupDb dbConn = do
|
setupDb dbConn = do
|
||||||
|
|||||||
Vendored
+3
-4
@@ -205,7 +205,7 @@ CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_token
|
|||||||
LANGUAGE sql SECURITY DEFINER
|
LANGUAGE sql SECURITY DEFINER
|
||||||
AS $$
|
AS $$
|
||||||
SELECT jwt.sign(
|
SELECT jwt.sign(
|
||||||
row_to_json(r), 'safe'
|
row_to_json(r), 'reallyreallyreallyreallyverysafe'
|
||||||
) as token
|
) as token
|
||||||
FROM (
|
FROM (
|
||||||
SELECT rolname::text, id::text
|
SELECT rolname::text, id::text
|
||||||
@@ -238,7 +238,7 @@ CREATE FUNCTION jwt_test() RETURNS public.jwt_token
|
|||||||
LANGUAGE sql SECURITY DEFINER
|
LANGUAGE sql SECURITY DEFINER
|
||||||
AS $$
|
AS $$
|
||||||
SELECT jwt.sign(
|
SELECT jwt.sign(
|
||||||
row_to_json(r), 'safe'
|
row_to_json(r), 'reallyreallyreallyreallyverysafe'
|
||||||
) as token
|
) as token
|
||||||
FROM (
|
FROM (
|
||||||
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
|
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
|
||||||
@@ -279,14 +279,13 @@ $$;
|
|||||||
--
|
--
|
||||||
|
|
||||||
CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
|
CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
|
||||||
iss text, sub text, aud text, exp bigint,
|
iss text, sub text, exp bigint,
|
||||||
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
|
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
|
||||||
)
|
)
|
||||||
LANGUAGE sql SECURITY DEFINER
|
LANGUAGE sql SECURITY DEFINER
|
||||||
AS $$
|
AS $$
|
||||||
SELECT current_setting('request.jwt.claim.iss') as iss,
|
SELECT current_setting('request.jwt.claim.iss') as iss,
|
||||||
current_setting('request.jwt.claim.sub') as sub,
|
current_setting('request.jwt.claim.sub') as sub,
|
||||||
current_setting('request.jwt.claim.aud') as aud,
|
|
||||||
current_setting('request.jwt.claim.exp')::bigint as exp,
|
current_setting('request.jwt.claim.exp')::bigint as exp,
|
||||||
current_setting('request.jwt.claim.nbf')::bigint as nbf,
|
current_setting('request.jwt.claim.nbf')::bigint as nbf,
|
||||||
current_setting('request.jwt.claim.iat')::bigint as iat,
|
current_setting('request.jwt.claim.iat')::bigint as iat,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
QSBCIEMKSXQncyBlYXN5IGFzLCAxIDIgMw==
|
QUJDCkVhc3kgYXMKMTIzCk9yIHNpbXBsZSBhcwpEbyByZSBtaQ==
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.MsR2A5HkhQdBsuQhXH8TvUdlvezBm5JEu4SOmHj34KI
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.p__cvqgrtteKUhxrJwTwqxeuFmlEm3hEj1mk7yO15M4
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
A B C
|
ABC
|
||||||
It's easy as, 1 2 3
|
Easy as
|
||||||
|
123
|
||||||
|
Or simple as
|
||||||
|
Do re mi
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
A B C
|
ABC
|
||||||
It's easy as, 1 2 3
|
Easy as
|
||||||
|
123
|
||||||
|
Or simple as
|
||||||
|
Do re mi
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
i6aWVaZ4Zt8=
|
RTwSHLM0/PWM2YCOyBiyChMQQamZLTZGrXdzGk61o5A=
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
協剖ヲxf゚
|
E<³4üõŒÙ€ŽÈ²
|
||||||
|
A©™-6FwsNµ£�
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.ySt_r_BN596NjZGcUAnlMAflDARDjrsR2c-fkOWlbFs
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.zyXYJKXgVYaWqMYgUXMk4zqXSU5cwA_vK2z9_5lRlqA
|
||||||
@@ -1 +1,2 @@
|
|||||||
協剖ヲxf゚
|
E<³4üõŒÙ€ŽÈ²
|
||||||
|
A©™-6FwsNµ£�
|
||||||
@@ -1 +1 @@
|
|||||||
QUJDMTIz
|
QUJDRWFzeUFzT25lVHdvVGhyZWVPclNpbXBsZUFzRG9SZU1p
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.yERzinJhzJ5XYKZuxVroqsAwGXMtCxntfm8HVxc1amI
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.1oyYyosD3_z4YzBE1DB-Pxf7wC0SCuZ2u1zLzPYU_nQ
|
||||||
@@ -1 +1 @@
|
|||||||
ABC123
|
ABCEasyAsOneTwoThreeOrSimpleAsDoReMi
|
||||||
@@ -1 +1 @@
|
|||||||
ABC123
|
ABCEasyAsOneTwoThreeOrSimpleAsDoReMi
|
||||||
|
|||||||
Reference in New Issue
Block a user