git.iko.soy/typeable/servant
on master help sign in
Merge pull request #376 from haskell-servant/auth-gen

Generalized Auth Support

commit16e7234ab1fb3a46b79a8eab1db5cae20b05aa48 authorAaron Levin <vilevin@gmail.com> dateWed 9 Mar 2016, 23:27 (11 years ago) parent51dbd82b3af5a8
17 files changed+334−76
servant-client/CHANGELOG.md+2 servant-client/servant-client.cabal+1 servant-client/src/Servant/Client.hs+12−1 servant-client/src/Servant/Client/Experimental/Auth.hs+36 servant-client/test/Servant/ClientSpec.hs+51−6 servant-examples/auth-combinator/auth-combinator.hs+89−61 servant-examples/servant-examples.cabal+2 servant-server/CHANGELOG.md+1 servant-server/servant-server.cabal+2−1 servant-server/src/Servant/Server.hs+5−1 servant-server/src/Servant/Server/Experimental/Auth.hs+66 servant-server/src/Servant/Server/Internal.hs+3−2 servant-server/test/Servant/ServerSpec.hs+42−4 servant/CHANGELOG.md+1 servant/servant.cabal+1 servant/src/Servant/API.hs+6 servant/src/Servant/API/Experimental/Auth.hs+14
servant-client/CHANGELOG.md+2
@@ -9,6 +9,8 @@ HEAD
99* Client functions now consider any 2xx successful.
1010* Remove matrix params.
1111* Added support for Basic authentication
12+* Add generalized authentication support via the `AuthClientData` type family and
13+ `AuthenticateReq` data type
1214
13150.4.1
1416-----
servant-client/servant-client.cabal+1
@@ -27,6 +27,7 @@ source-repository head
2727library
2828 exposed-modules:
2929 Servant.Client
30+ Servant.Client.Experimental.Auth
3031 Servant.Common.BaseUrl
3132 Servant.Common.BasicAuth
3233 Servant.Common.Req
servant-client/src/Servant/Client.hs+12−1
@@ -15,8 +15,11 @@
1515-- querying functions for each endpoint just from the type representing your
1616-- API.
1717module Servant.Client
18− ( client
18+ ( AuthClientData
19+ , AuthenticateReq(..)
20+ , client
1921 , HasClient(..)
22+ , mkAuthenticateReq
2023 , ServantError(..)
2124 , module Servant.Common.BaseUrl
2225 ) where
@@ -36,6 +39,7 @@ import Network.HTTP.Media
3639import qualified Network.HTTP.Types as H
3740import qualified Network.HTTP.Types.Header as HTTP
3841import Servant.API
42+import Servant.Client.Experimental.Auth
3943import Servant.Common.BaseUrl
4044import Servant.Common.BasicAuth
4145import Servant.Common.Req
@@ -424,6 +428,13 @@ instance HasClient subapi =>
424428 type Client (WithNamedContext name context subapi) = Client subapi
425429 clientWithRoute Proxy = clientWithRoute (Proxy :: Proxy subapi)
426430
431+instance ( HasClient api
432+ ) => HasClient (AuthProtect tag :> api) where
433+ type Client (AuthProtect tag :> api)
434+ = AuthenticateReq (AuthProtect tag) -> Client api
435+
436+ clientWithRoute Proxy req baseurl manager (AuthenticateReq (val,func)) =
437+ clientWithRoute (Proxy :: Proxy api) (func val req) baseurl manager
427438
428439-- * Basic Authentication
429440
servant-client/src/Servant/Client/Experimental/Auth.hsnew file+36
@@ -0,0 +1,36 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE TypeSynonymInstances #-}
3+{-# LANGUAGE TypeFamilies #-}
4+
5+-- | Authentication for clients
6+
7+module Servant.Client.Experimental.Auth (
8+ AuthenticateReq(AuthenticateReq, unAuthReq)
9+ , AuthClientData
10+ , mkAuthenticateReq
11+ ) where
12+
13+import Servant.Common.Req (Req)
14+
15+-- | For a resource protected by authentication (e.g. AuthProtect), we need
16+-- to provide the client with some data used to add authentication data
17+-- to a request
18+--
19+-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
20+type family AuthClientData a :: *
21+
22+-- | For better type inference and to avoid usage of a data family, we newtype
23+-- wrap the combination of some 'AuthClientData' and a function to add authentication
24+-- data to a request
25+--
26+-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
27+newtype AuthenticateReq a =
28+ AuthenticateReq { unAuthReq :: (AuthClientData a, AuthClientData a -> Req -> Req) }
29+
30+-- | Handy helper to avoid wrapping datatypes in tuples everywhere.
31+--
32+-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
33+mkAuthenticateReq :: AuthClientData a
34+ -> (AuthClientData a -> Req -> Req)
35+ -> AuthenticateReq a
36+mkAuthenticateReq val func = AuthenticateReq (val, func)
servant-client/test/Servant/ClientSpec.hs+51−6
@@ -12,6 +12,7 @@
1212{-# LANGUAGE RecordWildCards #-}
1313{-# LANGUAGE StandaloneDeriving #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE TypeFamilies #-}
1516{-# LANGUAGE TypeOperators #-}
1617{-# LANGUAGE UndecidableInstances #-}
1718{-# OPTIONS_GHC -fcontext-stack=100 #-}
@@ -41,7 +42,8 @@ import Network.HTTP.Media
4142import Network.HTTP.Types (Status (..), badRequest400,
4243 methodGet, ok200, status400)
4344import Network.Socket
44−import Network.Wai (Application, responseLBS)
45+import Network.Wai (Application, Request,
46+ requestHeaders, responseLBS)
4547import Network.Wai.Handler.Warp
4648import System.IO.Unsafe (unsafePerformIO)
4749import Test.Hspec
@@ -53,6 +55,8 @@ import Servant.API
5355import Servant.API.Internal.Test.ComprehensiveAPI
5456import Servant.Client
5557import Servant.Server
58+import Servant.Server.Experimental.Auth
59+import qualified Servant.Common.Req as SCR
5660
5761-- This declaration simply checks that all instances are in place.
5862_ = client comprehensiveAPI
@@ -63,6 +67,7 @@ spec = describe "Servant.Client" $ do
6367 failSpec
6468 wrappedApiSpec
6569 basicAuthSpec
70+ genAuthSpec
6671
6772-- * test data types
6873
@@ -149,8 +154,7 @@ failServer = serve failApi (
149154 :<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
150155 )
151156
152−
153−-- * auth stuff
157+-- * basic auth stuff
154158
155159type BasicAuthAPI =
156160 BasicAuth "foo-realm" () :> "private" :> "basic" :> Get '[JSON] Person
@@ -166,11 +170,35 @@ basicAuthHandler =
166170 else return Unauthorized
167171 in BasicAuthCheck check
168172
169−serverContext :: Context '[ BasicAuthCheck () ]
170−serverContext = basicAuthHandler :. EmptyContext
173+basicServerContext :: Context '[ BasicAuthCheck () ]
174+basicServerContext = basicAuthHandler :. EmptyContext
171175
172176basicAuthServer :: Application
173−basicAuthServer = serveWithContext basicAuthAPI serverContext (const (return alice))
177+basicAuthServer = serveWithContext basicAuthAPI basicServerContext (const (return alice))
178+
179+-- * general auth stuff
180+
181+type GenAuthAPI =
182+ AuthProtect "auth-tag" :> "private" :> "auth" :> Get '[JSON] Person
183+
184+genAuthAPI :: Proxy GenAuthAPI
185+genAuthAPI = Proxy
186+
187+type instance AuthServerData (AuthProtect "auth-tag") = ()
188+type instance AuthClientData (AuthProtect "auth-tag") = ()
189+
190+genAuthHandler :: AuthHandler Request ()
191+genAuthHandler =
192+ let handler req = case lookup "AuthHeader" (requestHeaders req) of
193+ Nothing -> throwE (err401 { errBody = "Missing auth header" })
194+ Just _ -> return ()
195+ in mkAuthHandler handler
196+
197+genAuthServerContext :: Context '[ AuthHandler Request () ]
198+genAuthServerContext = genAuthHandler :. EmptyContext
199+
200+genAuthServer :: Application
201+genAuthServer = serveWithContext genAuthAPI genAuthServerContext (const (return alice))
174202
175203{-# NOINLINE manager #-}
176204manager :: C.Manager
@@ -333,6 +361,23 @@ basicAuthSpec = beforeAll (startWaiApp basicAuthServer) $ afterAll endWaiApp $ d
333361 Left FailureResponse{..} <- runExceptT (getBasic basicAuthData)
334362 responseStatus `shouldBe` Status 403 "Forbidden"
335363
364+genAuthSpec :: Spec
365+genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do
366+ context "Authentication works when requests are properly authenticated" $ do
367+
368+ it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do
369+ let getProtected = client genAuthAPI baseUrl manager
370+ let authRequest = mkAuthenticateReq () (\_ req -> SCR.addHeader "AuthHeader" ("cool" :: String) req)
371+ (left show <$> runExceptT (getProtected authRequest)) `shouldReturn` Right alice
372+
373+ context "Authentication is rejected when requests are not authenticated properly" $ do
374+
375+ it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do
376+ let getProtected = client genAuthAPI baseUrl manager
377+ let authRequest = mkAuthenticateReq () (\_ req -> SCR.addHeader "Wrong" ("header" :: String) req)
378+ Left FailureResponse{..} <- runExceptT (getProtected authRequest)
379+ responseStatus `shouldBe` (Status 401 "Unauthorized")
380+
336381-- * utils
337382
338383startWaiApp :: Application -> IO (ThreadId, BaseUrl)
servant-examples/auth-combinator/auth-combinator.hs+89−61
@@ -9,56 +9,54 @@
99{-# LANGUAGE TypeOperators #-}
1010{-# LANGUAGE UndecidableInstances #-}
1111
12−import Data.Aeson
13−import Data.ByteString (ByteString)
14−import Data.IORef
15−import Data.Text (Text)
12+import Control.Monad.Trans.Except (ExceptT, throwE)
13+import Data.Aeson hiding ((.:))
14+import Data.ByteString (ByteString)
15+import Data.Monoid ((<>))
16+import Data.Map (Map, fromList)
17+import qualified Data.Map as Map
18+import Data.Text (Text)
1619import GHC.Generics
1720import Network.Wai
1821import Network.Wai.Handler.Warp
1922import Servant
20−import Servant.Server.Internal
21−
22−-- Pretty much stolen/adapted from
23−-- https://github.com/haskell-servant/HaskellSGMeetup2015/blob/master/examples/authentication-combinator/AuthenticationCombinator.hs
24−
25−type DBConnection = IORef [ByteString]
26−type DBLookup = DBConnection -> ByteString -> IO Bool
27−
28−initDB :: IO DBConnection
29−initDB = newIORef ["good password"]
30−
31−isGoodCookie :: DBLookup
32−isGoodCookie ref password = do
33− allowed <- readIORef ref
34− return (password `elem` allowed)
35−
36−data AuthProtected
37−
38−instance (HasContextEntry context DBConnection, HasServer rest context)
39− => HasServer (AuthProtected :> rest) context where
40−
41− type ServerT (AuthProtected :> rest) m = ServerT rest m
42−
43− route Proxy context subserver = WithRequest $ \ request ->
44− route (Proxy :: Proxy rest) context $ addAcceptCheck subserver $ cookieCheck request
45− where
46− cookieCheck req = case lookup "Cookie" (requestHeaders req) of
47− Nothing -> return $ FailFatal err401 { errBody = "Missing auth header" }
48− Just v -> do
49− let dbConnection = getContextEntry context
50− authGranted <- isGoodCookie dbConnection v
51− if authGranted
52− then return $ Route ()
53− else return $ FailFatal err403 { errBody = "Invalid cookie" }
54−
55−type PrivateAPI = Get '[JSON] [PrivateData]
56−
57−type PublicAPI = Get '[JSON] [PublicData]
58−
59−type API = "private" :> AuthProtected :> PrivateAPI
60− :<|> PublicAPI
61−
23+import Servant.Server.Experimental.Auth
24+
25+-- | This file contains an authenticated server using servant's generalized
26+-- authentication support. Our basic authentication scheme is trivial: we
27+-- look for a cookie named "servant-auth-cookie" and its value will contain
28+-- a key, which we use to lookup a User. Obviously this is an absurd example,
29+-- but we pick something simple and non-standard to show you how to extend
30+-- servant's support for authentication.
31+
32+-- | A user type that we "fetch from the database" after
33+-- performing authentication
34+newtype User = User { unUser :: Text }
35+
36+-- | A (pure) database mapping keys to users.
37+database :: Map ByteString User
38+database = fromList [ ("key1", User "Anne Briggs")
39+ , ("key2", User "Bruce Cockburn")
40+ , ("key3", User "Ghédalia Tazartès")
41+ ]
42+
43+-- | A method that, when given a password, will return a User.
44+-- This is our bespoke (and bad) authentication logic.
45+lookupUser :: ByteString -> ExceptT ServantErr IO User
46+lookupUser key = case Map.lookup key database of
47+ Nothing -> throwE (err403 { errBody = "Invalid Cookie" })
48+ Just usr -> return usr
49+
50+-- | The auth handler wraps a function from Request -> ExceptT ServantErr IO User
51+-- we look for a Cookie and pass the value of the cookie to `lookupUser`.
52+authHandler :: AuthHandler Request User
53+authHandler =
54+ let handler req = case lookup "servant-auth-cookie" (requestHeaders req) of
55+ Nothing -> throwE (err401 { errBody = "Missing auth header" })
56+ Just authCookieKey -> lookupUser authCookieKey
57+ in mkAuthHandler handler
58+
59+-- | Data types that will be returned from various api endpoints
6260newtype PrivateData = PrivateData { ssshhh :: Text }
6361 deriving (Eq, Show, Generic)
6462
@@ -69,28 +67,58 @@ newtype PublicData = PublicData { somedata :: Text }
6967
7068instance ToJSON PublicData
7169
70+-- | Our private API that we want to be auth-protected.
71+type PrivateAPI = Get '[JSON] [PrivateData]
72+
73+-- | Our public API that doesn't have any protection
74+type PublicAPI = Get '[JSON] [PublicData]
75+
76+-- | Our API, with auth-protection
77+type API = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
78+ :<|> "public" :> PublicAPI
79+
80+-- | A value holding our type-level API
7281api :: Proxy API
7382api = Proxy
7483
84+-- | We need to specify the data returned after authentication
85+type instance AuthServerData (AuthProtect "cookie-auth") = User
86+
87+-- | The context that will be made available to request handlers. We supply the
88+-- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance
89+-- of 'AuthProtect' can extract the handler and run it on the request.
90+serverContext :: Context (AuthHandler Request User ': '[])
91+serverContext = authHandler :. EmptyContext
92+
93+-- | Our API, where we provide all the author-supplied handlers for each end
94+-- point. Note that 'privateDataFunc' is a function that takes 'User' as an
95+-- argument. We dont' worry about the authentication instrumentation here,
96+-- that is taken care of by supplying context
7597server :: Server API
76−server = return prvdata :<|> return pubdata
98+server = privateDataFunc :<|> return publicData
7799
78− where prvdata = [PrivateData "this is a secret"]
79− pubdata = [PublicData "this is a public piece of data"]
100+ where privateDataFunc (User name) =
101+ return [PrivateData ("this is a secret: " <> name)]
102+ publicData = [PublicData "this is a public piece of data"]
80103
104+-- | run our server
81105main :: IO ()
82−main = do
83− dbConnection <- initDB
84− let context = dbConnection :. EmptyContext
85− run 8080 (serveWithContext api context server)
106+main = run 8080 (serveWithContext api serverContext server)
107+
108+{- Sample Session:
109+
110+$ curl -XGET localhost:8080/private
111+Missing auth header
112+>>>>>>> modify auth-combinator example for gen auth
113+>>>>>>> 8246c1f... modify auth-combinator example for gen auth
86114
87−{- Sample session:
88−$ curl http://localhost:8080/
115+$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
116+[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
117+
118+$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
119+Invalid Cookie
120+
121+$ curl -XGET localhost:8080/public
89122[{"somedata":"this is a public piece of data"}]
90−$ curl http://localhost:8080/private
91−Missing auth header.
92−$ curl -H "Cookie: good password" http://localhost:8080/private
93−[{"ssshhh":"this is a secret"}]
94−$ curl -H "Cookie: bad password" http://localhost:8080/private
95−Invalid cookie.
96123-}
124+
servant-examples/servant-examples.cabal+2
@@ -112,10 +112,12 @@ executable auth-combinator
112112 aeson >= 0.8
113113 , base >= 4.7 && < 5
114114 , bytestring
115+ , containers
115116 , http-types
116117 , servant == 0.5.*
117118 , servant-server == 0.5.*
118119 , text
120+ , transformers
119121 , wai
120122 , warp
121123 hs-source-dirs: auth-combinator
servant-server/CHANGELOG.md+1
@@ -12,6 +12,7 @@ HEAD
1212* Redefined constructors of `RouteResult`.
1313* Added `Delayed` and related functions (`addMethodCheck`, `addAcceptCheck`, `addBodyCheck`, `runDelayed`)
1414* Added support for Basic Authentication
15+* Add generalized authentication support via the `AuthServerData` type family and `AuthHandler` handler
1516
16170.4.1
1718-----
servant-server/servant-server.cabal+2−1
@@ -36,9 +36,10 @@ library
3636 exposed-modules:
3737 Servant
3838 Servant.Server
39+ Servant.Server.Experimental.Auth
3940 Servant.Server.Internal
40− Servant.Server.Internal.Context
4141 Servant.Server.Internal.BasicAuth
42+ Servant.Server.Internal.Context
4243 Servant.Server.Internal.Enter
4344 Servant.Server.Internal.Router
4445 Servant.Server.Internal.RoutingApplication
servant-server/src/Servant/Server.hs+5−1
@@ -45,11 +45,15 @@ module Servant.Server
4545 , NamedContext(..)
4646 , descendIntoNamedContext
4747
48−
4948 -- * Basic Authentication
5049 , BasicAuthCheck(BasicAuthCheck, unBasicAuthCheck)
5150 , BasicAuthResult(..)
5251
52+ -- * General Authentication
53+ -- , AuthHandler(unAuthHandler)
54+ -- , AuthServerData
55+ -- , mkAuthHandler
56+
5357 -- * Default error type
5458 , ServantErr(..)
5559 -- ** 3XX
servant-server/src/Servant/Server/Experimental/Auth.hsnew file+66
@@ -0,0 +1,66 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE PolyKinds #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TypeFamilies #-}
10+{-# LANGUAGE TypeOperators #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+
13+module Servant.Server.Experimental.Auth where
14+
15+import Control.Monad.Trans.Except (ExceptT,
16+ runExceptT)
17+import Data.Proxy (Proxy (Proxy))
18+import Data.Typeable (Typeable)
19+import GHC.Generics (Generic)
20+import Network.Wai (Request)
21+
22+import Servant ((:>))
23+import Servant.API.Experimental.Auth
24+import Servant.Server.Internal (HasContextEntry,
25+ HasServer, ServerT,
26+ getContextEntry,
27+ route)
28+import Servant.Server.Internal.Router (Router' (WithRequest))
29+import Servant.Server.Internal.RoutingApplication (RouteResult (FailFatal, Route),
30+ addAuthCheck)
31+import Servant.Server.Internal.ServantErr (ServantErr)
32+
33+-- * General Auth
34+
35+-- | Specify the type of data returned after we've authenticated a request.
36+-- quite often this is some `User` datatype.
37+--
38+-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
39+type family AuthServerData a :: *
40+
41+-- | Handlers for AuthProtected resources
42+--
43+-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
44+newtype AuthHandler r usr = AuthHandler
45+ { unAuthHandler :: r -> ExceptT ServantErr IO usr }
46+ deriving (Generic, Typeable)
47+
48+-- | NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
49+mkAuthHandler :: (r -> ExceptT ServantErr IO usr) -> AuthHandler r usr
50+mkAuthHandler = AuthHandler
51+
52+-- | Known orphan instance.
53+instance ( HasServer api context
54+ , HasContextEntry context (AuthHandler Request (AuthServerData (AuthProtect tag)))
55+ )
56+ => HasServer (AuthProtect tag :> api) context where
57+
58+ type ServerT (AuthProtect tag :> api) m =
59+ AuthServerData (AuthProtect tag) -> ServerT api m
60+
61+ route Proxy context subserver = WithRequest $ \ request ->
62+ route (Proxy :: Proxy api) context (subserver `addAuthCheck` authCheck request)
63+ where
64+ authHandler = unAuthHandler (getContextEntry context)
65+ authCheck = fmap (either FailFatal Route) . runExceptT . authHandler
66+
servant-server/src/Servant/Server/Internal.hs+3−2
@@ -453,8 +453,6 @@ instance HasServer api context => HasServer (HttpVersion :> api) context where
453453 route Proxy context subserver = WithRequest $ \req ->
454454 route (Proxy :: Proxy api) context (passToServer subserver $ httpVersion req)
455455
456−-- * Basic Authentication
457−
458456-- | Basic Authentication
459457instance ( KnownSymbol realm
460458 , HasServer api context
@@ -482,6 +480,9 @@ pathIsEmpty = go . pathInfo
482480ct_wildcard :: B.ByteString
483481ct_wildcard = "*" <> "/" <> "*" -- Because CPP
484482
483+-- * General Authentication
484+
485+
485486-- * contexts
486487
487488instance (HasContextEntry context (NamedContext name subContext), HasServer subApi subContext)
servant-server/test/Servant/ServerSpec.hs+42−4
@@ -31,14 +31,15 @@ import Network.HTTP.Types (Status (..), hAccept, hContentType,
3131 methodHead, methodPatch,
3232 methodPost, methodPut, ok200,
3333 parseQuery)
34−import Network.Wai (Application, Request, pathInfo,
34+import Network.Wai (Application, Request, requestHeaders, pathInfo,
3535 queryString, rawQueryString,
3636 responseBuilder, responseLBS)
3737import Network.Wai.Internal (Response (ResponseBuilder))
3838import Network.Wai.Test (defaultRequest, request,
3939 runSession, simpleBody,
4040 simpleHeaders, simpleStatus)
41−import Servant.API ((:<|>) (..), (:>), BasicAuth, BasicAuthData(BasicAuthData),
41+import Servant.API ((:<|>) (..), (:>), AuthProtect,
42+ BasicAuth, BasicAuthData(BasicAuthData),
4243 Capture, Delete, Get, Header (..),
4344 Headers, HttpVersion,
4445 IsSecure (..), JSON,
@@ -48,7 +49,7 @@ import Servant.API ((:<|>) (..), (:>), BasicAuth, Basic
4849 Raw, RemoteHost, ReqBody,
4950 StdMethod (..), Verb, addHeader)
5051import Servant.API.Internal.Test.ComprehensiveAPI
51−import Servant.Server (ServantErr (..), Server, err404,
52+import Servant.Server (ServantErr (..), Server, err401, err404,
5253 serve, serveWithContext, Context((:.), EmptyContext))
5354import Test.Hspec (Spec, context, describe, it,
5455 shouldBe, shouldContain)
@@ -59,6 +60,9 @@ import Test.Hspec.Wai (get, liftIO, matchHeaders,
5960
6061import Servant.Server.Internal.BasicAuth (BasicAuthCheck(BasicAuthCheck),
6162 BasicAuthResult(Authorized,Unauthorized))
63+import Servant.Server.Experimental.Auth
64+ (AuthHandler, AuthServerData,
65+ mkAuthHandler)
6266import Servant.Server.Internal.RoutingApplication
6367 (toApplication, RouteResult(..))
6468import Servant.Server.Internal.Router
@@ -90,6 +94,7 @@ spec = do
9094 routerSpec
9195 miscCombinatorSpec
9296 basicAuthSpec
97+ genAuthSpec
9398
9499------------------------------------------------------------------------------
95100-- * verbSpec {{{
@@ -534,7 +539,7 @@ miscCombinatorSpec = with (return $ serve miscApi miscServ) $
534539
535540-- }}}
536541------------------------------------------------------------------------------
537−-- * Authentication {{{
542+-- * Basic Authentication {{{
538543------------------------------------------------------------------------------
539544
540545type BasicAuthAPI = BasicAuth "foo" () :> "basic" :> Get '[JSON] Animal
@@ -564,6 +569,39 @@ basicAuthSpec = do
564569 it "returns 200 with the right password" $ do
565570 THW.request methodGet "/basic" [("Authorization","Basic c2VydmFudDpzZXJ2ZXI=")] "" `shouldRespondWith` 200
566571
572+-- }}}
573+------------------------------------------------------------------------------
574+-- * General Authentication {{{
575+------------------------------------------------------------------------------
576+
577+type GenAuthAPI = AuthProtect "auth" :> "auth" :> Get '[JSON] Animal
578+authApi :: Proxy GenAuthAPI
579+authApi = Proxy
580+authServer :: Server GenAuthAPI
581+authServer = const (return tweety)
582+
583+type instance AuthServerData (AuthProtect "auth") = ()
584+
585+genAuthContext :: Context '[ AuthHandler Request () ]
586+genAuthContext =
587+ let authHandler = (\req ->
588+ if elem ("Auth", "secret") (requestHeaders req)
589+ then return ()
590+ else throwE err401
591+ )
592+ in mkAuthHandler authHandler :. EmptyContext
593+
594+genAuthSpec :: Spec
595+genAuthSpec = do
596+ describe "Servant.API.Auth" $ do
597+ with (return (serveWithContext authApi genAuthContext authServer)) $ do
598+
599+ context "Custom Auth Protection" $ do
600+ it "returns 401 when missing headers" $ do
601+ get "/auth" `shouldRespondWith` 401
602+ it "returns 200 with the right header" $ do
603+ THW.request methodGet "/auth" [("Auth","secret")] "" `shouldRespondWith` 200
604+
567605-- }}}
568606------------------------------------------------------------------------------
569607-- * Test data types {{{
servant/CHANGELOG.md+1
@@ -11,6 +11,7 @@ HEAD
1111* Add new `Verbs` combinator, and make all existing and new verb combinators
1212type synonyms of it.
1313* Add `BasicAuth` combinator to support Basic authentication
14+* Add generalized authentication support
1415
15160.4.2
1617-----
servant/servant.cabal+1
@@ -30,6 +30,7 @@ library
3030 Servant.API.BasicAuth
3131 Servant.API.Capture
3232 Servant.API.ContentTypes
33+ Servant.API.Experimental.Auth
3334 Servant.API.Header
3435 Servant.API.HttpVersion
3536 Servant.API.Internal.Test.ComprehensiveAPI
servant/src/Servant/API.hs+6
@@ -48,6 +48,11 @@ module Servant.API (
4848 module Web.HttpApiData,
4949 -- | Classes and instances for types that can be converted to and from HTTP API data.
5050
51+
52+ -- * Experimental modules
53+ module Servant.API.Experimental.Auth,
54+ -- | General Authentication
55+
5156 -- * Utilities
5257 module Servant.Utils.Links,
5358 -- | Type-safe internal URIs
@@ -61,6 +66,7 @@ import Servant.API.ContentTypes (Accept (..), FormUrlEncoded,
6166 MimeRender (..), NoContent (NoContent),
6267 MimeUnrender (..), OctetStream,
6368 PlainText, ToFormUrlEncoded (..))
69+import Servant.API.Experimental.Auth (AuthProtect)
6470import Servant.API.Header (Header (..))
6571import Servant.API.HttpVersion (HttpVersion (..))
6672import Servant.API.IsSecure (IsSecure (..))
servant/src/Servant/API/Experimental/Auth.hsnew file+14
@@ -0,0 +1,14 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE KindSignatures #-}
4+{-# LANGUAGE PolyKinds #-}
5+module Servant.API.Experimental.Auth where
6+
7+import Data.Typeable (Typeable)
8+
9+-- | A generalized Authentication combinator. Use this if you have a
10+-- non-standard authentication technique.
11+--
12+-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE.
13+data AuthProtect (tag :: k) deriving (Typeable)
14+