Use sql to generate jwt, rather than custom haskell

This commit is contained in:
Joe Nelson
2016-09-24 21:32:47 -07:00
parent 62ed9e2c4d
commit 12a8c682bd
7 changed files with 43 additions and 48 deletions
+6 -14
View File
@@ -7,7 +7,8 @@ module PostgREST.App (
) where ) where
import Control.Applicative import Control.Applicative
import qualified Data.ByteString.Char8 as BS import Control.Monad ((>>))
import qualified Data.ByteString.Char8 as BS
import Data.IORef (IORef, readIORef) import Data.IORef (IORef, readIORef)
import Data.List (delete, lookup) import Data.List (delete, lookup)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
@@ -45,7 +46,7 @@ import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
, ctToHeader , ctToHeader
, userApiRequest , userApiRequest
, toHeader) , toHeader)
import PostgREST.Auth (tokenJWT, jwtClaims, containsRole) import PostgREST.Auth (jwtClaims, containsRole)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.Error (errResponse, pgErrResponse) import PostgREST.Error (errResponse, pgErrResponse)
@@ -97,6 +98,8 @@ transactionMode _ = HT.Write
app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response
app dbStructure conf apiRequest = app dbStructure conf apiRequest =
exposeSecretToSQL (configJwtSecret conf) >>
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
(ActionRead, TargetIdent qi, Nothing) -> (ActionRead, TargetIdent qi, Nothing) ->
@@ -213,10 +216,6 @@ app dbStructure conf apiRequest =
Just (PayloadJSON (UniformObjects payload))) -> do Just (PayloadJSON (UniformObjects payload))) -> do
let p = V.head payload let p = V.head payload
singular = iPreferSingular apiRequest singular = iPreferSingular apiRequest
jwtSecret = secret <$> configJwtSecret conf
returnType = lookup (qiName qi) $ dbProcs dbStructure
returnsJWT = fromMaybe False $
isInfixOf "jwt_claims" . pdReturnType <$> returnType
serves [CTApplicationJSON] (iAccepts apiRequest) $ \_ -> case readSqlParts of serves [CTApplicationJSON] (iAccepts apiRequest) $ \_ -> case readSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ toS e Left e -> return $ responseLBS status400 [jsonH] $ toS e
Right (q,cq) -> respondToRange $ do Right (q,cq) -> respondToRange $ do
@@ -224,14 +223,7 @@ app dbStructure conf apiRequest =
let (tableTotal, queryTotal, body) = let (tableTotal, queryTotal, body) =
fromMaybe (Just 0, 0, emptyArray) row fromMaybe (Just 0, 0, emptyArray) row
(status, contentRange) = rangeHeader queryTotal tableTotal (status, contentRange) = rangeHeader queryTotal tableTotal
return $ case (returnsJWT, jwtSecret) of return $ responseLBS status [jsonH, contentRange] (toS . encode $ body)
(True, Nothing) ->
errResponse status500 "Server lacks JWT secret"
(True, Just s) ->
responseLBS status [jsonH, contentRange] $
"{\"token\":\"" <> toS (tokenJWT s body) <> "\"}"
(False, _) ->
responseLBS status [jsonH, contentRange] (toS . encode $ body)
(ActionRead, TargetRoot, Nothing) -> do (ActionRead, TargetRoot, Nothing) -> do
let host = configHost conf let host = configHost conf
+5
View File
@@ -19,6 +19,7 @@ import PostgREST.ApiRequest (ApiRequest(..), ContentType(..),
import PostgREST.Auth (claimsToSQL, JWTAttempt(..)) import PostgREST.Auth (claimsToSQL, JWTAttempt(..))
import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (errResponse) import PostgREST.Error (errResponse)
import PostgREST.QueryBuilder (pgFmtLit)
import Protolude hiding (concat, null) import Protolude hiding (concat, null)
@@ -45,6 +46,10 @@ runWithClaims conf eClaims app req =
] ]
(toS $ "{\"message\":\""<>message<>"\"}") (toS $ "{\"message\":\""<>message<>"\"}")
exposeSecretToSQL :: Maybe Text -> H.Transaction ()
exposeSecretToSQL mS = do
for_ mS $ \s ->
H.sql $ "set local postgrest.jwt_secret = " <> toS (pgFmtLit s) <> ";"
defaultMiddle :: Application -> Application defaultMiddle :: Application -> Application
defaultMiddle = defaultMiddle =
+6 -4
View File
@@ -12,6 +12,7 @@ import Network.Wai (Application)
spec :: SpecWith Application spec :: SpecWith Application
spec = describe "authorization" $ do spec = describe "authorization" $ do
let single = ("Prefer","plurality=singular")
it "denies access to tables that anonymous does not own" $ it "denies access to tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` ResponseMatcher { get "/authors_only" `shouldRespondWith` ResponseMatcher {
@@ -38,17 +39,18 @@ spec = describe "authorization" $ do
} }
it "returns jwt functions as jwt tokens" $ it "returns jwt functions as jwt tokens" $
post "/rpc/login" [json| { "id": "jdoe", "pass": "1234" } |] request methodPost "/rpc/login" [single]
[json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |] matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |]
, matchStatus = 200 , matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
} }
it "sql functions can encode custom and standard claims" $ it "sql functions can encode custom and standard claims" $
post "/rpc/jwt_test" "{}" request methodPost "/rpc/jwt_test" [single] "{}"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6MTMwMDgxOTM4MCwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3QiLCJpYXQiOjEzMDA4MTkzODAsImF1ZCI6ImV2ZXJ5b25lIn0._tQCF79-ZZGMlLktd3csM_bVaiMg7A8YvIb6K2hcu5w"} |] matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |]
, matchStatus = 200 , matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
} }
+3 -4
View File
@@ -3,7 +3,6 @@ module Feature.NoJwtSpec where
-- {{{ Imports -- {{{ Imports
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types import Network.HTTP.Types
import SpecHelper import SpecHelper
@@ -19,6 +18,6 @@ spec = describe "server started without JWT secret" $ do
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 500 `shouldRespondWith` 500
it "responds with error when attempting to generate JWT token" $ it "behaves normally when user does not attempt auth" $ do
post "/rpc/login" [json| { "id": "jdoe", "pass": "1234" } |] request methodGet "/items" [] ""
`shouldRespondWith` 500 `shouldRespondWith` 200
+2
View File
@@ -72,8 +72,10 @@ testProxyCfg =
setupDb :: IO () setupDb :: IO ()
setupDb = do setupDb = do
void $ readProcess "psql" ["-d", "postgres", "-a", "-f", "test/fixtures/database.sql"] [] void $ readProcess "psql" ["-d", "postgres", "-a", "-f", "test/fixtures/database.sql"] []
void $ readProcess "psql" ["-d", "postgrest_test", "-a", "-c", "CREATE EXTENSION IF NOT EXISTS pgcrypto;"] []
loadFixture "roles" loadFixture "roles"
loadFixture "schema" loadFixture "schema"
loadFixture "jwt"
loadFixture "privileges" loadFixture "privileges"
resetDb resetDb
+1
View File
@@ -2,6 +2,7 @@
GRANT USAGE ON SCHEMA GRANT USAGE ON SCHEMA
postgrest postgrest
, test , test
, jwt
, "تست" , "تست"
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
+20 -26
View File
@@ -49,29 +49,11 @@ CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
SET search_path = public, pg_catalog; SET search_path = public, pg_catalog;
-- --
-- Name: jwt_claims; Type: TYPE; Schema: public; Owner: - -- Name: jwt_token; Type: TYPE; Schema: public; Owner: -
-- --
CREATE TYPE jwt_claims AS ( CREATE TYPE jwt_token AS (
role text, token text
id text
);
--
-- Name: big_jwt_claims; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE big_jwt_claims AS (
iss text,
sub text,
aud text,
exp integer,
nbf integer,
iat integer,
jti text,
role text,
"http://postgrest.com/foo" boolean
); );
@@ -208,10 +190,17 @@ $$;
-- Name: login(text, text); Type: FUNCTION; Schema: test; Owner: - -- Name: login(text, text); Type: FUNCTION; Schema: test; Owner: -
-- --
CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_claims CREATE FUNCTION login(id text, pass text) RETURNS public.jwt_token
LANGUAGE sql SECURITY DEFINER LANGUAGE sql SECURITY DEFINER
AS $$ AS $$
SELECT rolname::text, id::text FROM postgrest.auth WHERE id = id AND pass = pass; SELECT jwt.sign(
row_to_json(r), current_setting('postgrest.jwt_secret')
) as token
FROM (
SELECT rolname::text, id::text
FROM postgrest.auth
WHERE id = id AND pass = pass
) r;
$$; $$;
@@ -234,13 +223,18 @@ $_$;
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: - -- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
-- --
CREATE FUNCTION jwt_test() RETURNS public.big_jwt_claims CREATE FUNCTION jwt_test() RETURNS public.jwt_token
LANGUAGE sql SECURITY DEFINER LANGUAGE sql SECURITY DEFINER
AS $$ AS $$
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud, SELECT jwt.sign(
row_to_json(r), current_setting('postgrest.jwt_secret')
) as token
FROM (
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
1300819380 as exp, 1300819380 as nbf, 1300819380 as iat, 1300819380 as exp, 1300819380 as nbf, 1300819380 as iat,
'foo'::text as jti, 'postgrest_test'::text as role, 'foo'::text as jti, 'postgrest_test'::text as role,
true as "http://postgrest.com/foo"; true as "http://postgrest.com/foo"
) r;
$$; $$;