gogol-1.0.0.0: src/Gogol/Auth/InstalledApplication.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Gogol.Auth.InstalledApplication
-- Copyright : (c) 2015-2022 Brendan Hay
-- License : Mozilla Public License, v. 2.0.
-- Maintainer : Brendan Hay <brendan.g.hay@gmail.com>
-- Stability : provisional
-- Portability : non-portable (GHC extensions)
--
-- Credentials for applications that are installed on devices such as
-- computers, cell phones, or a tablet. Installed apps are distributed to
-- individual machines, and it is assumed that these apps securely store secrets.
--
-- These apps might access a Google service while the user is present at the
-- application, or when the application is running in the background.
--
-- /See:/ <https://developers.google.com/identity/protocols/OAuth2InstalledApp Installed Application Documentation>.
module Gogol.Auth.InstalledApplication
( installedApplication,
-- * Forming the URL
AccessType (..),
redirectURI,
formURL,
formAccessTypeURL,
formURLWith,
formAccessTypeURLWith,
-- * Internal Exchange and Refresh
exchangeCode,
refreshToken,
)
where
import Control.Monad.Catch (MonadCatch)
import Control.Monad.IO.Class (MonadIO)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import GHC.TypeLits (Symbol)
import Gogol.Auth.Scope
( KnownScopes (..),
queryEncodeScopes,
)
import Gogol.Internal.Auth
import Gogol.Internal.Logger (Logger)
import Gogol.Prelude
import Network.HTTP.Conduit (Manager)
import Network.HTTP.Conduit qualified as Client
-- | Create new Installed Application credentials.
--
-- Since it is intended that the user opens the URL generated by 'formURL' in a browser
-- and the resulting 'OAuthCode' is then received out-of-band,
-- you must ensure that the scopes passed to 'formURL' and the type of 'OAuthCode'
-- correctly match, otherwise an authorization error will occur.
--
-- For example, doing this via 'getLine' and copy-paste:
--
-- > {-# LANGUAGE ScopedTypeVariables #-}
--
-- > import Data.Proxy (Proxy (..))
-- > import Data.Text as T
-- > import Data.Text.IO as T
-- > import System.Exit (exitFailure)
-- > import System.Info (os)
-- > import System.Process (rawSystem)
--
-- > redirectPrompt :: KnownScopes (s :: [Symbol]) => OAuthClient -> proxy s -> IO (OAuthCode s)
-- > redirectPrompt c p = do
-- > let url = formURL c p
-- > T.putStrLn $ "Opening URL " `T.append` url
-- > _ <- case os of
-- > "darwin" -> rawSystem "open" [unpack url]
-- > "linux" -> rawSystem "xdg-open" [unpack url]
-- > _ -> T.putStrLn "Unsupported OS" >> exitFailure
-- > T.putStrLn "Please input the authorisation code: "
-- > OAuthCode <$> T.getLine
--
-- This ensures the scopes passed to 'formURL' and the type of 'OAuthCode' 's'
-- are correct.
installedApplication :: OAuthClient -> OAuthCode s -> Credentials s
installedApplication = FromClient
-- /See:/ <https://developers.google.com/identity/protocols/OAuth2WebServer#offline>
data AccessType = Online | Offline deriving (Show, Eq)
-- | The redirection URI used in 'formURL': @urn:ietf:wg:oauth:2.0:oob@.
redirectURI :: Text
redirectURI = "urn:ietf:wg:oauth:2.0:oob"
-- | Given an 'OAuthClient' and a list of scopes to authorize,
-- construct a URL that can be used to obtain the 'OAuthCode'.
--
-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl Forming the URL>.
formURL :: (KnownScopes (s :: [Symbol])) => OAuthClient -> proxy s -> Text
formURL c = formURLWith c . scopeVals
-- | 'formURL' for 'AccessType'
--
-- /See:/ 'formUrl'.
formAccessTypeURL :: (KnownScopes (s :: [Symbol])) => OAuthClient -> AccessType -> proxy s -> Text
formAccessTypeURL c a = formAccessTypeURLWith c a . scopeVals
-- | Form a URL using 'OAuthScope' values.
--
-- /See:/ 'formURL'.
formURLWith :: OAuthClient -> [OAuthScope] -> Text
formURLWith c ss =
accountsURL
<> "?response_type=code"
<> "&client_id="
<> toQueryParam (_clientId c)
<> "&redirect_uri="
<> redirectURI
<> "&scope="
<> Text.decodeUtf8 (queryEncodeScopes ss)
-- | 'formURLWith' for 'AccessType'
--
-- /See:/ 'formURLWith'.
formAccessTypeURLWith :: OAuthClient -> AccessType -> [OAuthScope] -> Text
formAccessTypeURLWith c a ss =
formURLWith c ss
<> "&access_type="
<> (Text.toLower . Text.pack $ show a)
-- | Exchange 'OAuthClient' details and the received 'OAuthCode' for a new
-- 'OAuthToken'.
--
-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse Exchanging the code>.
exchangeCode ::
(MonadIO m, MonadCatch m) =>
OAuthClient ->
(OAuthCode s) ->
Logger ->
Manager ->
m (OAuthToken s)
exchangeCode c n =
refreshRequest $
tokenRequest
{ Client.requestBody =
textBody $
"grant_type=authorization_code"
<> "&client_id="
<> toQueryParam (_clientId c)
<> "&client_secret="
<> toQueryParam (_clientSecret c)
<> "&code="
<> toQueryParam n
<> "&redirect_uri="
<> redirectURI
}
-- | Perform a refresh to obtain a valid 'OAuthToken' with a new expiry time.
--
-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#offline Refreshing tokens>.
refreshToken ::
(MonadIO m, MonadCatch m) =>
OAuthClient ->
(OAuthToken s) ->
Logger ->
Manager ->
m (OAuthToken s)
refreshToken c t =
refreshRequest $
tokenRequest
{ Client.requestBody =
textBody $
"grant_type=refresh_token"
<> "&client_id="
<> toQueryParam (_clientId c)
<> "&client_secret="
<> toQueryParam (_clientSecret c)
<> maybe mempty ("&refresh_token=" <>) (toQueryParam <$> _tokenRefresh t)
}