diff --git a/CHANGELOG.md b/CHANGELOG.md index a73d34b1a..67288ab50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- #567, Support more JWT signing algorithms, including RSA - @begriffs - #889, Allow more than two conditions in a single and/or - @steve-chavez - #883, Binary output support for RPC - @steve-chavez - #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui diff --git a/main/Main.hs b/main/Main.hs index 4493b40a8..902195e5f 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -13,8 +13,6 @@ import PostgREST.OpenAPI (isMalformedProxyUri) import PostgREST.Types (DbStructure, Schema) import Protolude -import Control.AutoUpdate (defaultUpdateSettings, - mkAutoUpdate, updateAction) import Control.Retry (RetryStatus, capDelay, exponentialBackoff, retrying, rsPreviousDelay) @@ -25,7 +23,6 @@ import Data.String (IsString (..)) import Data.Text (pack, replace, stripPrefix, strip) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.IO (hPutStrLn, readFile) -import Data.Time.Clock.POSIX (getPOSIXTime) import qualified Hasql.Decoders as HD import qualified Hasql.Encoders as HE import qualified Hasql.Pool as P @@ -72,7 +69,7 @@ isServerVersionSupported = do 4. If 2 or 3 fail to give their result it means the connection is down so it goes back to 1, otherwise it finishes his work successfully. -} -connectionWorker +connectionWorker :: ThreadId -- ^ This thread is killed if 'isServerVersionSupported' returns false -> P.Pool -- ^ The PostgreSQL connection pool -> Schema -- ^ Schema PostgREST is serving up @@ -144,7 +141,7 @@ connectingSucceeded pool = main :: IO () main = do -- - -- LineBuffering: the entire output buffer is flushed whenever a newline is + -- LineBuffering: the entire output buffer is flushed whenever a newline is -- output, the buffer overflows, a hFlush is issued or the handle is closed -- -- NoBuffering: output is written immediately and never stored in the buffer @@ -166,7 +163,7 @@ main = do . setTimeout 3600 $ defaultSettings -- - -- Checks that the provided proxy uri is formated correctly, + -- Checks that the provided proxy uri is formated correctly, -- does not test if it works here. when (isMalformedProxyUri $ toS <$> proxy) $ panic @@ -210,18 +207,15 @@ main = do ) Nothing void $ installHandler sigHUP ( - Catch $ connectionWorker - mainTid - pool - (configSchema conf) - refDbStructure + Catch $ connectionWorker + mainTid + pool + (configSchema conf) + refDbStructure refIsWorkerOn ) Nothing #endif - -- - -- ask for the OS time at most once per second - getTime <- - mkAutoUpdate defaultUpdateSettings {updateAction = getPOSIXTime} + -- -- run the postgrest application runSettings appSettings $ @@ -229,7 +223,6 @@ main = do conf refDbStructure pool - getTime (connectionWorker mainTid pool @@ -257,7 +250,7 @@ loadSecretFile conf = extractAndTransform mSecret mSecret = decodeUtf8 <$> configJwtSecret conf isB64 = configJwtSecretIsBase64 conf -- - -- The Text (variable name secret) here is mSecret from above which is the JWT + -- The Text (variable name secret) here is mSecret from above which is the JWT -- decoded as Utf8 -- -- stripPrefix: Return the suffix of the second string if its prefix matches diff --git a/postgrest.cabal b/postgrest.cabal index 127d354b1..62f4975b6 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -29,14 +29,12 @@ executable postgrest -rtsopts "-with-rtsopts=-N -I2" default-language: Haskell2010 - build-depends: auto-update - , base + build-depends: base , hasql , hasql-pool , postgrest , protolude , text - , time , warp , bytestring , base64-bytestring @@ -52,6 +50,7 @@ library build-depends: aeson , ansi-wl-pprint , base >= 4.8 && < 6 + , base64-bytestring , bytestring , case-insensitive , cassava @@ -67,7 +66,7 @@ library , http-types , insert-ordered-containers , interpolatedstring-perl6 - , jwt + , jose , lens , lens-aeson , network-uri @@ -80,7 +79,6 @@ library , scientific , swagger2 , text - , time , unordered-containers , vector , wai @@ -113,6 +111,7 @@ Test-Suite spec Hs-Source-Dirs: test Main-Is: Main.hs Other-Modules: Feature.AuthSpec + , Feature.AsymmetricJwtSpec , Feature.BinaryJwtSecretSpec , Feature.ConcurrentSpec , Feature.CorsSpec @@ -132,7 +131,6 @@ Test-Suite spec Build-Depends: aeson , aeson-qq , async - , auto-update , base , bytestring , base64-bytestring @@ -156,7 +154,6 @@ Test-Suite spec , process , protolude , regex-tdfa - , time , transformers-base , wai , wai-extra diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 657bf02f8..f07f5ab67 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -11,7 +11,6 @@ import qualified Data.ByteString.Char8 as BS import Data.Maybe import Data.IORef (IORef, readIORef) import Data.Text (intercalate) -import Data.Time.Clock.POSIX (POSIXTime) import qualified Hasql.Pool as P import qualified Hasql.Transaction as HT @@ -22,7 +21,6 @@ import Network.HTTP.Types.Status import Network.HTTP.Types.URI (renderSimpleQuery) import Network.Wai import Network.Wai.Middleware.RequestLogger (logStdout) -import Web.JWT (binarySecret) import qualified Data.Vector as V import qualified Hasql.Transaction as H @@ -35,7 +33,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..) , mutuallyAgreeable , userApiRequest ) -import PostgREST.Auth (jwtClaims, containsRole) +import PostgREST.Auth (jwtClaims, containsRole, parseJWK) import PostgREST.Config (AppConfig (..)) import PostgREST.DbStructure import PostgREST.DbRequestBuilder( readRequest @@ -63,13 +61,12 @@ import Data.Function (id) import Protolude hiding (intercalate, Proxy) import Safe (headMay) -postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO POSIXTime -> - IO () -> Application -postgrest conf refDbStructure pool getTime worker = - let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in +postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO () -> Application +postgrest conf refDbStructure pool worker = + let middle = (if configQuiet conf then id else logStdout) . defaultMiddle + jwtSecret = parseJWK <$> configJwtSecret conf in middle $ \ req respond -> do - time <- getTime body <- strictRequestBody req maybeDbStructure <- readIORef refDbStructure case maybeDbStructure of @@ -78,9 +75,9 @@ postgrest conf refDbStructure pool getTime worker = response <- case userApiRequest (configSchema conf) req body of Left err -> return $ apiRequestError err Right apiRequest -> do - let jwtSecret = binarySecret <$> configJwtSecret conf - eClaims = jwtClaims jwtSecret (iJWT apiRequest) time - authed = containsRole eClaims + eClaims <- jwtClaims jwtSecret (toS $ iJWT apiRequest) + + let authed = containsRole eClaims handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest txMode = transactionMode dbStructure (iTarget apiRequest) (iAction apiRequest) @@ -324,7 +321,7 @@ responseContentTypeOrError accepts action = serves contentTypesForRequest accept case mutuallyAgreeable sProduces cAccepts of Nothing -> do let failed = intercalate ", " $ map (toS . toMime) cAccepts - Left $ simpleError status415 $ + Left $ simpleError status415 [] $ "None of these Content-Types are available: " <> failed Just ct -> Right ct diff --git a/src/PostgREST/Auth.hs b/src/PostgREST/Auth.hs index 9f267091f..1f2708e63 100644 --- a/src/PostgREST/Auth.hs +++ b/src/PostgREST/Auth.hs @@ -14,63 +14,48 @@ very simple authentication system inside the PostgreSQL database. module PostgREST.Auth ( containsRole , jwtClaims - , tokenJWT , JWTAttempt(..) + , parseJWK ) where -import Protolude +import Protolude hiding ((&)) import Control.Lens -import Data.Aeson (Value (..), parseJSON, toJSON) -import Data.Aeson.Lens -import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray) -import qualified Data.Vector as V +import Data.Aeson (Value (..), decode, toJSON) +import qualified Data.ByteString.Lazy as BL 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 -} -data JWTAttempt = JWTExpired - | JWTInvalid +data JWTAttempt = JWTInvalid JWTError | JWTMissingSecret | JWTClaims (M.HashMap Text Value) - deriving Eq + deriving (Eq, Show) {-| Receives the JWT secret (from config) and a JWT and returns a map of JWT claims. -} -jwtClaims :: Maybe JWT.Secret -> Text -> NominalDiffTime -> JWTAttempt -jwtClaims _ "" _ = JWTClaims M.empty -jwtClaims secret jwt time = +jwtClaims :: Maybe JWK -> BL.ByteString -> IO JWTAttempt +jwtClaims _ "" = return $ JWTClaims M.empty +jwtClaims secret payload = case secret of - Nothing -> JWTMissingSecret - Just s -> - let mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature s jwt in - case isExpired <$> mClaims of - Just True -> JWTExpired - Nothing -> JWTInvalid - Just False -> JWTClaims $ value2map $ fromJust mClaims - where - isExpired claims = - let mExp = claims ^? key "exp" . _Integer - 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 + Nothing -> return JWTMissingSecret + Just jwk -> do + let validation = defaultJWTValidationSettings + eJwt <- runExceptT $ do + jwt <- decodeCompact payload + validateJWSJWT validation jwk jwt + return jwt + return $ case eJwt of + Left e -> JWTInvalid e + Right jwt -> JWTClaims . claims2map . jwtClaimsSet $ jwt {-| Whether a response from jwtClaims contains a role claim @@ -78,3 +63,30 @@ tokenJWT secret _ = tokenJWT secret emptyArray containsRole :: JWTAttempt -> Bool containsRole (JWTClaims claims) = M.member "role" claims 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)) diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index ab11e1f47..dec3c6cf8 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -18,12 +18,15 @@ import qualified Data.Aeson as JSON import Data.Text (unwords) import qualified Hasql.Pool as P import qualified Hasql.Session as H +import Network.HTTP.Types.Header import qualified Network.HTTP.Types.Status as HT import Network.Wai (Response, responseLBS) import PostgREST.Types apiRequestError :: ApiRequestError -> Response -apiRequestError err = errorResponse status err +apiRequestError err = + errorResponse status + [toHeader CTApplicationJSON] err where status = case err of @@ -35,13 +38,14 @@ apiRequestError err = errorResponse status err InvalidRange -> HT.status416 UnknownRelation -> HT.status404 -simpleError :: HT.Status -> Text -> Response -simpleError status message = - errorResponse status $ JSON.object ["message" .= message] +simpleError :: HT.Status -> [Header] -> Text -> Response +simpleError status hdrs message = + errorResponse status (toHeader CTApplicationJSON : hdrs) $ + JSON.object ["message" .= message] -errorResponse :: JSON.ToJSON a => HT.Status -> a -> Response -errorResponse status e = - responseLBS status [toHeader CTApplicationJSON] $ encodeError e +errorResponse :: JSON.ToJSON a => HT.Status -> [Header] -> a -> Response +errorResponse status hdrs e = + responseLBS status hdrs $ encodeError e pgError :: Bool -> P.UsageError -> Response pgError authed e = @@ -71,12 +75,12 @@ singularityError numRows = binaryFieldError :: Response binaryFieldError = - simpleError HT.status406 (toS (toMime CTOctetStream) <> + simpleError HT.status406 [] (toS (toMime CTOctetStream) <> " requested but a single column was not selected") connectionLostError :: Response 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.encode diff --git a/src/PostgREST/Middleware.hs b/src/PostgREST/Middleware.hs index f94eee430..04043572b 100644 --- a/src/PostgREST/Middleware.hs +++ b/src/PostgREST/Middleware.hs @@ -4,13 +4,13 @@ module PostgREST.Middleware where +import Crypto.JWT import Data.Aeson (Value (..)) import qualified Data.HashMap.Strict as M import qualified Hasql.Transaction as H import Network.HTTP.Types.Status (unauthorized401, status500) -import Network.Wai (Application, Response, - responseLBS) +import Network.Wai (Application, Response) import Network.Wai.Middleware.Cors (cors) import Network.Wai.Middleware.Gzip (def, gzip) import Network.Wai.Middleware.Static (only, staticPolicy) @@ -19,7 +19,6 @@ import PostgREST.ApiRequest (ApiRequest(..)) import PostgREST.Auth (JWTAttempt(..)) import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Error (simpleError) -import PostgREST.Types (ContentType (..), toHeader) import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar) import Protolude hiding (concat, null) @@ -29,9 +28,9 @@ runWithClaims :: AppConfig -> JWTAttempt -> ApiRequest -> H.Transaction Response runWithClaims conf eClaims app req = case eClaims of - JWTExpired -> return $ unauthed "JWT expired" - JWTInvalid -> return $ unauthed "JWT invalid" - JWTMissingSecret -> return $ simpleError status500 "Server lacks JWT secret" + JWTInvalid JWTExpired -> return $ unauthed "JWT expired" + JWTInvalid e -> return $ unauthed $ show e + JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret" JWTClaims claims -> do H.sql $ toS.mconcat $ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql mapM_ H.sql customReqCheck @@ -47,14 +46,13 @@ runWithClaims conf eClaims app req = anon = String . toS $ configAnonRole conf customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf where - unauthed message = responseLBS unauthorized401 - [ toHeader CTApplicationJSON - , ( "WWW-Authenticate" + unauthed message = simpleError + unauthorized401 + [( "WWW-Authenticate" , "Bearer error=\"invalid_token\", " <> - "error_description=\"" <> message <> "\"" - ) - ] - (toS $ "{\"message\":\""<>message<>"\"}") + "error_description=" <> show message + )] + message defaultMiddle :: Application -> Application defaultMiddle = diff --git a/stack.yaml b/stack.yaml index 0596112d3..3925a132e 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,9 +1,10 @@ resolver: lts-8.5 extra-deps: - - Ranged-sets-0.3.0 - - hasql-pool-0.4.1 - configurator-ng-0.0.0.1 - critbit-0.2.0.0 + - hasql-pool-0.4.1 + - jose-0.5.0.3 + - Ranged-sets-0.3.0 ghc-options: postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints nix: diff --git a/test/Feature/AsymmetricJwtSpec.hs b/test/Feature/AsymmetricJwtSpec.hs new file mode 100644 index 000000000..e7952fdda --- /dev/null +++ b/test/Feature/AsymmetricJwtSpec.hs @@ -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 diff --git a/test/Feature/AuthSpec.hs b/test/Feature/AuthSpec.hs index 22efc4c63..38fce172c 100644 --- a/test/Feature/AuthSpec.hs +++ b/test/Feature/AuthSpec.hs @@ -28,7 +28,7 @@ spec = describe "authorization" $ do } 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] "" `shouldRespondWith` [json| { "hint":null, @@ -42,41 +42,41 @@ spec = describe "authorization" $ do it "returns jwt functions as jwt tokens" $ request methodPost "/rpc/login" [single] [json| { "id": "jdoe", "pass": "1234" } |] - `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |] + `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs"} |] { matchStatus = 200 , matchHeaders = [matchContentTypeSingular] } it "sql functions can encode custom and standard claims" $ request methodPost "/rpc/jwt_test" [single] "{}" - `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |] + `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek"} |] { matchStatus = 200 , matchHeaders = [matchContentTypeSingular] } 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] "{}" - `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 - let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" + let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0" request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200 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] "" `shouldRespondWith` 200 -- this test will stop working 9999999999s after the UNIX EPOCH 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] "" `shouldRespondWith` 200 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] "" `shouldRespondWith` [json| {"message":"JWT expired"} |] { matchStatus = 401 @@ -89,27 +89,27 @@ spec = describe "authorization" $ do it "hides tables from users with invalid JWT" $ do let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" request methodGet "/authors_only" [auth] "" - `shouldRespondWith` [json| {"message":"JWT invalid"} |] + `shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError \"expected 3 parts, got 2\")"} |] { matchStatus = 401 , matchHeaders = [ "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 - let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.lu-rG8aSCiw-aOlN0IxpRGz5r7Jwq7K9r3tuMPUpytI" + let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk" request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401 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] "" `shouldRespondWith` 401 it "recovers after 401 error with logged in user" $ do _ <- 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 methodGet "/authors_only" [auth] "" `shouldRespondWith` 200 @@ -117,7 +117,7 @@ spec = describe "authorization" $ do describe "custom pre-request proc acting on id claim" $ do 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] [json| {} |] `shouldRespondWith` [str|"postgrest_test_author"|] @@ -126,7 +126,7 @@ spec = describe "authorization" $ do } 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] [json| {} |] `shouldRespondWith` [str|"postgrest_test_default_role"|] @@ -135,7 +135,7 @@ spec = describe "authorization" $ do } 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] [json| {} |] `shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|] diff --git a/test/Feature/BinaryJwtSecretSpec.hs b/test/Feature/BinaryJwtSecretSpec.hs index d2cf272a0..08230b4ca 100644 --- a/test/Feature/BinaryJwtSecretSpec.hs +++ b/test/Feature/BinaryJwtSecretSpec.hs @@ -16,6 +16,6 @@ spec = describe "server started with binary JWT secret" $ -- this test will stop working 9999999999s after the UNIX EPOCH 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] "" `shouldRespondWith` 200 diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 60b770465..e9d71f957 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -451,7 +451,7 @@ spec = do describe "Row level permission" $ 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":"jroe", "pass": "1234", "role": "postgrest_test_author" } |] @@ -464,7 +464,7 @@ spec = do p2 <- request methodPost "/authors_only" -- jwt token for jroe - [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ] + [ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ] [json| { "secret": "lolcat", "owner": "hacker" } |] liftIO $ do simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|] diff --git a/test/Feature/NoJwtSpec.hs b/test/Feature/NoJwtSpec.hs index 5f1f1b3f9..af7a1ad7a 100644 --- a/test/Feature/NoJwtSpec.hs +++ b/test/Feature/NoJwtSpec.hs @@ -16,7 +16,7 @@ spec = describe "server started without JWT secret" $ do -- this test will stop working 9999999999s after the UNIX EPOCH 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] "" `shouldRespondWith` 500 diff --git a/test/Main.hs b/test/Main.hs index 5ac8df314..b6d149519 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -7,12 +7,11 @@ import qualified Hasql.Pool as P import PostgREST.DbStructure (getDbStructure) import PostgREST.App (postgrest) -import Control.AutoUpdate import Data.Function (id) import Data.IORef -import Data.Time.Clock.POSIX (getPOSIXTime) import qualified Feature.AuthSpec +import qualified Feature.AsymmetricJwtSpec import qualified Feature.BinaryJwtSecretSpec import qualified Feature.ConcurrentSpec import qualified Feature.CorsSpec @@ -36,19 +35,16 @@ main = do setupDb 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" refDbStructure <- newIORef $ Just $ either (panic.show) id result - let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure () - ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure () - unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure () - proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure () - noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure () - binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure () + let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool $ pure () + ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool $ pure () + unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool $ pure () + proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool $ pure () + noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool $ pure () + binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool $ pure () + asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure () let reset = resetDb testDbConn hspec $ do @@ -74,6 +70,10 @@ main = do beforeAll_ reset . before binaryJwtApp $ describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec + -- this test runs with asymmetric JWK + beforeAll_ reset . before asymJwkApp $ + describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec + where specs = map (uncurry describe) [ ("Feature.AuthSpec" , Feature.AuthSpec.spec) diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index a39b21cd9..4610e4df6 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -13,7 +13,8 @@ import Data.List (lookup) import Text.Regex.TDFA ((=~)) import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Lazy as BL -import System.Process (readProcess) +import System.Process (readProcess) +import Text.Heredoc import PostgREST.Config (AppConfig(..)) @@ -67,7 +68,7 @@ _baseCfg :: AppConfig _baseCfg = -- Connection Settings AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000 -- Jwt settings - (Just $ encodeUtf8 "safe") False + (Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False -- Connection Modifiers 10 Nothing (Just "test.switch_role") -- Debug Settings @@ -89,9 +90,16 @@ testProxyCfg :: Text -> AppConfig testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" } testCfgBinaryJWT :: Text -> AppConfig -testCfgBinaryJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Just secretBs } - where secretBs = B64.decodeLenient "h2CGB1FoBd51aQooCS2g+UmRgYQfTPQ6v3+9ALbaqM4=" +testCfgBinaryJWT testDbConn = (testCfg testDbConn) { + 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 dbConn = do diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 83e9b70f6..7a7a51557 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -205,7 +205,7 @@ CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_token LANGUAGE sql SECURITY DEFINER AS $$ SELECT jwt.sign( - row_to_json(r), 'safe' + row_to_json(r), 'reallyreallyreallyreallyverysafe' ) as token FROM ( SELECT rolname::text, id::text @@ -238,7 +238,7 @@ CREATE FUNCTION jwt_test() RETURNS public.jwt_token LANGUAGE sql SECURITY DEFINER AS $$ SELECT jwt.sign( - row_to_json(r), 'safe' + row_to_json(r), 'reallyreallyreallyreallyverysafe' ) as token FROM ( SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud, @@ -279,14 +279,13 @@ $$; -- 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 ) LANGUAGE sql SECURITY DEFINER AS $$ SELECT current_setting('request.jwt.claim.iss') as iss, 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.nbf')::bigint as nbf, current_setting('request.jwt.claim.iat')::bigint as iat, diff --git a/test/io-tests/secrets/ascii.b64 b/test/io-tests/secrets/ascii.b64 index 3cae5a981..ace0484cb 100644 --- a/test/io-tests/secrets/ascii.b64 +++ b/test/io-tests/secrets/ascii.b64 @@ -1 +1 @@ -QSBCIEMKSXQncyBlYXN5IGFzLCAxIDIgMw== +QUJDCkVhc3kgYXMKMTIzCk9yIHNpbXBsZSBhcwpEbyByZSBtaQ== diff --git a/test/io-tests/secrets/ascii.jwt b/test/io-tests/secrets/ascii.jwt index 6def4713b..aab45ae0e 100644 --- a/test/io-tests/secrets/ascii.jwt +++ b/test/io-tests/secrets/ascii.jwt @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.MsR2A5HkhQdBsuQhXH8TvUdlvezBm5JEu4SOmHj34KI \ No newline at end of file +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.p__cvqgrtteKUhxrJwTwqxeuFmlEm3hEj1mk7yO15M4 \ No newline at end of file diff --git a/test/io-tests/secrets/ascii.noeol b/test/io-tests/secrets/ascii.noeol index ecdc0b6f3..0bccffc16 100644 --- a/test/io-tests/secrets/ascii.noeol +++ b/test/io-tests/secrets/ascii.noeol @@ -1,2 +1,5 @@ -A B C -It's easy as, 1 2 3 \ No newline at end of file +ABC +Easy as +123 +Or simple as +Do re mi \ No newline at end of file diff --git a/test/io-tests/secrets/ascii.txt b/test/io-tests/secrets/ascii.txt index cb84d70a2..e845b2601 100644 --- a/test/io-tests/secrets/ascii.txt +++ b/test/io-tests/secrets/ascii.txt @@ -1,2 +1,5 @@ -A B C -It's easy as, 1 2 3 +ABC +Easy as +123 +Or simple as +Do re mi diff --git a/test/io-tests/secrets/binary.b64 b/test/io-tests/secrets/binary.b64 index c8f7d998e..56ddacdb1 100644 --- a/test/io-tests/secrets/binary.b64 +++ b/test/io-tests/secrets/binary.b64 @@ -1 +1 @@ -i6aWVaZ4Zt8= +RTwSHLM0/PWM2YCOyBiyChMQQamZLTZGrXdzGk61o5A= diff --git a/test/io-tests/secrets/binary.eol b/test/io-tests/secrets/binary.eol index 4996d03e9..4562f3b2e 100644 --- a/test/io-tests/secrets/binary.eol +++ b/test/io-tests/secrets/binary.eol @@ -1 +1,2 @@ -‹¦–U¦xfß +E<³4üõŒÙ€ŽÈ² +A©™-6F­wsNµ£ diff --git a/test/io-tests/secrets/binary.jwt b/test/io-tests/secrets/binary.jwt index 3c85cd7a7..542c3bbf0 100644 --- a/test/io-tests/secrets/binary.jwt +++ b/test/io-tests/secrets/binary.jwt @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.ySt_r_BN596NjZGcUAnlMAflDARDjrsR2c-fkOWlbFs \ No newline at end of file +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.zyXYJKXgVYaWqMYgUXMk4zqXSU5cwA_vK2z9_5lRlqA \ No newline at end of file diff --git a/test/io-tests/secrets/binary.noeol b/test/io-tests/secrets/binary.noeol index b967aa167..230fc8332 100644 --- a/test/io-tests/secrets/binary.noeol +++ b/test/io-tests/secrets/binary.noeol @@ -1 +1,2 @@ -‹¦–U¦xfß \ No newline at end of file +E<³4üõŒÙ€ŽÈ² +A©™-6F­wsNµ£ \ No newline at end of file diff --git a/test/io-tests/secrets/word.b64 b/test/io-tests/secrets/word.b64 index 49b69ed61..bfdffc968 100644 --- a/test/io-tests/secrets/word.b64 +++ b/test/io-tests/secrets/word.b64 @@ -1 +1 @@ -QUJDMTIz +QUJDRWFzeUFzT25lVHdvVGhyZWVPclNpbXBsZUFzRG9SZU1p diff --git a/test/io-tests/secrets/word.jwt b/test/io-tests/secrets/word.jwt index 3dfcac028..b12ae7745 100644 --- a/test/io-tests/secrets/word.jwt +++ b/test/io-tests/secrets/word.jwt @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.yERzinJhzJ5XYKZuxVroqsAwGXMtCxntfm8HVxc1amI \ No newline at end of file +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.1oyYyosD3_z4YzBE1DB-Pxf7wC0SCuZ2u1zLzPYU_nQ \ No newline at end of file diff --git a/test/io-tests/secrets/word.noeol b/test/io-tests/secrets/word.noeol index 8edce441f..15fe8c19c 100644 --- a/test/io-tests/secrets/word.noeol +++ b/test/io-tests/secrets/word.noeol @@ -1 +1 @@ -ABC123 \ No newline at end of file +ABCEasyAsOneTwoThreeOrSimpleAsDoReMi \ No newline at end of file diff --git a/test/io-tests/secrets/word.txt b/test/io-tests/secrets/word.txt index 92dd64739..3bdbf802f 100644 --- a/test/io-tests/secrets/word.txt +++ b/test/io-tests/secrets/word.txt @@ -1 +1 @@ -ABC123 +ABCEasyAsOneTwoThreeOrSimpleAsDoReMi