module Web.Authenticate.OAuth
(
OAuth, def, newOAuth, oauthServerName, oauthRequestUri, oauthAccessTokenUri,
oauthAuthorizeUri, oauthSignatureMethod, oauthConsumerKey,
oauthConsumerSecret, oauthCallback, oauthRealm, oauthVersion,
OAuthVersion(..), SignMethod(..), Credential(..), OAuthException(..),
AccessTokenRequest,
defaultAccessTokenRequest,
accessTokenAddAuth,
accessTokenRequestHook,
accessTokenOAuth,
accessTokenTemporaryCredential,
accessTokenManager,
newCredential, emptyCredential, insert, delete, inserts, injectVerifier,
signOAuth, genSign, checkOAuth,
getTemporaryCredential, getTemporaryCredentialWithScope,
getTemporaryCredentialProxy, getTemporaryCredential',
authorizeUrl, authorizeUrl',
addAuthBody,
getAccessToken,
getAccessTokenProxy,
getTokenCredential,
getTokenCredentialProxy,
getAccessToken',
getAccessTokenWith,
paramEncode, addScope, addMaybeProxy
) where
import Blaze.ByteString.Builder (toByteString)
import Control.Exception
import Control.Arrow (second)
import Control.Monad
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Except
import Crypto.Types.PubKey.RSA (PrivateKey (..))
import Data.ByteString.Base64
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BSL
import Data.Char
import Data.Default
import Data.Digest.Pure.SHA
import qualified Data.IORef as I
import Data.List as List (sort, find)
import Data.Maybe
import Data.Time
import Network.HTTP.Client
import Network.HTTP.Types (SimpleQuery, parseSimpleQuery)
import Network.HTTP.Types (Header)
import Network.HTTP.Types (renderSimpleQuery, status200)
import Numeric
import System.Random
#if MIN_VERSION_base(4,7,0)
import Data.Data hiding (Proxy (..))
#else
import Data.Data
#endif
import Codec.Crypto.RSA (rsassa_pkcs1_v1_5_sign, hashSHA1)
data OAuth = OAuth { oauthServerName :: String
, oauthRequestUri :: String
, oauthAccessTokenUri :: String
, oauthAuthorizeUri :: String
, oauthSignatureMethod :: SignMethod
, oauthConsumerKey :: BS.ByteString
, oauthConsumerSecret :: BS.ByteString
, oauthCallback :: Maybe BS.ByteString
, oauthRealm :: Maybe BS.ByteString
, oauthVersion :: OAuthVersion
} deriving (Show, Eq, Read, Data, Typeable)
data OAuthVersion = OAuth10
| OAuth10a
deriving (Show, Eq, Enum, Ord, Data, Typeable, Read)
newOAuth :: OAuth
newOAuth = OAuth { oauthSignatureMethod = HMACSHA1
, oauthCallback = Nothing
, oauthRealm = Nothing
, oauthServerName = ""
, oauthRequestUri = ""
, oauthAccessTokenUri = ""
, oauthAuthorizeUri = ""
, oauthConsumerKey = error "You MUST specify oauthConsumerKey parameter."
, oauthConsumerSecret = error "You MUST specify oauthConsumerSecret parameter."
, oauthVersion = OAuth10a
}
instance Default OAuth where
def = newOAuth
data SignMethod = PLAINTEXT
| HMACSHA1
| RSASHA1 PrivateKey
deriving (Show, Eq, Read, Data, Typeable)
newtype OAuthException = OAuthException String
deriving (Show, Eq, Data, Typeable)
instance Exception OAuthException
data AccessTokenRequest = AccessTokenRequest {
accessTokenAddAuth :: (BS.ByteString -> Credential -> Request -> Request)
, accessTokenRequestHook :: (Request -> Request)
, accessTokenOAuth :: OAuth
, accessTokenTemporaryCredential :: Credential
, accessTokenManager :: Manager
}
defaultAccessTokenRequest :: OAuth -> Credential -> Manager -> AccessTokenRequest
defaultAccessTokenRequest oauth cred man = AccessTokenRequest
{ accessTokenAddAuth = addAuthHeader
, accessTokenRequestHook = id
, accessTokenOAuth = oauth
, accessTokenTemporaryCredential = cred
, accessTokenManager = man
}
newtype Credential = Credential
{ unCredential :: [(BS.ByteString, BS.ByteString)] }
deriving (Show, Eq, Ord, Read, Data, Typeable)
newCredential :: BS.ByteString
-> BS.ByteString
-> Credential
newCredential tok sec = Credential [("oauth_token", tok), ("oauth_token_secret", sec)]
emptyCredential :: Credential
emptyCredential = Credential []
insert :: BS.ByteString
-> BS.ByteString
-> Credential
-> Credential
insert k v = Credential . insertMap k v . unCredential
inserts :: [(BS.ByteString, BS.ByteString)] -> Credential -> Credential
inserts = flip $ foldr (uncurry insert)
delete :: BS.ByteString
-> Credential
-> Credential
delete key = Credential . deleteMap key . unCredential
injectVerifier :: BS.ByteString -> Credential -> Credential
injectVerifier = insert "oauth_verifier"
signOAuth :: MonadIO m
=> OAuth
-> Credential
-> Request
-> m Request
signOAuth oa crd req = signOAuth' oa crd True addAuthHeader req
signOAuth' :: MonadIO m
=> OAuth
-> Credential
-> Bool
-> (BS.ByteString -> Credential -> Request -> Request)
-> Request
-> m Request
signOAuth' oa crd withHash add_auth req = do
crd' <- addTimeStamp =<< addNonce crd
mhash <- moauth_body_hash
let tok = addHashToCred mhash $ injectOAuthToCred oa crd'
sign <- genSign oa tok req
let prefix = case oauthRealm oa of
Nothing -> "OAuth "
Just v -> "OAuth realm=\"" `BS.append` v `BS.append` "\","
return $ add_auth prefix
(insert "oauth_signature" sign tok)
req
where
moauth_body_hash = if not withHash || isBodyFormEncoded (requestHeaders req)
then return Nothing
else (Just
. encode
. BSL.toStrict
. bytestringDigest
. sha1
. BSL.fromStrict) `liftM` loadBodyBS req
addHashToCred (Just h) = insert "oauth_body_hash" h
addHashToCred Nothing = id
genSign :: MonadIO m => OAuth -> Credential -> Request -> m BS.ByteString
genSign oa tok req =
case oauthSignatureMethod oa of
HMACSHA1 -> do
text <- getBaseString tok req
let key = BS.intercalate "&" $ map paramEncode [oauthConsumerSecret oa, tokenSecret tok]
return $ encode $ toStrict $ bytestringDigest $ hmacSha1 (fromStrict key) text
PLAINTEXT ->
return $ BS.intercalate "&" $ map paramEncode [oauthConsumerSecret oa, tokenSecret tok]
RSASHA1 pr ->
liftM (encode . toStrict . rsassa_pkcs1_v1_5_sign hashSHA1 pr) (getBaseString tok req)
checkOAuth :: MonadIO m
=> OAuth -> Credential -> Request
-> ExceptT OAuthException m Request
checkOAuth oa crd req = if isBodyFormEncoded origHeaders then checkOAuthB oa crd req else do
case mosig of
Nothing -> throwE $ OAuthException "oauth_signature parameter not found"
Just osig -> do
mhash <- moauth_body_hash
case (\oh nh -> oh == paramEncode nh) `liftM` moauth_body_hash_orig `ap` mhash of
Just False -> throwE $ OAuthException "Failed test of oauth_body_hash"
_ -> let tok = addHashToCred mhash . injectOAuthToCred oa $ inserts (remParams authParams) crd
in genSign oa tok req
{requestHeaders = catMaybes [mtypeHeader]}
>>= \nsig -> if osig == paramEncode nsig
then return req
else throwE $ OAuthException "Failed test of oauth_signature"
where
origHeaders = requestHeaders req
mauthHeader = List.find ( ("Authorization" ==) . fst) $ origHeaders
mtypeHeader = List.find ( ("Content-Type" ==) . fst) $ origHeaders
authParams = (map parseParam . BS.split ',' . BS.drop 6 . snd) `liftM` mauthHeader
remParams Nothing = []
remParams (Just ms) = filter ( not . flip elem
("realm" : "oauth_signature" : map fst (unCredential crd))
. fst) ms
mosig = fmap snd . join $ List.find (("oauth_signature" ==) . fst) `liftM` authParams
parseParam = second (BS.takeWhile ('"' /=) . BS.drop 1 . BS.dropWhile ('"' /=))
. splitEq . BS.dropWhile (' ' ==)
splitEq s = case BS.elemIndex '=' s of
Nothing -> (s,"")
Just i -> BS.splitAt i s
moauth_body_hash_orig = join $ (fmap snd . List.find ( ("oauth_body_hash" ==) . fst)) `liftM` authParams
moauth_body_hash = if moauth_body_hash_orig == Nothing
then return Nothing
else (Just
. encode
. BSL.toStrict
. bytestringDigest
. sha1
. BSL.fromStrict) `liftM` loadBodyBS req
addHashToCred (Just h) = insert "oauth_body_hash" h
addHashToCred Nothing = id
checkOAuthB :: MonadIO m
=> OAuth -> Credential -> Request
-> ExceptT OAuthException m Request
checkOAuthB oa crd req0 = do
(mosig, reqBody) <- getSig `liftM` loadBodyBS req0
let req = req0 {requestBody = RequestBodyBS reqBody}
case mosig of
"" -> throwE $ OAuthException "oauth_signature parameter not found"
osig -> do
nsig <- genSign oa crd req
if osig == paramEncode nsig
then return req0
else throwE $ OAuthException "Failed test of oauth_signature"
where
getSig b = let (h1 , r ) = BS.breakSubstring "&oauth_signature=" b
(sig, h2) = BS.breakSubstring "&" $ BS.drop 17 r
in (sig, h1 `BS.append` h2)
getTemporaryCredential :: MonadIO m
=> OAuth
-> Manager
-> m Credential
getTemporaryCredential = getTemporaryCredential' id
getTemporaryCredentialWithScope :: MonadIO m
=> BS.ByteString
-> OAuth
-> Manager
-> m Credential
getTemporaryCredentialWithScope bs = getTemporaryCredential' (addScope bs)
getTemporaryCredentialProxy :: MonadIO m
=> Maybe Proxy
-> OAuth
-> Manager
-> m Credential
getTemporaryCredentialProxy p oa m = getTemporaryCredential' (addMaybeProxy p) oa m
getTemporaryCredential' :: MonadIO m
=> (Request -> Request)
-> OAuth
-> Manager
-> m Credential
getTemporaryCredential' hook oa manager = do
let req = fromJust $ parseUrl $ oauthRequestUri oa
crd = maybe id (insert "oauth_callback") (oauthCallback oa) $ emptyCredential
req' <- signOAuth' oa crd False addAuthHeader $ hook (req { method = "POST" })
rsp <- liftIO $ httpLbs req' manager
if responseStatus rsp == status200
then do
let dic = parseSimpleQuery . toStrict . responseBody $ rsp
return $ Credential dic
else liftIO . throwIO . OAuthException
$ "Gaining OAuth Temporary Credential Failed: " ++ BSL.unpack (responseBody rsp)
authorizeUrl :: OAuth
-> Credential
-> String
authorizeUrl = authorizeUrl' $ \oa -> const [("oauth_consumer_key", oauthConsumerKey oa)]
authorizeUrl' :: (OAuth -> Credential -> SimpleQuery)
-> OAuth
-> Credential
-> String
authorizeUrl' f oa cr = oauthAuthorizeUri oa ++ BS.unpack (renderSimpleQuery True queries)
where fixed = ("oauth_token", token cr):f oa cr
queries =
case oauthCallback oa of
Nothing -> fixed
Just callback -> ("oauth_callback", callback):fixed
getAccessToken, getTokenCredential
:: MonadIO m
=> OAuth
-> Credential
-> Manager
-> m Credential
getAccessToken = getAccessToken' id
getAccessTokenProxy, getTokenCredentialProxy
:: MonadIO m
=> Maybe Proxy
-> OAuth
-> Credential
-> Manager
-> m Credential
getAccessTokenProxy p = getAccessToken' $ addMaybeProxy p
getAccessToken' :: MonadIO m
=> (Request -> Request)
-> OAuth
-> Credential
-> Manager
-> m Credential
getAccessToken' hook oauth cr manager = do
maybe_access_token <- getAccessTokenWith AccessTokenRequest
{ accessTokenAddAuth = addAuthHeader
, accessTokenRequestHook = hook
, accessTokenOAuth = oauth
, accessTokenTemporaryCredential = cr
, accessTokenManager = manager
}
case maybe_access_token of
Left error_response -> liftIO . throwIO . OAuthException
$ "Gaining OAuth Token Credential Failed: "
++ BSL.unpack (responseBody error_response)
Right access_token -> return access_token
getAccessTokenWith :: MonadIO m
=> AccessTokenRequest
-> m (Either (Response BSL.ByteString) Credential
)
getAccessTokenWith params = do
let req = hook (fromJust $ parseUrl $ oauthAccessTokenUri oa) { method = "POST" }
rsp <- liftIO $ flip httpLbs manager
=<< signOAuth' oa (if oauthVersion oa == OAuth10
then delete "oauth_verifier" cr
else cr) False add_auth req
if responseStatus rsp == status200
then do
let dic = parseSimpleQuery . toStrict . responseBody $ rsp
return $ Right $ Credential dic
else
return $ Left rsp
where
add_auth = accessTokenAddAuth params
hook = accessTokenRequestHook params
oa = accessTokenOAuth params
cr = accessTokenTemporaryCredential params
manager = accessTokenManager params
getTokenCredential = getAccessToken
getTokenCredentialProxy = getAccessTokenProxy
baseTime :: UTCTime
baseTime = UTCTime day 0
where
day = ModifiedJulianDay 40587
showSigMtd :: SignMethod -> BS.ByteString
showSigMtd PLAINTEXT = "PLAINTEXT"
showSigMtd HMACSHA1 = "HMAC-SHA1"
showSigMtd (RSASHA1 _) = "RSA-SHA1"
addNonce :: MonadIO m => Credential -> m Credential
addNonce cred = do
nonce <- liftIO $ replicateM 10 (randomRIO ('a','z'))
return $ insert "oauth_nonce" (BS.pack nonce) cred
addTimeStamp :: MonadIO m => Credential -> m Credential
addTimeStamp cred = do
stamp <- (floor . (`diffUTCTime` baseTime)) `liftM` liftIO getCurrentTime
return $ insert "oauth_timestamp" (BS.pack $ show (stamp :: Integer)) cred
injectOAuthToCred :: OAuth -> Credential -> Credential
injectOAuthToCred oa cred =
inserts [ ("oauth_signature_method", showSigMtd $ oauthSignatureMethod oa)
, ("oauth_consumer_key", oauthConsumerKey oa)
, ("oauth_version", "1.0")
] cred
addAuthBody :: a -> Credential -> Request -> Request
addAuthBody _ (Credential cred) req = urlEncodedBody (filterCreds cred) req
addAuthHeader :: BS.ByteString -> Credential -> Request -> Request
addAuthHeader prefix (Credential cred) req =
req { requestHeaders = insertMap "Authorization" (renderAuthHeader prefix cred) $ requestHeaders req }
renderAuthHeader :: BS.ByteString -> [(BS.ByteString, BS.ByteString)] -> BS.ByteString
renderAuthHeader prefix = (prefix `BS.append`)
. BS.intercalate ","
. map (\(a,b) -> BS.concat [paramEncode a, "=\"", paramEncode b, "\""])
. filterCreds
filterCreds :: [(BS.ByteString, BS.ByteString)] -> [(BS.ByteString, BS.ByteString)]
filterCreds = filter (not . flip elem ["realm", "oauth_token_secret"] . fst )
getBaseString :: MonadIO m => Credential -> Request -> m BSL.ByteString
getBaseString tok req = do
let bsMtd = BS.map toUpper $ method req
isHttps = secure req
scheme = if isHttps then "https" else "http"
bsPort = if (isHttps && port req /= 443) || (not isHttps && port req /= 80)
then ':' `BS.cons` BS.pack (show $ port req) else ""
bsURI = BS.concat [scheme, "://", host req, bsPort, path req]
bsQuery = parseSimpleQuery $ queryString req
bsBodyQ <- if isBodyFormEncoded $ requestHeaders req
then liftM parseSimpleQuery $ loadBodyBS req
else return []
let bsAuthParams = filterCreds $ unCredential tok
allParams = bsQuery++bsBodyQ++bsAuthParams
bsParams = BS.intercalate "&" $ map (\(a,b)->BS.concat[a,"=",b]) $ sort
$ map (\(a,b) -> (paramEncode a,paramEncode b)) allParams
return . BSL.fromStrict $ BS.intercalate "&" $ map paramEncode [bsMtd, bsURI, bsParams]
paramEncode :: BS.ByteString -> BS.ByteString
paramEncode = BS.concatMap escape
where
escape c | isAscii c && (isAlpha c || isDigit c || c `elem` ("-._~" :: String)) = BS.singleton c
| otherwise = let num = map toUpper $ showHex (ord c) ""
oct = '%' : replicate (2 length num) '0' ++ num
in BS.pack oct
addScope :: BS.ByteString -> Request -> Request
addScope scope req | BS.null scope = req
| otherwise = urlEncodedBody [("scope", scope)] req
token, tokenSecret :: Credential -> BS.ByteString
token = fromMaybe "" . lookup "oauth_token" . unCredential
tokenSecret = fromMaybe "" . lookup "oauth_token_secret" . unCredential
addMaybeProxy :: Maybe Proxy -> Request -> Request
addMaybeProxy p req = req { proxy = p }
insertMap :: Eq a => a -> b -> [(a,b)] -> [(a,b)]
insertMap key val = ((key,val):) . filter ((/=key).fst)
deleteMap :: Eq a => a -> [(a,b)] -> [(a,b)]
deleteMap k = filter ((/=k).fst)
toStrict :: BSL.ByteString -> BS.ByteString
toStrict = BS.concat . BSL.toChunks
fromStrict :: BS.ByteString -> BSL.ByteString
fromStrict = BSL.fromChunks . return
loadBodyBS :: MonadIO m => Request -> m BS.ByteString
loadBodyBS = toBS . requestBody
toBS :: MonadIO m => RequestBody -> m BS.ByteString
toBS (RequestBodyLBS l) = return $ toStrict l
toBS (RequestBodyBS s) = return s
toBS (RequestBodyBuilder _ b) = return $ toByteString b
toBS (RequestBodyStream _ givesPopper) = toBS' givesPopper
toBS (RequestBodyStreamChunked givesPopper) = toBS' givesPopper
#if MIN_VERSION_http_client(0, 4, 28)
toBS (RequestBodyIO op) = liftIO op >>= toBS
#else
#endif
toBS' :: MonadIO m => GivesPopper () -> m BS.ByteString
toBS' gp = liftIO $ do
ref <- I.newIORef BS.empty
gp (go ref)
I.readIORef ref
where
go ref popper =
loop id
where
loop front = do
bs <- popper
if BS.null bs
then I.writeIORef ref $ BS.concat $ front []
else loop (front . (bs:))
isBodyFormEncoded :: [Header] -> Bool
isBodyFormEncoded = maybe False (=="application/x-www-form-urlencoded") . lookup "Content-Type"