{-# 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 mkPair (k, v) = k <> "=\"" <> 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 "&" in combine . sort . map build . mconcat $ [ oauthParams oax server , bodyParams req , 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