{-| Module : PostgREST.Auth Description : PostgREST authorization functions. This module provides functions to deal with the JWT authorization (http://jwt.io). It also can be used to define other authorization functions, in the future Oauth, LDAP and similar integrations can be coded here. Authentication should always be implemented in an external service. In the test suite there is an example of simple login function that can be used for a very simple authentication system inside the PostgreSQL database. -} {-# LANGUAGE RecordWildCards #-} module PostgREST.Auth ( AuthResult (..) , getResult , getRole , middleware ) where import qualified Crypto.JWT as JWT import qualified Data.Aeson as JSON import qualified Data.Aeson.Types as JSON import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.HashMap.Strict as M import qualified Data.Text.Encoding as T import qualified Data.Vault.Lazy as Vault import qualified Data.Vector as V import qualified Network.HTTP.Types.Header as HTTP import qualified Network.Wai as Wai import qualified Network.Wai.Middleware.HttpAuth as Wai import Control.Lens (set) import Control.Monad.Except (liftEither) import Data.Either.Combinators (mapLeft, mapRight) import Data.List (lookup) import Data.Time.Clock (UTCTime) import System.IO.Unsafe (unsafePerformIO) import PostgREST.AppState (AppState, getConfig, getTime) import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..)) import PostgREST.Error (Error (..)) import Protolude data AuthResult = AuthResult { authClaims :: M.HashMap Text JSON.Value , authRole :: Text } -- | Receives the JWT secret and audience (from config) and a JWT and returns a -- JSON object of JWT claims. parseToken :: Monad m => AppConfig -> LByteString -> UTCTime -> ExceptT Error m JSON.Value parseToken _ "" _ = return JSON.emptyObject parseToken AppConfig{..} token time = do secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS eitherClaims <- lift . runExceptT $ JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact token liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims where validation = JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 1 audienceCheck :: JWT.StringOrURI -> Bool audienceCheck = maybe (const True) (==) configJwtAudience jwtClaimsError :: JWT.JWTError -> Error jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired" jwtClaimsError e = JwtTokenInvalid $ show e parseClaims :: AppConfig -> JSON.Value -> AuthResult parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = AuthResult { authClaims = mclaims & M.insert "role" (JSON.toJSON role) , authRole = role } where -- role defaults to anon if not specified in jwt role = maybe configDbAnonRole unquoted (walkJSPath (Just jclaims) configJwtRoleClaimKey) walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value walkJSPath x [] = x walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o) rest walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest walkJSPath _ _ = Nothing unquoted :: JSON.Value -> Text unquoted (JSON.String t) = t unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v -- impossible case - just added to please -Wincomplete-patterns parseClaims _ _ = AuthResult { authClaims = M.empty, authRole = mempty } -- | Validate authorization header. -- Parse and store JWT claims for future use in the request. middleware :: AppState -> Wai.Middleware middleware appState app req respond = do conf <- getConfig appState time <- getTime appState let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req) claims <- runExceptT $ parseToken conf (LBS.fromStrict token) time let authResult = mapRight (parseClaims conf) claims req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult } app req' respond authResultKey :: Vault.Key (Either Error AuthResult) authResultKey = unsafePerformIO Vault.newKey {-# NOINLINE authResultKey #-} getResult :: Wai.Request -> Maybe (Either Error AuthResult) getResult = Vault.lookup authResultKey . Wai.vault getRole :: Wai.Request -> Maybe Text getRole req = authRole <$> (rightToMaybe =<< getResult req)