{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} -- | -- Module : Network.OAuth.Signing -- Copyright : (c) Joseph Abrahamson 2013 -- License : MIT -- -- Maintainer : me@jspha.com -- Stability : experimental -- Portability : non-portable -- -- Signing forms the core process for OAuth. Given a 'C.Request' about to be -- sent, 'Server' parameters, and a full 'Oa' we append a set of parameters to -- the 'C.Request' which turns it into a signed OAuth request. module Network.OAuth.Signing ( -- * Primary interface -- | The 'oauth' and 'sign' commands can be used as low level signing -- primitives, and they are indeed used to build the "Network.OAuth.Stateful" -- interface exported by default. oauth, sign, -- * Low-level interface -- | The low-level interface is used to build 'oauth' and 'sign' and can be -- useful for testing. makeSignature, augmentRequest, canonicalBaseString, canonicalParams, oauthParams, canonicalUri, bodyParams, queryParams ) where import qualified Blaze.ByteString.Builder as Blz import Control.Applicative import Crypto.Hash.SHA1 (hash) import Crypto.MAC.HMAC (hmac) import Crypto.Random import qualified Data.ByteString as S import qualified Data.ByteString.Base64 as S64 import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as SL import Data.Char (toUpper) import Data.List (sort) import Data.Maybe (fromMaybe, mapMaybe) import Data.Monoid import qualified Network.HTTP.Client as C import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types.QueryLike as H import Network.OAuth.MuLens import Network.OAuth.Types.Credentials import Network.OAuth.Types.Params import Network.OAuth.Util import Network.URI -- | Sign a request with a fresh set of parameters. oauth :: CPRG gen => Cred ty -> Server -> C.Request -> gen -> IO (C.Request, gen) oauth creds sv req gen = do (oax, gen') <- freshOa creds gen return (sign oax sv req, gen') -- | Sign a request given generated parameters sign :: Oa ty -> Server -> C.Request -> C.Request sign oax server req = let payload = canonicalBaseString oax server req sigKey = signingKey (credentials oax) sig = makeSignature (signatureMethod server) sigKey payload params = ("oauth_signature", H.toQueryValue sig) : oauthParams oax server in augmentRequest (parameterMethod server) params req makeSignature :: SignatureMethod -> S.ByteString -> S.ByteString -> S.ByteString makeSignature HmacSha1 sigKey payload = S64.encode (hmac hash 64 sigKey payload) makeSignature Plaintext sigKey _ = sigKey -- | Augments whatever component of the 'C.Request' is specified by -- 'ParameterMethod' with one built from the apropriate OAuth parameters -- (passed as a 'H.Query'). -- -- Currently this actually /replaces/ the @Authorization@ header if one -- exists. This may be a bad idea if the @realm@ parameter is pre-set, -- perhaps. -- -- TODO: Parse @Authorization@ header and augment it. -- -- Currently this actually /replaces/ the entity body if one -- exists. This is definitely just me being lazy. -- -- TODO: Try to parse entity body and augment it. augmentRequest :: ParameterMethod -> H.Query -> C.Request -> C.Request augmentRequest AuthorizationHeader q req = let replaceHeader :: H.HeaderName -> S.ByteString -> H.RequestHeaders -> H.RequestHeaders replaceHeader n b [] = [(n, b)] replaceHeader n b (x@(hn, _):rest) | n == hn = (n, b):rest | otherwise = x : replaceHeader n b rest authHeader = "OAuth " <> S8.intercalate ", " pairs pairs = map mkPair q -- We should perhaps pctEncode the key in each pair as well, but so -- long as this is a well-formed OAuth header the keys will never -- require encoding. mkPair (k, v) = k <> "=\"" <> pctEncode (fromMaybe "" v) <> "\"" in req { C.requestHeaders = replaceHeader H.hAuthorization authHeader (C.requestHeaders req) } augmentRequest QueryString q req = let q0 = H.parseQuery (C.queryString req) in req { C.queryString = H.renderQuery True (q ++ q0) } augmentRequest RequestEntityBody q req = let fixQ = mapMaybe (\(a, mayB) -> (a,) <$> mayB) q in C.urlEncodedBody fixQ req canonicalBaseString :: Oa ty -> Server -> C.Request -> S.ByteString canonicalBaseString oax server req = S8.intercalate "&" [ S8.map toUpper (C.method req) , canonicalUri req , canonicalParams oax server req ] canonicalParams :: Oa ty -> Server -> C.Request -> S.ByteString canonicalParams oax server req = let build :: H.QueryItem -> S.ByteString build (k, mayV) = pctEncode k <> maybe S.empty (\v -> "=" <> pctEncode v) mayV combine :: [S.ByteString] -> S.ByteString combine = pctEncode . S8.intercalate "&" reqIsFormUrlEncoded = case lookup H.hContentType (C.requestHeaders req) of Just "application/x-www-form-urlencoded" -> True _ -> False in combine . sort . map build . mconcat $ [ oauthParams oax server , if reqIsFormUrlEncoded then bodyParams req else [] , queryParams req ] oauthParams :: Oa ty -> Server -> H.Query oauthParams (Oa {..}) (Server {..}) = let OaPin {..} = pin infix 8 -: s -: v = (s, H.toQueryValue v) workflowParams Standard = [] workflowParams (TemporaryTokenRequest callback) = [ "oauth_callback" -: callback ] workflowParams (PermanentTokenRequest verifier) = [ "oauth_verifier" -: verifier ] in [ "oauth_version" -: oAuthVersion , "oauth_consumer_key" -: (credentials ^. clientToken . key) , "oauth_signature_method" -: signatureMethod , "oauth_token" -: (getResourceTokenDef credentials ^. key) , "oauth_timestamp" -: timestamp , "oauth_nonce" -: nonce ] ++ workflowParams workflow canonicalUri :: C.Request -> S.ByteString canonicalUri req = pctEncode $ S8.pack $ uriScheme <> "//" <> fauthority uriAuthority <> uriPath where URI {..} = C.getUri req fauthority Nothing = "" fauthority (Just (URIAuth {..})) = let -- Canonical URIs do not display their port unless it is non-standard fport | (uriPort == ":443") && (uriScheme == "https:") = "" | (uriPort == ":80" ) && (uriScheme == "http:" ) = "" | otherwise = uriPort in uriRegName <> fport -- | Queries a 'C.Request' body and tries to interpret it as a set of OAuth -- valid parameters. It makes the assumption that if the body type is a -- streaming variety then it is /not/ a set of OAuth parameters---dropping this -- assumption would prevent this from being pure. bodyParams :: C.Request -> H.Query bodyParams = digestBody . C.requestBody where digestBody :: C.RequestBody -> H.Query digestBody (C.RequestBodyLBS lbs) = H.parseQuery (SL.toStrict lbs) digestBody (C.RequestBodyBS bs) = H.parseQuery bs digestBody (C.RequestBodyBuilder _ b) = H.parseQuery (Blz.toByteString b) digestBody (C.RequestBodyStream _ _) = [] digestBody (C.RequestBodyStreamChunked _) = [] -- digestBody (Left (_, builder)) = H.parseQuery (Blz.toByteString builder) -- digestBody (Right _) = [] queryParams :: C.Request -> H.Query queryParams = H.parseQuery . C.queryString