{-# LANGUAGE OverloadedStrings #-}
module JwtMiddleware where
import Control.Applicative
import Control.Monad
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as SBS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List as List
import qualified Data.Text as Text
import Data.Time.Clock.POSIX (POSIXTime)
import qualified Data.Time.Clock.POSIX as Clock
import qualified Network.HTTP.Types as Http
import qualified Network.Wai as Wai
import qualified Web.JWT as JWT
import AccessControl
import JwtAuth
import Store (Path)
data AuthError
= TokenError TokenError
| OperationNotAllowed
data AuthResult
= AuthRejected AuthError
| AuthAccepted
isRequestAuthorized :: Http.RequestHeaders -> Http.Query -> POSIXTime -> Maybe JWT.Signer -> Path -> AccessMode -> AuthResult
isRequestAuthorized headers query now maybeSecret path mode =
case getRequestClaim headers query now maybeSecret of
Left err -> AuthRejected (TokenError err)
Right claim | isAuthorizedByClaim claim path mode
-> AuthAccepted
| otherwise
-> AuthRejected OperationNotAllowed
getRequestClaim :: Http.RequestHeaders -> Http.Query -> POSIXTime -> Maybe JWT.Signer -> Either TokenError IcepeakClaim
getRequestClaim headers query now maybeSecret =
let getTokenBytes = maybe (Left $ VerificationError TokenNotFound) Right (findTokenBytes headers query)
in case maybeSecret of
Nothing ->
getTokenBytes >>= extractClaimUnverified
Just secret -> getTokenBytes >>= extractClaim now secret
findTokenBytes :: Http.RequestHeaders -> Http.Query -> Maybe SBS.ByteString
findTokenBytes headers query = headerToken headers <|> queryToken query
headerToken :: Http.RequestHeaders -> Maybe SBS.ByteString
headerToken =
SBS.stripPrefix "Bearer " <=< List.lookup Http.hAuthorization
queryToken :: Http.Query -> Maybe SBS.ByteString
queryToken = join . lookup "access_token"
instance Aeson.ToJSON AuthError where
toJSON aerr = case aerr of
TokenError terr -> case terr of
ClaimError ce -> Aeson.object [ "error" .= ce ]
VerificationError ve | ve `elem` [TokenInvalid, TokenNotFound]
-> Aeson.object [ "error" .= Text.pack "invalid token format" ]
_ -> Aeson.object [ "data" .= Aeson.Null ]
OperationNotAllowed -> Aeson.object [ "error" .= Text.pack "not allowed" ]
errorResponseBody :: AuthError -> LBS.ByteString
errorResponseBody = Aeson.encode
jwtMiddleware :: Maybe JWT.Signer -> Wai.Application -> Wai.Application
jwtMiddleware secret app req respond = do
now <- Clock.getPOSIXTime
case getRequestClaim headers query now secret of
Left err -> rejectUnauthorized (TokenError err)
Right claim | isAuthorized claim -> app req respond
| otherwise -> rejectUnauthorized OperationNotAllowed
where
path = Wai.pathInfo req
query = Wai.queryString req
headers = Wai.requestHeaders req
maybeMode | Wai.requestMethod req == Http.methodGet = Just ModeRead
| Wai.requestMethod req == Http.methodPut = Just ModeWrite
| Wai.requestMethod req == Http.methodDelete = Just ModeWrite
| otherwise = Nothing
isAuthorized claim = maybe False (isAuthorizedByClaim claim path) maybeMode
rejectUnauthorized err = respond $ Wai.responseLBS
Http.unauthorized401
[(Http.hContentType, "application/json")]
(Aeson.encode err)