Skip to content

Commit

Permalink
Update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
michivi committed Apr 21, 2022
1 parent abcf320 commit 614fac0
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 24 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@ In this section, we will introduce the client-server example.
To run it locally you can:

```shell
$ cabal new-build
$ cabal new-exec readme
cabal run readme
```

So,it will run this on your machine.

### Setting up
## Setting up

Since this tutorial is written using Literate Haskell, first, let's write all necessary pragmas and imports.

Expand Down Expand Up @@ -126,6 +123,8 @@ main = do
threadDelay $ 10 ^ (6 :: Int)
```

## New experimental API

### Note on large requests and streaming

The authentication relies on various information about the request such as its
Expand Down
19 changes: 19 additions & 0 deletions servant-hmac-auth.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ source-repository head
type: git
location: https://github.com/holmusk/servant-hmac-auth.git

flag build-readme
description: Build the example in the README.md file.
default: False
manual: True

common common-options
build-depends: base >= 4.11.1.0 && < 4.16

Expand Down Expand Up @@ -83,6 +88,20 @@ library
, servant-server ^>= 0.18 || ^>= 0.19
, transformers ^>= 0.5
, wai ^>= 3.2.2.1
executable readme
import: common-options
if !flag(build-readme)
buildable: False
main-is: README.lhs
build-depends: aeson >= 1.4 && < 1.6
, http-client
, servant
, servant-hmac-auth
, servant-client
, servant-server
, warp ^>= 3.3.5
build-tool-depends: markdown-unlit:markdown-unlit
ghc-options: -pgmL markdown-unlit

test-suite servant-hmac-auth-test
import: common-options
Expand Down
104 changes: 85 additions & 19 deletions src/Servant/Auth/Hmac/Secure.hs
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
{-# LANGUAGE AllowAmbiguousTypes #-}

{- |
Servant combinator and functions for HMAC authentication of requests.
Module : Control.Monad.IO.Class
Copyright : (c) Holmusk 2022
License : MIT (see the file LICENSE)
Stability : experimental
Servant combinator and operations for HMAC authentication of requests.
IMPORTANT NOTE: the HMAC authentication scheme requires hashing the entirety of
the content. The later is still required though by the backend for consumption.
To that end, it will be retained in-memory. Users need to keep that in mind, and
take the necessary precautions to prevent any DoS attacks by throwing very large
payloads to the protected endpoint.
-}
module Servant.Auth.Hmac.Secure (
-- * Servant combinator
-- * The HmacAuthed Servant combinator
HmacAuthed,

-- * HMAC server
-- * HMAC server-side context
HmacServerSideAuth (..),

-- * HMAC client
-- * HMAC client-side context and operations
HmacClientM,
HmacClientSideAuth (..),
hmacClient,
runHmacClient,

-- * Miscellaneous
-- * Error management
HmacSignatureException (..),
) where

Expand All @@ -30,8 +42,8 @@ import Data.Sequence ((|>))
import qualified Data.Sequence as Seq
import Network.HTTP.Types.Header (HeaderName, hHost)
import qualified Network.Wai as Wai
import Servant (HasContextEntry (getContextEntry), HasLink (MkLink, toLink), HasServer (ServerT), Proxy (Proxy), ServerError (errBody), err401, type (:>))
import Servant.Auth.Hmac.Crypto (SecretKey, Signature (..), requestSignature, verifySignatureHmac)
import Servant (HasContextEntry (getContextEntry), HasLink (MkLink, toLink), HasServer (ServerT), PlainText, Post, Proxy (Proxy), ReqBody, ServerError (errBody), err401, type (:<|>), type (:>))
import Servant.Auth.Hmac.Crypto (SecretKey, Signature (..), requestSignature, signSHA256, verifySignatureHmac)
import Servant.Auth.Hmac.Internal (normalizedHostFromUrl, servantDuplicateRequestBody, servantRequestToPayload, waiDuplicateRequestBody, waiRequestToPayload)
import Servant.Client (BaseUrl, ClientEnv (baseUrl), ClientError (ConnectionError), ClientM, HasClient (Client, hoistClientMonad), runClientM)
import Servant.Client.Core (RequestF (..), RunClient, clientIn)
Expand All @@ -46,13 +58,27 @@ import Servant.Server.Internal.RoutingApplication (RoutingApplication)

-- | Potential error during the HMAC authentication generation or verification.
newtype HmacSignatureException
= -- | The HMAC signature generation has failed during the request's body generation.
= -- | The HMAC signature generation has failed during the request's body
-- generation. This happen when request streaming has failed.
RequestBodyInspectionFailed String
deriving stock (Eq, Show)

instance Exception HmacSignatureException

{- Client-side HMAC authentication contextual information. -}
{- | Client-side HMAC authentication contextual information.
>>> :{
clientSideAuth = HmacClientSideAuth
{ hcsaSign = signSHA256
, hcsaUserRequest = \case
UserA -> pure . Client.addHeader "X-User" ("user-a" :: String)
UserB -> pure . Client.addHeader "X-User" ("user-b" :: String)
, hcsaSecretKey = \case
UserA -> userASk
UserB -> userBSk
}
}
-}
data HmacClientSideAuth usr = HmacClientSideAuth
{ hcsaSign :: !(SecretKey -> ByteString -> Signature)
-- ^ Signing algorithm used for the authentication, taking in both the
Expand All @@ -63,7 +89,22 @@ data HmacClientSideAuth usr = HmacClientSideAuth
-- ^ User secret key used for the authentication.
}

{- Server-side HMAC authentication contextual information. -}
{- | Server-side HMAC authentication contextual information required to
authenticate the remote users using their secret key.
>>> :{
serverSideAuth = HmacServerSideAuth
{ hssaSign = signSHA256
, hssaIdentifyUser = \req -> case lookup "X-User" (Wai.requestHeaders req) of
Just "user-a" -> pure $ Just UserA
Just "user-b" -> pure $ Just UserB
_ -> pure Nothing
, hssaSecretKey = \case
UserA -> userASk
UserB -> userBSk
}
:}
-}
data HmacServerSideAuth usr = HmacServerSideAuth
{ hssaSign :: !(SecretKey -> ByteString -> Signature)
-- ^ Signing algorithm used for the authentication, taking in both the
Expand All @@ -80,29 +121,33 @@ data HmacServerSideAuth usr = HmacServerSideAuth
-- The secret key may change depending on the remote user.
}

{- | The HMAC authentication combinator.
{- | Combinator for HMAC authentication.
'usr' is the type of user that may be authenticated through HMAC. It is used to
communicate to the program the authenticated user and to retrieve their secret
key.
Example:
>>> data HelloUser = UserA | UserB
>>> type HelloApi = HmacAuthed HelloUser :> "hello" :> ReqBody '[PlainText] String :> Post '[PlainText] String
Note that the HMAC signature check is done very early on server-side as opposed
to in the regular delayed workflow. Indeed, the request's content needs to be
inspected and reinjected and that can only happen before the request is handled
because of lazyness (i.e. inspecting the content will consume it and make it
lost to the server backend). Checking for the signature before checking for
captures and method seems acceptable.
IMPORTANT NOTE: the HMAC authentication scheme requires hashing the entirety of
the content. The later is still required though by the backend for consumption.
To that end, it will be retained in-memory. Users need to keep that in mind, and
take the necessary precautions to prevent any DoS attacks by throwing very large
payloads to the protected endpoint.
'usr' is the type of user that may be authenticated through HMAC.
-}
data HmacAuthed (usr :: Type)

instance HasLink sub => HasLink (HmacAuthed usr :> sub) where
type MkLink (HmacAuthed usr :> sub) r = MkLink sub r
toLink toA _ = toLink toA (Proxy @sub)

{- | 'HmacClientM' is the monad in which HMAC compatible client functions run.
'usr' is the type of user that can authenticate against the remote server.
-}
newtype HmacClientM usr a = HmacClientM
{unHmacClientM :: ReaderT (HmacClientSideAuth usr, usr) ClientM a}
deriving (Functor, Applicative, Monad, MonadError ClientError, MonadIO, MonadReader (HmacClientSideAuth usr, usr))
Expand All @@ -114,9 +159,30 @@ instance RunClient (HmacClientM usr) where
liftClient :: ClientM a -> HmacClientM usr a
liftClient = HmacClientM . ReaderT . const

{- | Produce operations to query an API using the HMAC compatible client 'HmacClientM'.
> type MyApi = HmacAuthed () :> "messages" :> ReqBody '[PlainText] String :> Post '[PlainText] String
> :<|> HmacAuthed () :> "messages" :> Get '[PlainText] String
>
> myApi :: Proxy MyApi
> myApi = Proxy
>
> postMessage :: String -> HmacClientM HelloUser String
> getMessages :: HmacClientM HelloUser String
> (postMessage :<|> getMessages) = hmacClient myApi
-}
hmacClient :: forall api usr. HasClient (HmacClientM usr) api => Proxy api -> Client (HmacClientM usr) api
hmacClient _ = Proxy @api `clientIn` Proxy @(HmacClientM usr)

{- | Run the given 'HmacClientM' operation using the provided 'ClientEnv' environment,
'HmacClientSideAuth' 'usr' HMAC context and authenticate as 'usr'.
Examples:
>>> runHmacClient (postMessage "Hello!") clientEnv clientSideAuth UserA
To call an API without HMAC authentication, users have to fall back to 'runClientM'.
-}
runHmacClient :: HmacClientM usr a -> ClientEnv -> HmacClientSideAuth usr -> usr -> IO (Either ClientError a)
runHmacClient ma env hac usr = runClientM (runReaderT (unHmacClientM ma) (hac, usr)) env

Expand Down

0 comments on commit 614fac0

Please sign in to comment.