diff --git a/Yesod/Auth/OAuth2.hs b/Yesod/Auth/OAuth2.hs
--- a/Yesod/Auth/OAuth2.hs
+++ b/Yesod/Auth/OAuth2.hs
@@ -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
diff --git a/Yesod/Auth/OAuth2/Github.hs b/Yesod/Auth/OAuth2/Github.hs
--- a/Yesod/Auth/OAuth2/Github.hs
+++ b/Yesod/Auth/OAuth2/Github.hs
@@ -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)]
diff --git a/Yesod/Auth/OAuth2/Google.hs b/Yesod/Auth/OAuth2/Google.hs
deleted file mode 100644
--- a/Yesod/Auth/OAuth2/Google.hs
+++ /dev/null
@@ -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
diff --git a/Yesod/Auth/OAuth2/Learn.hs b/Yesod/Auth/OAuth2/Learn.hs
deleted file mode 100644
--- a/Yesod/Auth/OAuth2/Learn.hs
+++ /dev/null
@@ -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)
-    ]
diff --git a/Yesod/Auth/OAuth2/Spotify.hs b/Yesod/Auth/OAuth2/Spotify.hs
--- a/Yesod/Auth/OAuth2/Spotify.hs
+++ b/Yesod/Auth/OAuth2/Spotify.hs
@@ -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)
diff --git a/Yesod/Auth/OAuth2/Upcase.hs b/Yesod/Auth/OAuth2/Upcase.hs
new file mode 100644
--- /dev/null
+++ b/Yesod/Auth/OAuth2/Upcase.hs
@@ -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)
+        ]
+    }
diff --git a/yesod-auth-oauth2.cabal b/yesod-auth-oauth2.cabal
--- a/yesod-auth-oauth2.cabal
+++ b/yesod-auth-oauth2.cabal
@@ -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
