Skip to content

Commit

Permalink
Add examples for the new experimental API
Browse files Browse the repository at this point in the history
  • Loading branch information
michivi committed Apr 21, 2022
1 parent 614fac0 commit a8ec6ca
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 17 deletions.
43 changes: 26 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@

Servant authentication with HMAC

## New experimental API notice

A new experimental API is being tested with the following features:

- Include the MD5 hashing of the request's content into the signature algorithm.
- Identify the user in the request and pick the appropriate secret key for
authentication.

The previous API is still available but may be deprecated and remove in future
versions.

### Note on large requests and streaming

The authentication relies on various information about the request such as its
body, more specifically the MD5 hash of the entire body. As a consequence, the
library will consume the request's content in its entirety before transfering it
to the underlying client or server. Thus, very large requests will be buffered
in-memory while hashing, and streaming won't work as expected as all the chunks
will be transfered at once only after signing. This is true whether the client /
server actually consumes the content or not. This library is therefore not
suited for those use cases.

Note that this also comes with a DoS risk as very large requests will be stored
in memory for signature and consumption. Users need to keep that in mind and
take the necessary precautions to prevent those.

## Example

In this section, we will introduce the client-server example.
Expand Down Expand Up @@ -122,20 +148,3 @@ 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
body, more specifically the MD5 hash of the entire body. As a consequence, the
library will consume the request's content in its entirety before transfering it
to the underlying client or server. Thus, very large requests will be buffered
in-memory while hashing, and streaming won't work as expected as all the chunks
will be transfered at once only after signing. This is true whether the client /
server actually consumes the content or not. This library is therefore not
suited for those use cases.

Note that this also comes with a DoS risk as very large requests will be stored
in memory for signature and consumption. Users need to keep that in mind and
take the necessary precautions to prevent those.
46 changes: 46 additions & 0 deletions examples/client/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module Main (main) where

import Data.Proxy
import Network.HTTP.Client (defaultManagerSettings, newManager)
import Servant.API
import Servant.Auth.Hmac.Crypto
import Servant.Auth.Hmac.Secure
import Servant.Client
import qualified Servant.Client.Core as Client

data MyUser
= UserA
| UserB
deriving stock (Show)

type MyApi =
HmacAuthed MyUser :> "messages" :> ReqBody '[PlainText] String :> Post '[PlainText] String
:<|> HmacAuthed MyUser :> "messages" :> Get '[PlainText] String

myApi :: Proxy MyApi
myApi = Proxy

postMessage :: String -> HmacClientM MyUser String
getMessages :: HmacClientM MyUser String
(postMessage :<|> getMessages) = hmacClient myApi

clientSideAuth :: HmacClientSideAuth MyUser
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 -> SecretKey "User-A-Secret"
UserB -> SecretKey "Secret-from-User-B"
}

main :: IO ()
main = do
manager <- newManager defaultManagerSettings
let clientEnv = mkClientEnv manager (BaseUrl Http "localhost" 8080 "")
postResponse <- runHmacClient (postMessage "Hello!") clientEnv clientSideAuth UserA
print postResponse
getResponse <- runHmacClient getMessages clientEnv clientSideAuth UserB
print getResponse
54 changes: 54 additions & 0 deletions examples/server/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Main (main) where

import Control.Monad.IO.Class
import qualified Network.Wai as Wai
import Network.Wai.Handler.Warp (run)
import Servant
import Servant.Auth.Hmac.Crypto
import Servant.Auth.Hmac.Secure

data MyUser
= UserA
| UserB
deriving stock (Show)

type MyApi =
HmacAuthed MyUser :> "messages" :> ReqBody '[PlainText] String :> Post '[PlainText] String
:<|> HmacAuthed MyUser :> "messages" :> Get '[PlainText] String

myApi :: Proxy MyApi
myApi = Proxy

postMessage :: MyUser -> String -> Handler String
postMessage usr msg = do
liftIO . putStrLn $ "Message from " <> show usr <> ": " <> msg
pure "Message printed!"

getMessages :: MyUser -> Handler String
getMessages usr = pure $ "Nothing else for " <> show usr

myServer :: Server MyApi
myServer = postMessage :<|> getMessages

serverSideAuth :: HmacServerSideAuth MyUser
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 -> SecretKey "User-A-Secret"
UserB -> SecretKey "Secret-from-User-B"
}

myApp :: Application
myApp =
serveWithContext
myApi
(serverSideAuth :. EmptyContext)
myServer

main :: IO ()
main = run 8080 myApp
30 changes: 30 additions & 0 deletions servant-hmac-auth.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ flag build-readme
default: False
manual: True

flag build-examples
description: Build the examples in the examples directory.
default: False
manual: True

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

Expand Down Expand Up @@ -103,6 +108,31 @@ executable readme
build-tool-depends: markdown-unlit:markdown-unlit
ghc-options: -pgmL markdown-unlit

executable examples-client
import: common-options
if !flag(build-examples)
buildable: False
main-is: Main.hs
hs-source-dirs: examples/client
build-depends: http-client
, servant
, servant-hmac-auth
, servant-client
, servant-client-core ^>= 0.18 || ^>= 0.19

executable examples-server
import: common-options
if !flag(build-examples)
buildable: False
main-is: Main.hs
hs-source-dirs: examples/server
build-depends: http-client
, servant
, servant-hmac-auth
, servant-server
, wai ^>= 3.2.2.1
, warp ^>= 3.3

test-suite servant-hmac-auth-test
import: common-options
type: exitcode-stdio-1.0
Expand Down

0 comments on commit a8ec6ca

Please sign in to comment.