{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-| Module : Network.Pusher.Internal.Auth Description : Functions to perform authentication (generate auth signatures) Copyright : (c) Will Sewell, 2016 Licence : MIT Maintainer : me@willsewell.com Stability : experimental This module contains helper functions for authenticating HTTP requests, as well as publically facing functions for authentication private and presence channel users; these functions are re-exported in the main Pusher module. -} module Network.Pusher.Internal.Auth ( AuthString , AuthSignature , authenticatePresence , authenticatePresenceWithEncoder , authenticatePrivate , makeQS ) where import qualified Data.Aeson as A import Data.Char (toLower) import Data.Monoid ((<>)) import Data.Text.Encoding (encodeUtf8) import GHC.Exts (sortWith) #if MIN_VERSION_aeson(1,0,0) import qualified Data.Aeson.Text as A #else import qualified Data.Aeson.Encode as A #endif import qualified Crypto.Hash as Hash import qualified Crypto.MAC.HMAC as HMAC import qualified Data.ByteArray as BA import qualified Data.ByteString as B import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Char8 as BC import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TL import Network.Pusher.Data (AppKey, AppSecret, Channel, Credentials(..), SocketID, renderChannel) import Network.Pusher.Internal.HTTP (RequestQueryString) import Network.Pusher.Internal.Util (show') -- |Generate the required query string parameters required to send API requests -- to Pusher. makeQS :: AppKey -> AppSecret -> T.Text -> T.Text -> RequestQueryString -- ^Any additional parameters. -> B.ByteString -> Int -- ^Current UNIX timestamp. -> RequestQueryString makeQS appKey appSecret method fullPath params body ts -- Generate all required parameters and add them to the list of existing ones -- Parameters are: -- - In alphabetical order -- - Keys are lower case = let allParams = alphabeticalOrder . lowercaseKeys . (params ++) $ [ ("auth_key", appKey) , ("auth_timestamp", show' ts) , ("auth_version", "1.0") , ( "body_md5" , B16.encode $ BA.convert (Hash.hash body :: Hash.Digest Hash.MD5)) ] -- Generate the auth signature from the list of parameters -- - Method name is upper case authSig = authSignature appSecret $ B.intercalate "\n" [ encodeUtf8 . T.toUpper $ method , encodeUtf8 fullPath , formQueryString allParams ] -- Add the auth string to the list in ("auth_signature", authSig) : allParams where alphabeticalOrder = sortWith fst lowercaseKeys = map (\(k, v) -> (BC.map toLower k, v)) -- |Render key-value tuple mapping of query string parameters into a string. formQueryString :: RequestQueryString -> B.ByteString formQueryString = B.intercalate "&" . map (\(a, b) -> a <> "=" <> b) -- |The bytestring to sign with the app secret to create a signature from. type AuthString = B.ByteString -- |A Pusher auth signature. type AuthSignature = B.ByteString -- |Create a Pusher auth signature of a string using the provided credentials. authSignature :: AppSecret -> AuthString -> AuthSignature authSignature appSecret authString = B16.encode $ BA.convert (HMAC.hmac appSecret authString :: HMAC.HMAC Hash.SHA256) -- |Generate an auth signature of the form "app_key:auth_sig" for a user of a -- private channel. authenticatePrivate :: Credentials -> SocketID -> Channel -> AuthSignature authenticatePrivate cred socketID channel = let sig = authSignature (credentialsAppSecret cred) (encodeUtf8 $ socketID <> ":" <> renderChannel channel) in credentialsAppKey cred <> ":" <> sig -- |Generate an auth signature of the form "app_key:auth_sig" for a user of a -- presence channel. authenticatePresence :: A.ToJSON a => Credentials -> SocketID -> Channel -> a -> AuthSignature authenticatePresence = authenticatePresenceWithEncoder (TL.toStrict . TL.toLazyText . A.encodeToTextBuilder . A.toJSON) -- |As above, but allows the encoder of the user data to be specified. This is -- useful for testing because the encoder can be mocked; aeson's encoder enodes -- JSON object fields in arbitrary orders, which makes it impossible to test. authenticatePresenceWithEncoder :: (a -> T.Text) -- ^The encoder of the user data. -> Credentials -> SocketID -> Channel -> a -> AuthSignature authenticatePresenceWithEncoder userEncoder cred socketID channel userData = let authString = encodeUtf8 $ socketID <> ":" <> renderChannel channel <> ":" <> userEncoder userData sig = authSignature (credentialsAppSecret cred) authString in credentialsAppKey cred <> ":" <> sig