webgear-server-0.2.0: src/WebGear/Middlewares/Auth/Basic.hs
-- |
-- Copyright : (c) Raghu Kaippully, 2020
-- License : MPL-2.0
-- Maintainer : rkaippully@gmail.com
--
-- Basic authentication support.
--
module WebGear.Middlewares.Auth.Basic
( BasicAuth
, Realm (..)
, Username (..)
, Password (..)
, Credentials (..)
, BasicAuthError (..)
, basicAuth
) where
import Control.Arrow (Kleisli (..))
import Control.Monad (when, (>=>))
import Control.Monad.Except (throwError)
import Data.ByteString (ByteString, intercalate)
import Data.ByteString.Base64 (decodeLenient)
import Data.ByteString.Char8 (split)
import Data.CaseInsensitive (CI, mk)
import Data.Proxy (Proxy (..))
import Data.String (IsString)
import WebGear.Trait (Has (..), Linked, Result (..), Trait (..), probe)
import WebGear.Types (MonadRouter (..), Request, RequestMiddleware', Response (..), forbidden403,
requestHeader, setResponseHeader, unauthorized401)
import WebGear.Util (maybeToRight)
-- | Trait for HTTP basic authentication: https://tools.ietf.org/html/rfc7617
data BasicAuth
-- | The protection space for basic authentication
newtype Realm = Realm ByteString
deriving newtype (Eq, Ord, Show, Read, IsString)
-- | Username for basic authentication. Valid usernames cannot contain
-- \':\' characters.
newtype Username = Username ByteString
deriving newtype (Eq, Ord, Show, Read, IsString)
-- | Password for basic authentication.
newtype Password = Password ByteString
deriving newtype (Eq, Ord, Show, Read, IsString)
-- | Basic authentication credentials retrieved from an HTTP request
data Credentials = Credentials
{ credentialsUsername :: !Username
, credentialsPassword :: !Password
}
deriving (Eq, Ord, Show, Read)
-- | Error extracting credentials from an HTTP request
data BasicAuthError = AuthHeaderError -- ^ Authorization header is missing or badly formatted
| AuthSchemeMismatch -- ^ Authorization scheme is not "Basic"
deriving (Eq, Ord, Show, Read)
instance Monad m => Trait BasicAuth Request m where
type Attribute BasicAuth Request = Credentials
type Absence BasicAuth Request = BasicAuthError
toAttribute :: Request -> m (Result BasicAuth Request)
toAttribute r = pure $ either NotFound Found $ do
h <- getAuthHeader r
(scheme, creds) <- parseAuthHeader h
when (scheme /= "Basic") $
throwError AuthSchemeMismatch
parseCreds creds
type Scheme = CI ByteString
type EncodedPassword = ByteString
getAuthHeader :: Request -> Either BasicAuthError ByteString
getAuthHeader r = maybeToRight AuthHeaderError $ requestHeader "Authorization" r
parseAuthHeader :: ByteString -> Either BasicAuthError (Scheme, EncodedPassword)
parseAuthHeader s =
case split ' ' s of
[x, y] -> pure (mk x, y)
_ -> throwError AuthHeaderError
parseCreds :: EncodedPassword -> Either BasicAuthError Credentials
parseCreds enc =
case split ':' (decodeLenient enc) of
[] -> throwError AuthHeaderError
u:ps -> pure $ Credentials (Username u) (Password $ intercalate ":" ps)
-- | Middleware to add basic authentication protection for a handler.
--
-- Example usage:
--
-- > basicAuth "realm" isValidCredentials handler
--
-- This middleware returns a 401 response if no credentials are found
-- in the request. It returns a 403 response if credentials are
-- present but isValidCredentials returns False.
--
basicAuth :: forall m req a. MonadRouter m
=> Realm
-> (Credentials -> m Bool)
-> RequestMiddleware' m req (BasicAuth : req) a
basicAuth (Realm realm) credCheck handler = Kleisli $
probe @BasicAuth >=> either unauthorized (validateCredentials >=> runKleisli handler)
where
unauthorized :: BasicAuthError -> m (Response a)
unauthorized = const $ errorResponse
$ setResponseHeader "WWW-Authenticate" ("Basic realm=\"" <> realm <> "\"")
$ unauthorized401 "Unauthorized"
validateCredentials :: Linked (BasicAuth : req) Request
-> m (Linked (BasicAuth : req) Request)
validateCredentials req = do
valid <- credCheck $ get (Proxy @BasicAuth) req
if valid
then pure req
else errorResponse $ forbidden403 "Forbidden"