-- | -- Module: Network.Payments.PayPal.Auth -- Copyright: (C) 2016 Braden Walters -- License: MIT (see LICENSE file) -- Maintainer: Braden Walters -- Stability: experimental -- Portability: ghc {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Network.Payments.PayPal.Auth ( ClientID , Secret , Seconds , AccessToken(..) , AccessTokenWithExpiration , AccessTokenError(..) , AccessTokenResult , fetchAccessToken , fetchAccessTokenWithExpiration , safeExpirationTime ) where #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif import Control.Exception import Control.Lens import Control.Monad import Data.Aeson import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as LBS import qualified Data.Text as T import Data.Time.Clock import qualified Network.HTTP.Client as HTTP import Network.Wreq import qualified Network.Wreq.Types as WTypes import Network.Payments.PayPal.Environment -- |PayPal client ID with which to execute actions. type ClientID = String -- |PayPal secret of user with which to execute actions. type Secret = String -- |Number representing seconds of time. type Seconds = Integer -- |Access token returned from OAuth. data AccessToken = AccessToken { aTokenScope :: [String] , aToken :: String , aTokenType :: String , aTokenAppId :: String , aTokenExpires :: Seconds } deriving (Eq, Show) -- |An access token from OAuth together with its UTC expiration time. type AccessTokenWithExpiration = (AccessToken, UTCTime) -- |Error while fetching access token. data AccessTokenError = AccessTokenHttpError HTTP.HttpException | AccessTokenStatusError Int | -- Contains error message and JSON text. AccessTokenParseError String LBS.ByteString -- |Either an access token or the error encountered while fetching it. type AccessTokenResult = Either AccessTokenError AccessToken -- |Either an access token with an expiration or the error encountered while -- fetching it type AccessTokenWithExpirationResult = Either AccessTokenError AccessTokenWithExpiration instance FromJSON AccessToken where parseJSON (Object obj) = AccessToken <$> (map T.unpack <$> T.split (== ' ') <$> (obj .: "scope")) <*> obj .: "access_token" <*> obj .: "token_type" <*> obj .: "app_id" <*> obj .: "expires_in" parseJSON _ = mzero -- |Use a PayPal environment and login credentials to get an OAuth access token. fetchAccessToken :: EnvironmentUrl -> ClientID -> Secret -> IO AccessTokenResult fetchAccessToken (EnvironmentUrl url) username password = do let usernameBS = BS8.pack username passwordBS = BS8.pack password fullUrl = url ++ "/v1/oauth2/token" options' = defaults & header "Accept" .~ ["application/json"] & auth ?~ basicAuth usernameBS passwordBS contentType = "application/x-www-form-urlencoded" content = "grant_type=client_credentials" payload = WTypes.Raw contentType $ HTTP.RequestBodyBS content responseOrErr <- (try $ postWith options' fullUrl payload) :: IO (Either HTTP.HttpException (Response LBS.ByteString)) case responseOrErr of Left err -> return $ Left $ AccessTokenHttpError err Right response -> let statusCode' = response ^. responseStatus . statusCode in if statusCode' == 200 then let responseText = response ^. responseBody in return $ case eitherDecode responseText of Left errMsg -> Left $ AccessTokenParseError errMsg responseText Right result -> Right result else return $ Left $ AccessTokenStatusError statusCode' -- |Use a PayPal environment and login credentials to get an OAuth access token -- with an expiration time. fetchAccessTokenWithExpiration :: EnvironmentUrl -> ClientID -> Secret -> IO AccessTokenWithExpirationResult fetchAccessTokenWithExpiration environment username password= do currentTime <- getCurrentTime accessTokenOrErr <- fetchAccessToken environment username password let getExpire accToken = (accToken, safeExpirationTime currentTime accToken) return $ either Left (Right . getExpire) accessTokenOrErr -- |Time at which the token should be considered expired. This is a few seconds -- before the time that PayPal gives us. Parameters are the time at which the -- access token was retrieved and the access token. safeExpirationTime :: UTCTime -> AccessToken -> UTCTime safeExpirationTime currentTime token = let safetyBuffer = 10 -- Seconds. seconds = aTokenExpires token - safetyBuffer in addUTCTime (fromIntegral seconds) currentTime