Add basic-auth support to servant-client
This commit is contained in:
parent
104ac29bf8
commit
d989d15e4c
4 changed files with 73 additions and 0 deletions
|
@ -28,11 +28,13 @@ library
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
Servant.Client
|
Servant.Client
|
||||||
Servant.Common.BaseUrl
|
Servant.Common.BaseUrl
|
||||||
|
Servant.Common.BasicAuth
|
||||||
Servant.Common.Req
|
Servant.Common.Req
|
||||||
build-depends:
|
build-depends:
|
||||||
base >=4.7 && <5
|
base >=4.7 && <5
|
||||||
, aeson
|
, aeson
|
||||||
, attoparsec
|
, attoparsec
|
||||||
|
, base64-bytestring
|
||||||
, bytestring
|
, bytestring
|
||||||
, exceptions
|
, exceptions
|
||||||
, http-api-data >= 0.1 && < 0.3
|
, http-api-data >= 0.1 && < 0.3
|
||||||
|
|
|
@ -37,6 +37,7 @@ import qualified Network.HTTP.Types as H
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.Common.BaseUrl
|
import Servant.Common.BaseUrl
|
||||||
|
import Servant.Common.BasicAuth
|
||||||
import Servant.Common.Req
|
import Servant.Common.Req
|
||||||
|
|
||||||
-- * Accessing APIs as a Client
|
-- * Accessing APIs as a Client
|
||||||
|
@ -424,6 +425,15 @@ instance HasClient subapi =>
|
||||||
clientWithRoute Proxy = clientWithRoute (Proxy :: Proxy subapi)
|
clientWithRoute Proxy = clientWithRoute (Proxy :: Proxy subapi)
|
||||||
|
|
||||||
|
|
||||||
|
-- * Basic Authentication
|
||||||
|
|
||||||
|
instance HasClient api => HasClient (BasicAuth realm usr :> api) where
|
||||||
|
type Client (BasicAuth realm usr :> api) = BasicAuthData -> Client api
|
||||||
|
|
||||||
|
clientWithRoute Proxy req baseurl manager val =
|
||||||
|
clientWithRoute (Proxy :: Proxy api) (basicAuthReq val req) baseurl manager
|
||||||
|
|
||||||
|
|
||||||
{- Note [Non-Empty Content Types]
|
{- Note [Non-Empty Content Types]
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Rather than have
|
Rather than have
|
||||||
|
|
21
servant-client/src/Servant/Common/BasicAuth.hs
Normal file
21
servant-client/src/Servant/Common/BasicAuth.hs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TypeSynonymInstances #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
|
-- | Basic Authentication for clients
|
||||||
|
|
||||||
|
module Servant.Common.BasicAuth (
|
||||||
|
basicAuthReq
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.ByteString.Base64 (encode)
|
||||||
|
import Data.Monoid ((<>))
|
||||||
|
import Data.Text.Encoding (decodeUtf8)
|
||||||
|
import Servant.Common.Req (addHeader, Req)
|
||||||
|
import Servant.API.BasicAuth (BasicAuthData(BasicAuthData))
|
||||||
|
|
||||||
|
-- | Authenticate a request using Basic Authentication
|
||||||
|
basicAuthReq :: BasicAuthData -> Req -> Req
|
||||||
|
basicAuthReq (BasicAuthData user pass) req =
|
||||||
|
let authText = decodeUtf8 ("Basic " <> encode (user <> ":" <> pass))
|
||||||
|
in addHeader "Authorization" authText req
|
|
@ -62,6 +62,7 @@ spec = describe "Servant.Client" $ do
|
||||||
sucessSpec
|
sucessSpec
|
||||||
failSpec
|
failSpec
|
||||||
wrappedApiSpec
|
wrappedApiSpec
|
||||||
|
basicAuthSpec
|
||||||
|
|
||||||
-- * test data types
|
-- * test data types
|
||||||
|
|
||||||
|
@ -148,6 +149,29 @@ failServer = serve failApi (
|
||||||
:<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
|
:<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
-- * auth stuff
|
||||||
|
|
||||||
|
type BasicAuthAPI =
|
||||||
|
BasicAuth "foo-realm" () :> "private" :> "basic" :> Get '[JSON] Person
|
||||||
|
|
||||||
|
basicAuthAPI :: Proxy BasicAuthAPI
|
||||||
|
basicAuthAPI = Proxy
|
||||||
|
|
||||||
|
basicAuthHandler :: BasicAuthCheck ()
|
||||||
|
basicAuthHandler =
|
||||||
|
let check (BasicAuthData username password) =
|
||||||
|
if username == "servant" && password == "server"
|
||||||
|
then return (Authorized ())
|
||||||
|
else return Unauthorized
|
||||||
|
in BasicAuthCheck check
|
||||||
|
|
||||||
|
serverConfig :: Config '[ BasicAuthCheck () ]
|
||||||
|
serverConfig = basicAuthHandler :. EmptyConfig
|
||||||
|
|
||||||
|
basicAuthServer :: Application
|
||||||
|
basicAuthServer = serve basicAuthAPI serverConfig (const (return alice))
|
||||||
|
|
||||||
{-# NOINLINE manager #-}
|
{-# NOINLINE manager #-}
|
||||||
manager :: C.Manager
|
manager :: C.Manager
|
||||||
manager = unsafePerformIO $ C.newManager C.defaultManagerSettings
|
manager = unsafePerformIO $ C.newManager C.defaultManagerSettings
|
||||||
|
@ -292,6 +316,22 @@ data WrappedApi where
|
||||||
HasClient api, Client api ~ ExceptT ServantError IO ()) =>
|
HasClient api, Client api ~ ExceptT ServantError IO ()) =>
|
||||||
Proxy api -> WrappedApi
|
Proxy api -> WrappedApi
|
||||||
|
|
||||||
|
basicAuthSpec :: Spec
|
||||||
|
basicAuthSpec = beforeAll (startWaiApp basicAuthServer) $ afterAll endWaiApp $ do
|
||||||
|
context "Authentication works when requests are properly authenticated" $ do
|
||||||
|
|
||||||
|
it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do
|
||||||
|
let getBasic = client basicAuthAPI baseUrl manager
|
||||||
|
let basicAuthData = BasicAuthData "servant" "server"
|
||||||
|
(left show <$> runExceptT (getBasic basicAuthData)) `shouldReturn` Right alice
|
||||||
|
|
||||||
|
context "Authentication is rejected when requests are not authenticated properly" $ do
|
||||||
|
|
||||||
|
it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do
|
||||||
|
let getBasic = client basicAuthAPI baseUrl manager
|
||||||
|
let basicAuthData = BasicAuthData "not" "password"
|
||||||
|
Left FailureResponse{..} <- runExceptT (getBasic basicAuthData)
|
||||||
|
responseStatus `shouldBe` Status 403 "Forbidden"
|
||||||
|
|
||||||
-- * utils
|
-- * utils
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue