2015-08-13 02:20:18 +02:00
|
|
|
-- | An example of a custom authentication framework that checks a Cookie for a
|
|
|
|
-- value.
|
2015-09-10 08:49:19 +02:00
|
|
|
|
2015-08-13 02:20:18 +02:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2015-12-25 00:25:15 +01:00
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
2015-08-13 02:20:18 +02:00
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2015-08-09 17:28:58 +02:00
|
|
|
import Data.Aeson
|
|
|
|
import Data.ByteString (ByteString)
|
2015-12-25 00:25:15 +01:00
|
|
|
import qualified Data.ByteString as B
|
2015-08-09 17:28:58 +02:00
|
|
|
import Data.Monoid ((<>))
|
|
|
|
import Data.Text (Text)
|
|
|
|
import GHC.Generics
|
|
|
|
import Network.Wai
|
|
|
|
import Network.Wai.Handler.Warp
|
|
|
|
import Servant
|
|
|
|
import Servant.API.Authentication
|
|
|
|
|
|
|
|
-- | Data we will use to test for authentication
|
|
|
|
data CookieAuth = CookieAuth { cookie :: ByteString }
|
|
|
|
|
2015-12-25 00:25:15 +01:00
|
|
|
-- | An ADT to index errors we may encounter when retrieving a cookie from a request
|
|
|
|
data CookieFromRequestError = CookieNotPresent
|
|
|
|
| CookieInvalid
|
|
|
|
|
2015-08-11 16:28:17 +02:00
|
|
|
-- | A 'User' datatype we get once the authentication data is tested.
|
2015-08-09 17:28:58 +02:00
|
|
|
type User = ByteString
|
|
|
|
|
2015-12-25 00:25:15 +01:00
|
|
|
-- | An ADT to index errors we may encounter when retrieving a user from the database
|
|
|
|
-- based on the cookie
|
|
|
|
data UserFromCookieError = NoUserAssociatedWithCookie
|
|
|
|
| MoreThanOneCookieInDatabase Int
|
|
|
|
|
2015-08-11 16:28:17 +02:00
|
|
|
-- | We will look up authentication data in the database and extract a User.
|
2015-12-25 00:25:15 +01:00
|
|
|
type DBLookup = CookieAuth -> IO (Either UserFromCookieError User)
|
2015-08-09 17:28:58 +02:00
|
|
|
|
2015-08-11 16:28:17 +02:00
|
|
|
-- | Method that tests for authentication and extracts a User type.
|
2015-12-25 00:25:15 +01:00
|
|
|
userFromCookie :: DBLookup
|
|
|
|
userFromCookie (CookieAuth cookie) = if cookie == "good cookie" then return (Right "one user") else return (Left NoUserAssociatedWithCookie)
|
|
|
|
|
|
|
|
-- | a handler that takes a cookie error and returns a ServantErr in IO
|
|
|
|
missingCookieHandler :: OnMissing IO ServantErr 'Strict CookieFromRequestError
|
|
|
|
missingCookieHandler =
|
|
|
|
let handler error = case error of
|
|
|
|
CookieNotPresent -> return $ err401 { errReasonPhrase = "No cookie" }
|
|
|
|
CookieInvalid -> return (err401 { errReasonPhrase = "Invalid Cookie" })
|
|
|
|
in StrictMissing handler
|
|
|
|
|
|
|
|
-- | a handler that takes a Cookie + extraction error and returns a IO ServantErr.
|
|
|
|
-- This handler is called when a valid authentication data (e.g. Cookie) was extracted
|
|
|
|
-- from the request, but we were not able to find a valid user in our database.
|
|
|
|
unauthenticatedHandler :: OnUnauthenticated IO ServantErr 'Strict UserFromCookieError CookieAuth
|
|
|
|
unauthenticatedHandler =
|
|
|
|
let handler error _ = case error of
|
|
|
|
NoUserAssociatedWithCookie ->
|
|
|
|
return (err403 { errReasonPhrase = "you don't exist" })
|
|
|
|
(MoreThanOneCookieInDatabase i) ->
|
|
|
|
return (err403 { errReasonPhrase = "we found " <> show i <> " of you. WAT?" })
|
|
|
|
in StrictUnauthenticated handler
|
2015-04-24 14:00:57 +02:00
|
|
|
|
2015-08-09 17:28:58 +02:00
|
|
|
-- | 'AuthData' is a typeclass that provides a method to extract authentication
|
2015-12-25 00:25:15 +01:00
|
|
|
-- data from a 'Request'.
|
|
|
|
instance AuthData CookieAuth CookieFromRequestError where
|
|
|
|
authData req = case lookup "Cookie" (requestHeaders req) of
|
|
|
|
Nothing -> Left CookieNotPresent
|
|
|
|
(Just cookieVal) -> if B.length cookieVal > 10
|
|
|
|
then Left CookieInvalid
|
|
|
|
else Right (CookieAuth cookieVal)
|
2015-04-24 14:00:57 +02:00
|
|
|
|
2015-08-11 16:28:17 +02:00
|
|
|
-- | Some data we will return from our API that is protected
|
2015-04-24 14:00:57 +02:00
|
|
|
newtype PrivateData = PrivateData { ssshhh :: Text }
|
|
|
|
deriving (Eq, Show, Generic)
|
|
|
|
|
|
|
|
instance ToJSON PrivateData
|
|
|
|
|
2015-08-09 17:28:58 +02:00
|
|
|
-- | Some data we will return from our API that is not protected
|
2015-04-24 14:00:57 +02:00
|
|
|
newtype PublicData = PublicData { somedata :: Text }
|
|
|
|
deriving (Eq, Show, Generic)
|
|
|
|
|
|
|
|
instance ToJSON PublicData
|
|
|
|
|
2015-08-09 17:28:58 +02:00
|
|
|
-- | Private API (will require authentication) as a type
|
|
|
|
type PrivateAPI = Get '[JSON] [PrivateData]
|
|
|
|
|
|
|
|
-- | Public API (non-authenticated) as a type
|
|
|
|
type PublicAPI = Get '[JSON] [PublicData]
|
|
|
|
|
2015-12-25 00:25:15 +01:00
|
|
|
-- | A type alias for our auth protection
|
|
|
|
type CookieAuthProtected = AuthProtect CookieAuth User 'Strict CookieFromRequestError 'Strict UserFromCookieError
|
|
|
|
|
2015-08-09 17:28:58 +02:00
|
|
|
-- | Our full API as a type with authentication
|
2015-12-25 00:25:15 +01:00
|
|
|
type API = CookieAuthProtected :> "private" :> PrivateAPI
|
2015-08-09 17:28:58 +02:00
|
|
|
:<|> PublicAPI
|
|
|
|
|
2015-04-24 14:00:57 +02:00
|
|
|
api :: Proxy API
|
|
|
|
api = Proxy
|
|
|
|
|
|
|
|
server :: Server API
|
2015-12-25 00:25:15 +01:00
|
|
|
server = authProtect missingCookieHandler
|
|
|
|
unauthenticatedHandler
|
|
|
|
userFromCookie
|
|
|
|
(\_ -> return prvdata)
|
2015-08-09 17:28:58 +02:00
|
|
|
:<|> return pubdata
|
2015-04-24 14:00:57 +02:00
|
|
|
|
|
|
|
where prvdata = [PrivateData "this is a secret"]
|
|
|
|
pubdata = [PublicData "this is a public piece of data"]
|
|
|
|
|
|
|
|
main :: IO ()
|
|
|
|
main = run 8080 (serve api server)
|
|
|
|
|
|
|
|
{- Sample session:
|
|
|
|
$ curl http://localhost:8080/
|
|
|
|
[{"somedata":"this is a public piece of data"}]
|
|
|
|
$ curl http://localhost:8080/private
|
|
|
|
Missing auth header.
|
|
|
|
$ curl -H "Cookie: good password" http://localhost:8080/private
|
|
|
|
[{"ssshhh":"this is a secret"}]
|
|
|
|
$ curl -H "Cookie: bad password" http://localhost:8080/private
|
|
|
|
Invalid cookie.
|
2015-05-03 01:53:38 +02:00
|
|
|
-}
|