First draft of big auth simplification

This commit is contained in:
Diogo Biazus
2015-10-16 15:55:47 -04:00
parent 2c67b8d7ba
commit 3add3f5b6c
5 changed files with 42 additions and 121 deletions
+4 -39
View File
@@ -57,12 +57,12 @@ import PostgREST.Types
import Prelude import Prelude
app :: DbStructure -> AppConfig -> DbRole -> BL.ByteString -> DbRole -> Request -> H.Tx P.Postgres s Response app :: DbStructure -> AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response
app dbstructure conf authenticator reqBody dbrole req = app dbstructure conf reqBody req =
case (path, verb) of case (path, verb) of
([], _) -> do ([], _) -> do
let body = encode $ filter (filterTableAcl dbrole) $ filter ((cs schema==).tableSchema) allTabs let body = encode $ filter ((cs schema==).tableSchema) allTabs
return $ responseLBS status200 [jsonH] $ cs body return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do ([table], "OPTIONS") -> do
@@ -130,41 +130,6 @@ app dbstructure conf authenticator reqBody dbrole req =
countQuery = requestToCountQuery schema <$> apiRequest countQuery = requestToCountQuery schema <$> apiRequest
queries = (,) <$> query <*> countQuery queries = (,) <$> query <*> countQuery
(["postgrest", "users"], "POST") -> do
let user = decode reqBody :: Maybe AuthUser
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
_ <- addUser (cs $ userId u)
(cs $ userPass u) (cs <$> userRole u)
return $ responseLBS status201
[ jsonH
, (hLocation, "/postgrest/users?id=eq." <> cs (userId u))
] ""
(["postgrest", "tokens"], "POST") ->
case jwtSecret of
"secret" -> return $ responseLBS status500 [jsonH] $
encode . object $ [("message", String "JWT Secret is set as \"secret\" which is an unsafe default.")]
_ -> do
let user = decode reqBody :: Maybe AuthUser
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
setRole authenticator
login <- signInRole (cs $ userId u) (cs $ userPass u)
case login of
LoginSuccess role uid ->
return $ responseLBS status201 [ jsonH ] $
encode . object $ [("token", String $ tokenJWT jwtSecret uid role)]
_ -> return $ responseLBS status401 [jsonH] $
encode . object $ [("message", String "Failed authentication.")]
([table], "POST") -> do ([table], "POST") -> do
let qt = qualify table let qt = qualify table
echoRequested = hasPrefer "return=representation" echoRequested = hasPrefer "return=representation"
@@ -295,7 +260,7 @@ app dbstructure conf authenticator reqBody dbrole req =
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
accept = lookupHeader hAccept accept = lookupHeader hAccept
schema = cs $ configSchema conf schema = cs $ configSchema conf
jwtSecret = cs $ configJwtSecret conf jwtSecret = (cs $ configJwtSecret conf) :: Text
range = rangeRequested hdrs range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
contentType = fromMaybe "application/json" $ contentTypeForAccept accept contentType = fromMaybe "application/json" $ contentTypeForAccept accept
+17 -46
View File
@@ -1,23 +1,24 @@
{-# LANGUAGE FlexibleContexts #-}
module PostgREST.Auth where module PostgREST.Auth where
import Control.Applicative import Control.Applicative
import Control.Monad (mzero) import Control.Monad (mzero)
import Crypto.BCrypt
import Data.Aeson import Data.Aeson
import Data.Map import Data.Map (lookup, fromList, toList)
import Data.Monoid import Data.Monoid
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text
import Data.Maybe (isNothing) import Data.Maybe (isNothing)
import Data.Text (Text)
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Hasql as H import qualified Hasql as H
import qualified Hasql.Backend as B import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P import qualified Hasql.Postgres as P
import PostgREST.PgQuery (pgFmtLit) import PostgREST.PgQuery (pgFmtLit)
import Prelude import Prelude
import qualified Web.JWT as JWT import qualified Web.JWT as JWT
import System.IO.Unsafe
data AuthUser = AuthUser { data AuthUser = AuthUser {
userId :: String userId :: String
@@ -48,51 +49,21 @@ data LoginAttempt =
| LoginSuccess DbRole UserId | LoginSuccess DbRole UserId
deriving (Eq, Show) deriving (Eq, Show)
checkPass :: Text -> Text -> Bool setJWTEnv :: Text -> Text -> Maybe [Text]
checkPass = (. cs) . validatePassword . cs setJWTEnv secret input = setDBEnv $ jwtClaims secret input
setRole :: Text -> H.Tx P.Postgres s () setDBEnv :: Maybe JWT.ClaimsMap -> Maybe [Text]
setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True setDBEnv maybeClaims =
(map setVar . toList) <$> maybeClaims
where
setVar ("role", String val) = setRole val
setVar (key, String val) = "set local postgrest." <> key <> " = " <> cs (pgFmtLit val) <> ";"
setUserId :: Text -> H.Tx P.Postgres s () setRole role = "set local role " <> cs (pgFmtLit role) <> ";"
setUserId uid =
if uid /= ""
then H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
else resetUserId
resetUserId :: H.Tx P.Postgres s () jwtClaims :: Text -> Text -> Maybe JWT.ClaimsMap
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|] jwtClaims secret input = claims
addUser :: Text -> Text -> Maybe Text -> H.Tx P.Postgres s ()
addUser identity pass role =
H.unitEx $
if isNothing role
then [H.stmt|insert into postgrest.auth (id, pass) values (?, ?)|]
identity hashedText
else [H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
identity hashedText role
where Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
hashedText = cs hashed :: Text
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
signInRole user pass = do
u <- H.maybeEx $ [H.stmt|select id, pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (uid, hashed, role) = r in
if checkPass hashed pass
then LoginSuccess role uid
else LoginFailed
) u
signInWithJWT :: Text -> Text -> LoginAttempt
signInWithJWT secret input = case maybeRole of
Just (Just (String role)) -> case maybeUserId of
Just (Just (String uid)) -> LoginSuccess (cs role) (cs uid)
_ -> LoginFailed
_ -> LoginFailed
where where
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
+1 -1
View File
@@ -98,5 +98,5 @@ main = do
runSettings appSettings $ middle $ \ req respond -> do runSettings appSettings $ middle $ \ req respond -> do
body <- strictRequestBody req body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx txSettings $ resOrError <- liftIO $ H.session pool $ H.tx txSettings $
authenticated conf authenticator (app dbstructure conf authenticator body) req runWithClaims conf (app dbstructure conf body) req
either (respond . errResponse) respond resOrError either (respond . errResponse) respond resOrError
+19 -34
View File
@@ -27,46 +27,31 @@ import Network.Wai.Middleware.Static (only, staticPolicy)
import Codec.Binary.Base64.String (decode) import Codec.Binary.Base64.String (decode)
import PostgREST.App (contentTypeForAccept) import PostgREST.App (contentTypeForAccept)
import PostgREST.Auth (DbRole, LoginAttempt (..), import PostgREST.Auth (DbRole, LoginAttempt (..),
setRole, setUserId, signInRole, setRole, setJWTEnv)
signInWithJWT)
import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Config (AppConfig (..), corsPolicy)
import Prelude import Prelude hiding(concat)
import qualified Web.JWT as JWT
authenticated :: forall s. AppConfig -> DbRole -> import qualified Data.Vector as V
(DbRole -> Request -> H.Tx P.Postgres s Response) -> import qualified Hasql.Backend as B
runWithClaims :: forall s. AppConfig ->
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response Request -> H.Tx P.Postgres s Response
authenticated conf authenticator app req = do runWithClaims conf app req = do
attempt <- httpRequesterRole (requestHeaders req) H.unitEx $ B.Stmt env V.empty True
case attempt of app req
MalformedAuth ->
return $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
return $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role uid -> if role /= authenticator then runInRole role uid else app authenticator req
NoCredentials -> if anon /= authenticator then runInRole anon "" else app authenticator req
where where
jwtSecret = cs $ configJwtSecret conf hdrs = requestHeaders req
jwtSecret = (cs $ configJwtSecret conf) :: Text
auth = fromMaybe "" $ lookup hAuthorization hdrs
anon = cs $ configAnonRole conf anon = cs $ configAnonRole conf
httpRequesterRole :: RequestHeaders -> H.Tx P.Postgres s LoginAttempt jwtEnv =
httpRequesterRole hdrs = do case split (==' ') (cs auth) of
let auth = fromMaybe "" $ lookup hAuthorization hdrs ("Bearer" : jwt : _) -> fromMaybe [] (setJWTEnv jwtSecret jwt)
case split (==' ') (cs auth) of _ -> []
("Basic" : b64 : _) -> env = concat $ setRole anon : jwtEnv
case split (==':') (cs . decode . cs $ b64) of
(u:p:_) -> signInRole u p
_ -> return MalformedAuth
("Bearer" : jwt : _) ->
return $ signInWithJWT jwtSecret jwt
_ -> return NoCredentials
runInRole :: Text -> Text -> H.Tx P.Postgres s Response
runInRole r uid = do
setUserId uid
setRole r
app r req
redirectInsecure :: Application -> Application redirectInsecure :: Application -> Application
redirectInsecure app req respond = do redirectInsecure app req respond = do
+1 -1
View File
@@ -80,7 +80,7 @@ withApp perform = do
perform $ middle $ \req resp -> do perform $ middle $ \req resp -> do
body <- strictRequestBody req body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx txSettings result <- liftIO $ H.session pool $ H.tx txSettings
$ authenticated cfg authenticator (app dbstructure cfg authenticator body) req $ runWithClaims cfg (app dbstructure cfg body) req
either (resp . errResponse) resp result either (resp . errResponse) resp result
where middle = defaultMiddle False where middle = defaultMiddle False