packages feed

google-oauth2 (empty) → 0.1.0

raw patch · 5 files changed

+308/−0 lines, 5 filesdep +HTTPdep +aesondep +basesetup-changed

Dependencies added: HTTP, aeson, base, bytestring, google-oauth2, hspec, http-conduit, http-types, load-env

Files

+ LICENSE view
@@ -0,0 +1,19 @@+Copyright (c) 2014 Pat Brisbin <pbrisbin@gmail.com>++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ google-oauth2.cabal view
@@ -0,0 +1,82 @@+Name:                   google-oauth2+Version:                0.1.0+Author:                 Pat Brisbin <pbrisbin@gmail.com>+Maintainer:             Pat Brisbin <pbrisbin@gmail.com>+License:                MIT+License-File:           LICENSE+Synopsis:               Google OAuth2 token negotiation+Description:+  Interacting with the Google OAuth2 authorization API+  .+  1. Prompt the user for a verification code+  2. POST that code to the Google API for a set of tokens (access and refresh)+  3. Use the access token until it expires+  4. Use the refresh token to get a new access token+  5. Repeat from 3+  .+  Example usage:+  .+  > import Data.Monoid+  > import Network.Google.OAuth2+  > import Network.HTTP.Conduit+  > import Network.HTTP.Types (hAuthorization)+  >+  > import qualified Data.ByteString.Char8 as B8+  > import qualified Data.ByteString.Lazy.Char8 as L8+  >+  > main :: IO ()+  > main = do+  >     let client = OAuth2Client clientId clientSecret+  >         scopes = ["https://www.googleapis.com/auth/drive"]+  >+  >     token <- getAccessToken client scopes Nothing+  >+  >     request <- parseUrl "https://www.googleapis.com/drive/v2/files"+  >     response <- withManager $ httpLbs $ authorize token request+  >+  >     L8.putStrLn $ responseBody response+  >+  >  where+  >    authorize token request = request+  >        -- Note: haddock chokes on curly braces for some reason, so I'm using+  >        -- parens here instead.+  >        ( requestHeaders = [(hAuthorization, B8.pack $ "Bearer " <> token)] )+  >+  >    -- Setup in Google Developers Console+  >    clientId = "..."+  >    clientSecret = "..."+  .++Cabal-Version:          >= 1.10+Build-Type:             Simple++Library+  Default-Language:     Haskell2010+  HS-Source-Dirs:       src+  GHC-Options:          -Wall+  Exposed-Modules:      Network.Google.OAuth2+  Build-Depends:        base >= 4 && < 5+                      , HTTP            >= 4000.2       && < 4000.3+                      , aeson           >= 0.8          && < 0.9+                      , bytestring+                      , http-conduit+  Default-Extensions:   OverloadedStrings+                        RecordWildCards++Test-Suite spec+  Type:                 exitcode-stdio-1.0+  Default-Language:     Haskell2010+  Hs-Source-Dirs:       test+  Ghc-Options:          -Wall+  Main-Is:              Spec.hs+  Build-Depends:        base+                      , google-oauth2+                      , hspec+                      , bytestring+                      , http-conduit+                      , http-types+                      , load-env++Source-Repository head+  Type:                 git+  Location:             https://github.com/pbrisbin/google-oauth2
+ src/Network/Google/OAuth2.hs view
@@ -0,0 +1,204 @@+module Network.Google.OAuth2+    (+    -- * Types+      OAuth2Client(..)+    , OAuth2Code+    , OAuth2Scope+    , OAuth2Token+    , OAuth2Tokens(..)++    -- * Getting an access token+    , getAccessToken++    -- * Lower-level steps+    , newTokens+    , refreshTokens+    , promptForCode+    , exchangeCode+    ) where++import Control.Arrow (second)+import Control.Applicative ((<$>),(<*>))+import Control.Monad (mzero, void)+import Data.Aeson+import Data.ByteString (ByteString)+import Data.List (intercalate)+import Data.Maybe (fromJust)+import Data.Monoid ((<>))+import Network.HTTP.Conduit+    ( Response(..)+    , httpLbs+    , parseUrl+    , withManager+    , urlEncodedBody+    )+import Network.HTTP.Base (urlEncode)+import System.IO (hFlush, stdout)++import qualified Control.Exception as E+import qualified Data.ByteString.Char8 as C8+import qualified Data.ByteString.Lazy as BL++type OAuth2Code = String+type OAuth2Scope = String+type OAuth2Token = String++-- | OAuth2 client definition+--+-- https://developers.google.com/console/help/new/#generatingoauth2+--+data OAuth2Client = OAuth2Client+    { clientId :: !String+    , clientSecret :: !String+    }+    deriving (Read, Show)++data OAuth2Tokens = OAuth2Tokens+    { accessToken :: !OAuth2Token+    , refreshToken :: !OAuth2Token+    , expiresIn :: !Int+    , tokenType :: !String+    }+    deriving (Read, Show)++instance FromJSON OAuth2Tokens where+    parseJSON (Object o) = OAuth2Tokens+        <$> o .: "access_token"+        <*> o .: "refresh_token"+        <*> o .: "expires_in"+        <*> o .: "token_type"++    parseJSON _ = mzero++-- Used only when refreshing a token, where the response lacks the originally+-- supplied refresh_token+data RefreshResponse = RefreshResponse+    { rAccessToken :: OAuth2Token+    , rExpiresIn :: Int+    , rTokenType :: String+    }++instance FromJSON RefreshResponse where+    parseJSON (Object o) = RefreshResponse+        <$> o .: "access_token"+        <*> o .: "expires_in"+        <*> o .: "token_type"++    parseJSON _ = mzero++toOAuth2Tokens :: OAuth2Token -> RefreshResponse -> OAuth2Tokens+toOAuth2Tokens token RefreshResponse{..} =+    OAuth2Tokens+        { accessToken = rAccessToken+        , refreshToken = token+        , expiresIn = rExpiresIn+        , tokenType = rTokenType+        }++-- | Get a valid access token with the given scopes+--+-- If given, credentials are cached in a file, thus preventing the need for any+-- prompting on subsequent reuse. N.B. this function always refreshes the access+-- token before returning it.+--+getAccessToken :: OAuth2Client+               -> [OAuth2Scope]+               -> Maybe FilePath -- ^ File in which to cache the token+               -> IO OAuth2Token -- ^ Refreshed token+getAccessToken client scopes (Just tokenFile) = do+    cached <- cachedTokens tokenFile+    tokens <- case cached of+        Just t -> return t+        Nothing -> newTokens client scopes++    refreshed <- refreshTokens client tokens+    void $ cacheTokens tokenFile refreshed++    return $ accessToken refreshed++getAccessToken client scopes Nothing = do+    tokens <- newTokens client scopes+    refreshed <- refreshTokens client tokens++    return $ accessToken refreshed++-- | Prompt the user for a verification code and exchange it for tokens+newTokens :: OAuth2Client -> [OAuth2Scope] -> IO OAuth2Tokens+newTokens client scopes = do+    code <- promptForCode client scopes+    tokens <- exchangeCode client code++    return tokens++-- | Prompt the user for a verification code+promptForCode :: OAuth2Client -> [OAuth2Scope] -> IO OAuth2Code+promptForCode client scopes = do+    putStrLn ""+    putStrLn "Visit the following URL to retrieve a verification code:"+    putStrLn ""+    putStrLn $ permissionUrl client scopes+    putStrLn ""+    putStr   "Verification code: "+    hFlush stdout++    getLine++-- | Exchange a verification code for tokens+exchangeCode :: OAuth2Client -> OAuth2Code -> IO OAuth2Tokens+exchangeCode client code = postTokens+    [ ("client_id", clientId client)+    , ("client_secret", clientSecret client)+    , ("grant_type", "authorization_code")+    , ("redirect_uri", redirectUri)+    , ("code", code)+    ]++-- | Use the refresh token to get a new access token+refreshTokens :: OAuth2Client -> OAuth2Tokens -> IO OAuth2Tokens+refreshTokens client tokens = do+    refreshed <- postTokens+        [ ("client_id", clientId client)+        , ("client_secret", clientSecret client)+        , ("grant_type", "refresh_token")+        , ("refresh_token", refreshToken tokens)+        ]++    return $ toOAuth2Tokens (refreshToken tokens) refreshed++postTokens :: FromJSON a => [(ByteString, String)] -> IO a+postTokens params = do+    request <- parseUrl "https://accounts.google.com/o/oauth2/token"++    let params' = map (second C8.pack) params++    fmap unsafeDecode $ withManager $ httpLbs $ urlEncodedBody params' request++cachedTokens :: FilePath -> IO (Maybe OAuth2Tokens)+cachedTokens tokenFile = do+    result <- fmap (fmap reads) $ try $ readFile tokenFile++    return $ case result of+        Right ((t,_):_) -> Just t+        _ -> Nothing++cacheTokens :: FilePath -> OAuth2Tokens -> IO OAuth2Tokens+cacheTokens tokenFile t = fmap (const t) $ try $ writeFile tokenFile (show t)++permissionUrl :: OAuth2Client -> [OAuth2Scope] -> String+permissionUrl client scopes =+    "https://accounts.google.com/o/oauth2/auth"+    <> "?response_type=code"+    <> "&client_id=" <> clientId client+    <> "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"+    <> "&scope=" <> intercalate "+" (map urlEncode scopes)++redirectUri :: String+redirectUri = "urn:ietf:wg:oauth:2.0:oob"++-- With token responses, we assume that if we don't get an HTTP exception, then+-- the response body will parse correctly.+unsafeDecode :: FromJSON a => Response BL.ByteString -> a+unsafeDecode = fromJust . decode . responseBody++try :: IO a -> IO (Either E.IOException a)+try = E.try
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}