yesod-auth-oauth2 0.0.12 → 0.1.0
raw patch · 7 files changed
+244/−261 lines, 7 filesdep +randomdep −uuiddep ~basedep ~hoauth2PVP ok
version bump matches the API change (PVP)
Dependencies added: random
Dependencies removed: uuid
Dependency ranges changed: base, hoauth2
API changes (from Hackage documentation)
- Yesod.Auth.OAuth2.Google: oauth2Google :: YesodAuth m => Text -> Text -> AuthPlugin m
- Yesod.Auth.OAuth2.Learn: instance FromJSON LearnResponse
- Yesod.Auth.OAuth2.Learn: instance FromJSON LearnUser
- Yesod.Auth.OAuth2.Learn: oauth2Learn :: YesodAuth m => Text -> Text -> AuthPlugin m
+ Yesod.Auth.OAuth2.Upcase: instance FromJSON UpcaseResponse
+ Yesod.Auth.OAuth2.Upcase: instance FromJSON UpcaseUser
+ Yesod.Auth.OAuth2.Upcase: oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m
Files
- Yesod/Auth/OAuth2.hs +54/−29
- Yesod/Auth/OAuth2/Github.hs +40/−59
- Yesod/Auth/OAuth2/Google.hs +0/−30
- Yesod/Auth/OAuth2/Learn.hs +0/−78
- Yesod/Auth/OAuth2/Spotify.hs +54/−46
- Yesod/Auth/OAuth2/Upcase.hs +85/−0
- yesod-auth-oauth2.cabal +11/−19
Yesod/Auth/OAuth2.hs view
@@ -1,11 +1,13 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -- | -- -- Generic OAuth2 plugin for Yesod ----- * See Yesod.Auth.OAuth2.Learn for example usage.+-- * See Yesod.Auth.OAuth2.GitHub for example usage. -- module Yesod.Auth.OAuth2 ( authOAuth2@@ -14,23 +16,29 @@ , module Network.OAuth.OAuth2 ) where +#if __GLASGOW_HASKELL__ < 710+import Control.Applicative ((<$>))+#endif+ import Control.Exception.Lifted import Control.Monad.IO.Class import Data.ByteString (ByteString)-import Data.Text (Text)+import Data.Monoid ((<>))+import Data.Text (Text, pack) import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding.Error (lenientDecode) import Data.Typeable+import Network.HTTP.Conduit (Manager) import Network.OAuth.OAuth2-import Network.HTTP.Conduit(Manager)+import System.Random import Yesod.Auth import Yesod.Core import Yesod.Form -import qualified Data.ByteString.Lazy as BSL+import qualified Data.ByteString.Lazy as BL -- | Provider name and Aeson parse error-data YesodOAuth2Exception = InvalidProfileResponse Text BSL.ByteString+data YesodOAuth2Exception = InvalidProfileResponse Text BL.ByteString deriving (Show, Typeable) instance Exception YesodOAuth2Exception@@ -49,35 +57,52 @@ -> AuthPlugin m authOAuth2 name oauth getCreds = AuthPlugin name dispatch login - where- url = PluginR name ["callback"]+ where+ url = PluginR name ["callback"] - withCallback = do- tm <- getRouteToParent- render <- lift $ getUrlRender- return $ oauth { oauthCallback = Just $ encodeUtf8 $ render $ tm url }+ withCallback csrfToken = do+ tm <- getRouteToParent+ render <- lift getUrlRender+ return oauth+ { oauthCallback = Just $ encodeUtf8 $ render $ tm url+ , oauthOAuthorizeEndpoint = oauthOAuthorizeEndpoint oauth+ <> "&state=" <> encodeUtf8 csrfToken+ } - dispatch "GET" ["forward"] = do- authUrl <- fmap (bsToText . authorizationUrl) withCallback- lift $ redirect authUrl+ dispatch "GET" ["forward"] = do+ csrfToken <- liftIO generateToken+ setSession tokenSessionKey csrfToken+ authUrl <- bsToText . authorizationUrl <$> withCallback csrfToken+ lift $ redirect authUrl - dispatch "GET" ["callback"] = do- code <- lift $ runInputGet $ ireq textField "code"- oauth' <- withCallback- master <- lift getYesod- result <- liftIO $ fetchAccessToken (authHttpManager master) oauth' (encodeUtf8 code)- case result of- Left _ -> permissionDenied "Unable to retreive OAuth2 token"- Right token -> do- creds <- liftIO $ getCreds (authHttpManager master) token- lift $ setCredsRedirect creds+ dispatch "GET" ["callback"] = do+ newToken <- lookupGetParam "state"+ oldToken <- lookupSession tokenSessionKey+ deleteSession tokenSessionKey+ case newToken of+ Just csrfToken | newToken == oldToken -> do+ code <- lift $ runInputGet $ ireq textField "code"+ oauth' <- withCallback csrfToken+ master <- lift getYesod+ result <- liftIO $ fetchAccessToken (authHttpManager master) oauth' (encodeUtf8 code)+ case result of+ Left _ -> permissionDenied "Unable to retreive OAuth2 token"+ Right token -> do+ creds <- liftIO $ getCreds (authHttpManager master) token+ lift $ setCredsRedirect creds+ _ ->+ permissionDenied "Invalid OAuth2 state token" - dispatch _ _ = notFound+ dispatch _ _ = notFound - login tm = do- render <- getUrlRender- let oaUrl = render $ tm $ oauth2Url name- [whamlet| <a href=#{oaUrl}>Login via #{name} |]+ generateToken = pack . take 30 . randomRs ('a', 'z') <$> newStdGen++ tokenSessionKey :: Text+ tokenSessionKey = "_yesod_oauth2_" <> name++ login tm = [whamlet|+ <a href=@{tm $ oauth2Url name}>Login via #{name}+ |] bsToText :: ByteString -> Text bsToText = decodeUtf8With lenientDecode
Yesod/Auth/OAuth2/Github.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} -- | --@@ -13,36 +14,35 @@ , module Yesod.Auth.OAuth2 ) where -import Control.Applicative ((<$>), (<*>))+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative ((<$>), (<*>), pure)+#endif+ import Control.Exception.Lifted import Control.Monad (mzero) import Data.Aeson+import Data.Monoid ((<>)) import Data.Text (Text)-import Data.Monoid (mappend) import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import Network.HTTP.Conduit (Manager) import Yesod.Auth import Yesod.Auth.OAuth2-import Yesod.Core-import Yesod.Form-import Network.HTTP.Conduit(Manager)-import Data.UUID (toString)-import Data.UUID.V4 (nextRandom)-import qualified Data.ByteString as BS+ import qualified Data.Text as T data GithubUser = GithubUser- { githubUserId :: Int- , githubUserName :: Maybe Text+ { githubUserId :: Int+ , githubUserName :: Maybe Text , githubUserLogin :: Text , githubUserAvatarUrl :: Text } instance FromJSON GithubUser where- parseJSON (Object o) =- GithubUser <$> o .: "id"- <*> o .:? "name"- <*> o .: "login"- <*> o .: "avatar_url"+ parseJSON (Object o) = GithubUser+ <$> o .: "id"+ <*> o .:? "name"+ <*> o .: "login"+ <*> o .: "avatar_url" parseJSON _ = mzero @@ -51,8 +51,8 @@ } instance FromJSON GithubUserEmail where- parseJSON (Object o) =- GithubUserEmail <$> o .: "email"+ parseJSON (Object o) = GithubUserEmail+ <$> o .: "email" parseJSON _ = mzero @@ -67,37 +67,15 @@ -> Text -- ^ Client Secret -> [Text] -- ^ List of scopes to request -> AuthPlugin m-oauth2GithubScoped clientId clientSecret scopes = basicPlugin {apDispatch = dispatch}- where- oauth = OAuth2- { oauthClientId = encodeUtf8 clientId- , oauthClientSecret = encodeUtf8 clientSecret- , oauthOAuthorizeEndpoint = encodeUtf8 $ "https://github.com/login/oauth/authorize?scope=" `T.append` T.intercalate "," scopes- , oauthAccessTokenEndpoint = "https://github.com/login/oauth/access_token"- , oauthCallback = Nothing- }-- withState state = authOAuth2 "github"- (oauth {oauthOAuthorizeEndpoint = oauthOAuthorizeEndpoint oauth `BS.append` "&state=" `BS.append` encodeUtf8 state})- fetchGithubProfile-- basicPlugin = authOAuth2 "github" oauth fetchGithubProfile-- dispatch "GET" ["forward"] = do- state <- liftIO $ fmap (T.pack . toString) nextRandom- setSession "githubState" state- apDispatch (withState state) "GET" ["forward"]-- dispatch "GET" ["callback"] = do- state <- lift $ runInputGet $ ireq textField "state"- savedState <- lookupSession "githubState"- _ <- apDispatch basicPlugin "GET" ["callback"]- case savedState of- Just saved | saved == state -> apDispatch basicPlugin "GET" ["callback"]- Just saved -> invalidArgs ["state: " `mappend` state `mappend` ", and not: " `mappend` saved]- _ -> invalidArgs ["state: " `mappend` state]-- dispatch method ps = apDispatch basicPlugin method ps+oauth2GithubScoped clientId clientSecret scopes = authOAuth2 "github" oauth fetchGithubProfile+ where+ oauth = OAuth2+ { oauthClientId = encodeUtf8 clientId+ , oauthClientSecret = encodeUtf8 clientSecret+ , oauthOAuthorizeEndpoint = encodeUtf8 $ "https://github.com/login/oauth/authorize?scope=" <> T.intercalate "," scopes+ , oauthAccessTokenEndpoint = "https://github.com/login/oauth/access_token"+ , oauthCallback = Nothing+ } fetchGithubProfile :: Manager -> AccessToken -> IO (Creds m) fetchGithubProfile manager token = do@@ -111,14 +89,17 @@ (_, Left err) -> throwIO $ InvalidProfileResponse "github" err toCreds :: GithubUser -> [GithubUserEmail] -> AccessToken -> Creds m-toCreds user userMail token = Creds "github"- (T.pack $ show $ githubUserId user)- cExtra- where- cExtra = [ ("email", githubUserEmail $ head userMail)- , ("login", githubUserLogin user)- , ("avatar_url", githubUserAvatarUrl user)- , ("access_token", decodeUtf8 $ accessToken token)- ] ++ (maybeName $ githubUserName user)- maybeName Nothing = []- maybeName (Just name) = [("name", name)]+toCreds user userMail token = Creds+ { credsPlugin = "github"+ , credsIdent = T.pack $ show $ githubUserId user+ , credsExtra =+ [ ("email", githubUserEmail $ head userMail)+ , ("login", githubUserLogin user)+ , ("avatar_url", githubUserAvatarUrl user)+ , ("access_token", decodeUtf8 $ accessToken token)+ ] ++ maybeName (githubUserName user)+ }++ where+ maybeName Nothing = []+ maybeName (Just name) = [("name", name)]
− Yesod/Auth/OAuth2/Google.hs
@@ -1,30 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- |------ OAuth2 plugin for http://google.com------ * Note: this module is unfinished, do not use.----module Yesod.Auth.OAuth2.Google- ( oauth2Google- , module Yesod.Auth.OAuth2- ) where--import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8)-import Yesod.Auth-import Yesod.Auth.OAuth2--oauth2Google :: YesodAuth m- => Text -- ^ Client ID- -> Text -- ^ Client Secret- -> AuthPlugin m-oauth2Google clientId clientSecret = authOAuth2 "google"- (OAuth2- { oauthClientId = encodeUtf8 clientId- , oauthClientSecret = encodeUtf8 clientSecret- , oauthOAuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth"- , oauthAccessTokenEndpoint = "https://accounts.google.com/o/oauth2/token"- , oauthCallback = Nothing- })- undefined -- TODO
− Yesod/Auth/OAuth2/Learn.hs
@@ -1,78 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- |------ OAuth2 plugin for http://learn.thoughtbot.com------ * Authenticates against learn--- * Uses learn user id as credentials identifier--- * Returns first_name, last_name, and email as extras----module Yesod.Auth.OAuth2.Learn- ( oauth2Learn- , module Yesod.Auth.OAuth2- ) where--import Control.Applicative ((<$>), (<*>))-import Control.Exception.Lifted-import Control.Monad (mzero)-import Data.Aeson-import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8)-import Yesod.Auth-import Yesod.Auth.OAuth2-import Network.HTTP.Conduit(Manager)-import qualified Data.Text as T--data LearnUser = LearnUser- { learnUserId :: Int- , learnUserFirstName :: Text- , learnUserLastName :: Text- , learnUserEmail :: Text- }--instance FromJSON LearnUser where- parseJSON (Object o) =- LearnUser <$> o .: "id"- <*> o .: "first_name"- <*> o .: "last_name"- <*> o .: "email"-- parseJSON _ = mzero--data LearnResponse = LearnResponse LearnUser--instance FromJSON LearnResponse where- parseJSON (Object o) =- LearnResponse <$> o .: "user"-- parseJSON _ = mzero--oauth2Learn :: YesodAuth m- => Text -- ^ Client ID- -> Text -- ^ Client Secret- -> AuthPlugin m-oauth2Learn clientId clientSecret = authOAuth2 "learn"- (OAuth2- { oauthClientId = encodeUtf8 clientId- , oauthClientSecret = encodeUtf8 clientSecret- , oauthOAuthorizeEndpoint = "http://learn.thoughtbot.com/oauth/authorize"- , oauthAccessTokenEndpoint = "http://learn.thoughtbot.com/oauth/token"- , oauthCallback = Nothing- })- fetchLearnProfile--fetchLearnProfile :: Manager -> AccessToken -> IO (Creds m)-fetchLearnProfile manager token = do- result <- authGetJSON manager token "http://learn.thoughtbot.com/api/v1/me.json"-- case result of- Right (LearnResponse user) -> return $ toCreds user- Left err -> throwIO $ InvalidProfileResponse "learn" err--toCreds :: LearnUser -> Creds m-toCreds user = Creds "learn"- (T.pack $ show $ learnUserId user)- [ ("first_name", learnUserFirstName user)- , ("last_name" , learnUserLastName user)- , ("email" , learnUserEmail user)- ]
Yesod/Auth/OAuth2/Spotify.hs view
@@ -1,11 +1,18 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-}-+-- |+--+-- OAuth2 plugin for http://spotify.com+-- module Yesod.Auth.OAuth2.Spotify ( oauth2Spotify , module Yesod.Auth.OAuth2 ) where -import Control.Applicative ((<$>), (<*>))+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative ((<$>), (<*>), pure)+#endif+ import Control.Exception.Lifted import Control.Monad (mzero) import Data.Aeson@@ -16,6 +23,7 @@ import Network.HTTP.Conduit(Manager) import Yesod.Auth import Yesod.Auth.OAuth2+ import qualified Data.ByteString as B import qualified Data.Text as T @@ -26,49 +34,50 @@ } instance FromJSON SpotifyUserImage where- parseJSON (Object v) = SpotifyUserImage <$>- v .: "height" <*>- v .: "width" <*>- v .: "url"+ parseJSON (Object v) = SpotifyUserImage+ <$> v .: "height"+ <*> v .: "width"+ <*> v .: "url" parseJSON _ = mzero data SpotifyUser = SpotifyUser- { spotifyUserId :: Text- , spotifyUserHref :: Text- , spotifyUserUri :: Text+ { spotifyUserId :: Text+ , spotifyUserHref :: Text+ , spotifyUserUri :: Text , spotifyUserDisplayName :: Maybe Text- , spotifyUserProduct :: Maybe Text- , spotifyUserCountry :: Maybe Text- , spotifyUserEmail :: Maybe Text- , spotifyUserImages :: Maybe [SpotifyUserImage]+ , spotifyUserProduct :: Maybe Text+ , spotifyUserCountry :: Maybe Text+ , spotifyUserEmail :: Maybe Text+ , spotifyUserImages :: Maybe [SpotifyUserImage] } instance FromJSON SpotifyUser where- parseJSON (Object v) = SpotifyUser <$>- v .: "id" <*>- v .: "href" <*>- v .: "uri" <*>- v .:? "display_name" <*>- v .:? "product" <*>- v .:? "country" <*>- v .:? "email" <*>- v .:? "images"+ parseJSON (Object v) = SpotifyUser+ <$> v .: "id"+ <*> v .: "href"+ <*> v .: "uri"+ <*> v .:? "display_name"+ <*> v .:? "product"+ <*> v .:? "country"+ <*> v .:? "email"+ <*> v .:? "images"+ parseJSON _ = mzero oauth2Spotify :: YesodAuth m- => Text -- ^ Client ID- -> Text -- ^ Client Secret- -> [ByteString] -- ^ Scopes- -> AuthPlugin m+ => Text -- ^ Client ID+ -> Text -- ^ Client Secret+ -> [ByteString] -- ^ Scopes+ -> AuthPlugin m oauth2Spotify clientId clientSecret scope = authOAuth2 "spotify"- (OAuth2- { oauthClientId = encodeUtf8 clientId- , oauthClientSecret = encodeUtf8 clientSecret- , oauthOAuthorizeEndpoint = B.append "https://accounts.spotify.com/authorize?scope=" (B.intercalate "%20" scope)+ OAuth2+ { oauthClientId = encodeUtf8 clientId+ , oauthClientSecret = encodeUtf8 clientSecret+ , oauthOAuthorizeEndpoint = B.append "https://accounts.spotify.com/authorize?scope=" (B.intercalate "%20" scope) , oauthAccessTokenEndpoint = "https://accounts.spotify.com/api/token"- , oauthCallback = Nothing- })+ , oauthCallback = Nothing+ } fetchSpotifyProfile fetchSpotifyProfile :: Manager -> AccessToken -> IO (Creds m)@@ -79,9 +88,11 @@ Left err -> throwIO $ InvalidProfileResponse "spotify" err toCreds :: SpotifyUser -> Creds m-toCreds user = Creds "spotify"- (spotifyUserId user)- (mapMaybe getExtra extrasTemplate)+toCreds user = Creds+ { credsPlugin = "spotify"+ , credsIdent = spotifyUserId user+ , credsExtra = mapMaybe getExtra extrasTemplate+ } where userImage :: Maybe SpotifyUserImage@@ -90,18 +101,15 @@ userImagePart :: (SpotifyUserImage -> Maybe a) -> Maybe a userImagePart getter = userImage >>= getter - extrasTemplate = [ ("href" , Just $ spotifyUserHref user)- , ("uri" , Just $ spotifyUserUri user)+ extrasTemplate = [ ("href", Just $ spotifyUserHref user)+ , ("uri", Just $ spotifyUserUri user) , ("display_name", spotifyUserDisplayName user)- , ("product" , spotifyUserProduct user)- , ("country" , spotifyUserCountry user)- , ("email" , spotifyUserEmail user)- , ("image_url" , userImage >>=- return . spotifyUserImageUrl)- , ("image_height", userImagePart spotifyUserImageHeight >>=- return . T.pack . show)- , ("image_width" , userImagePart spotifyUserImageWidth >>=- return . T.pack . show)+ , ("product", spotifyUserProduct user)+ , ("country", spotifyUserCountry user)+ , ("email", spotifyUserEmail user)+ , ("image_url", spotifyUserImageUrl <$> userImage)+ , ("image_height", T.pack . show <$> userImagePart spotifyUserImageHeight)+ , ("image_width", T.pack . show <$> userImagePart spotifyUserImageWidth) ] getExtra :: (Text, Maybe Text) -> Maybe (Text, Text)
+ Yesod/Auth/OAuth2/Upcase.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+--+-- OAuth2 plugin for http://upcase.com+--+-- * Authenticates against upcase+-- * Uses upcase user id as credentials identifier+-- * Returns first_name, last_name, and email as extras+--+module Yesod.Auth.OAuth2.Upcase+ ( oauth2Upcase+ , module Yesod.Auth.OAuth2+ ) where++#if __GLASGOW_HASKELL__ < 710+import Control.Applicative ((<$>), (<*>), pure)+#endif++import Control.Exception.Lifted+import Control.Monad (mzero)+import Data.Aeson+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Yesod.Auth+import Yesod.Auth.OAuth2+import Network.HTTP.Conduit(Manager)+import qualified Data.Text as T++data UpcaseUser = UpcaseUser+ { upcaseUserId :: Int+ , upcaseUserFirstName :: Text+ , upcaseUserLastName :: Text+ , upcaseUserEmail :: Text+ }++instance FromJSON UpcaseUser where+ parseJSON (Object o) = UpcaseUser+ <$> o .: "id"+ <*> o .: "first_name"+ <*> o .: "last_name"+ <*> o .: "email"++ parseJSON _ = mzero++data UpcaseResponse = UpcaseResponse UpcaseUser++instance FromJSON UpcaseResponse where+ parseJSON (Object o) = UpcaseResponse+ <$> o .: "user"++ parseJSON _ = mzero++oauth2Upcase :: YesodAuth m+ => Text -- ^ Client ID+ -> Text -- ^ Client Secret+ -> AuthPlugin m+oauth2Upcase clientId clientSecret = authOAuth2 "upcase"+ OAuth2+ { oauthClientId = encodeUtf8 clientId+ , oauthClientSecret = encodeUtf8 clientSecret+ , oauthOAuthorizeEndpoint = "http://upcase.com/oauth/authorize"+ , oauthAccessTokenEndpoint = "http://upcase.com/oauth/token"+ , oauthCallback = Nothing+ }+ fetchUpcaseProfile++fetchUpcaseProfile :: Manager -> AccessToken -> IO (Creds m)+fetchUpcaseProfile manager token = do+ result <- authGetJSON manager token "http://upcase.com/api/v1/me.json"++ case result of+ Right (UpcaseResponse user) -> return $ toCreds user+ Left err -> throwIO $ InvalidProfileResponse "upcase" err++toCreds :: UpcaseUser -> Creds m+toCreds user = Creds+ { credsPlugin = "upcase"+ , credsIdent = T.pack $ show $ upcaseUserId user+ , credsExtra =+ [ ("first_name", upcaseUserFirstName user)+ , ("last_name" , upcaseUserLastName user)+ , ("email" , upcaseUserEmail user)+ ]+ }
yesod-auth-oauth2.cabal view
@@ -1,18 +1,16 @@ name: yesod-auth-oauth2-version: 0.0.12+version: 0.1.0 license: BSD3 license-file: LICENSE author: Tom Streller-maintainer: Tom Streller-synopsis: Library to authenticate with OAuth 2.0 for Yesod web applications.-description: OAuth 2.0 authentication+maintainer: Pat Brisbin <pat@thoughtbot.com>+synopsis: OAuth 2.0 authentication plugins+description: Library to authenticate with OAuth 2.0 for Yesod web applications. category: Web stability: Experimental cabal-version: >= 1.6 build-type: Simple-homepage: http://github.com/scan/yesod-auth-oauth2--flag ghc7+homepage: http://github.com/thoughtbot/yesod-auth-oauth2 flag network-uri description: Get Network.URI from the network-uri package@@ -24,34 +22,28 @@ else build-depends: network < 2.6 - if flag(ghc7)- build-depends: base >= 4.3 && < 5- cpp-options: -DGHC7- else- build-depends: base >= 4 && < 4.3-- build-depends: bytestring >= 0.9.1.4+ build-depends: base >= 4.5 && < 5+ , bytestring >= 0.9.1.4 , http-conduit >= 2.0 && < 3.0 , http-types >= 0.8 && < 0.9 , aeson >= 0.6 && < 0.9 , yesod-core >= 1.2 && < 1.5 , authenticate >= 1.3.2.7 && < 1.4+ , random , yesod-auth >= 1.3 && < 1.5 , text >= 0.7 && < 2.0 , yesod-form >= 1.3 && < 1.5 , transformers >= 0.2.2 && < 0.5- , hoauth2 >= 0.4.1 && < 0.5+ , hoauth2 >= 0.4.7 && < 0.5 , lifted-base >= 0.2 && < 0.4- , uuid >= 1.3 && < 1.4 exposed-modules: Yesod.Auth.OAuth2- Yesod.Auth.OAuth2.Google- Yesod.Auth.OAuth2.Learn Yesod.Auth.OAuth2.Github Yesod.Auth.OAuth2.Spotify+ Yesod.Auth.OAuth2.Upcase ghc-options: -Wall source-repository head type: git- location: git://github.com/scan/authenticate-oauth2.git+ location: https://github.com/thoughtbot/authenticate-oauth2.git