diff --git a/URI/ByteString/Extension.hs b/URI/ByteString/Extension.hs
new file mode 100644
--- /dev/null
+++ b/URI/ByteString/Extension.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module URI.ByteString.Extension where
+
+import Data.ByteString (ByteString)
+import Data.String (IsString(..))
+import Data.Text (Text)
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Lens.Micro
+
+import qualified Data.ByteString.Char8 as C8
+
+import URI.ByteString
+
+instance IsString Scheme where
+    fromString = Scheme . fromString
+
+instance IsString Host where
+    fromString = Host . fromString
+
+instance IsString (URIRef Absolute) where
+    fromString = either (error . show) id
+        . parseURI strictURIParserOptions
+        . C8.pack
+
+instance IsString (URIRef Relative) where
+    fromString = either (error . show) id
+        . parseRelativeRef strictURIParserOptions
+        . C8.pack
+
+fromText :: Text -> Maybe URI
+fromText = either (const Nothing) Just
+    . parseURI strictURIParserOptions
+    . encodeUtf8
+
+unsafeFromText :: Text -> URI
+unsafeFromText = either (error . show) id
+    . parseURI strictURIParserOptions
+    . encodeUtf8
+
+toText :: URI -> Text
+toText = decodeUtf8 . serializeURIRef'
+
+fromRelative :: Scheme -> Host -> RelativeRef -> URI
+fromRelative s h = flip withHost h . toAbsolute s
+
+withHost :: URIRef a -> Host -> URIRef a
+withHost u h = u & authorityL %~ maybe
+    (Just $ Authority Nothing h Nothing)
+    (\a -> Just $ a & authorityHostL .~ h)
+
+withQuery :: URIRef a -> [(ByteString, ByteString)] -> URIRef a
+withQuery u q = u & (queryL . queryPairsL) %~ (++ q)
diff --git a/Yesod/Auth/OAuth2.hs b/Yesod/Auth/OAuth2.hs
--- a/Yesod/Auth/OAuth2.hs
+++ b/Yesod/Auth/OAuth2.hs
@@ -3,6 +3,7 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TupleSections #-}
 -- |
 --
 -- Generic OAuth2 plugin for Yesod
@@ -15,8 +16,12 @@
     , oauth2Url
     , fromProfileURL
     , YesodOAuth2Exception(..)
+    , invalidProfileResponse
+    , scopeParam
     , maybeExtra
     , module Network.OAuth.OAuth2
+    , module URI.ByteString
+    , module URI.ByteString.Extension
     ) where
 
 #if __GLASGOW_HASKELL__ < 710
@@ -26,20 +31,22 @@
 import Control.Exception.Lifted
 import Control.Monad.IO.Class
 import Control.Monad (unless)
-import Data.ByteString (ByteString)
+import Data.Aeson (Value(..), encode)
 import Data.Monoid ((<>))
+import Data.ByteString (ByteString)
 import Data.Text (Text, pack)
-import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
-import Data.Text.Encoding.Error (lenientDecode)
+import Data.Text.Encoding (encodeUtf8)
 import Data.Typeable
 import Network.HTTP.Conduit (Manager)
-import Network.OAuth.OAuth2
+import Network.OAuth.OAuth2 hiding (error)
 import System.Random
+import URI.ByteString
+import URI.ByteString.Extension
 import Yesod.Auth
 import Yesod.Core
 
+import qualified Data.Text as T
 import qualified Data.ByteString.Lazy as BL
-import qualified Data.ByteString.Char8 as C8
 
 -- | Provider name and Aeson parse error
 data YesodOAuth2Exception = InvalidProfileResponse Text BL.ByteString
@@ -47,6 +54,14 @@
 
 instance Exception YesodOAuth2Exception
 
+-- | Construct an @'InvalidProfileResponse'@ exception from an @'OAuth2Error'@
+--
+-- This forces the @e@ in @'OAuth2Error' e@ to parse as a JSON @'Value'@ which
+-- is then re-encoded for the exception message.
+--
+invalidProfileResponse :: Text -> OAuth2Error Value -> YesodOAuth2Exception
+invalidProfileResponse name = InvalidProfileResponse name . encode
+
 oauth2Url :: Text -> AuthRoute
 oauth2Url name = PluginR name ["forward"]
 
@@ -57,11 +72,11 @@
 authOAuth2 :: YesodAuth m
            => Text   -- ^ Service name
            -> OAuth2 -- ^ Service details
-           -> (Manager -> AccessToken -> IO (Creds m))
-           -- ^ This function defines how to take an @'AccessToken'@ and
-           --   retrieve additional information about the user, to be
-           --   set in the session as @'Creds'@. Usually this means a
-           --   second authorized request to @api/me.json@.
+           -> (Manager -> OAuth2Token -> IO (Creds m))
+           -- ^ This function defines how to take an @'OAuth2Token'@ and
+           --   retrieve additional information about the user, to be set in the
+           --   session as @'Creds'@. Usually this means a second authorized
+           --   request to @api/me.json@.
            --
            --   See @'fromProfileURL'@ for an example.
            -> AuthPlugin m
@@ -76,7 +91,7 @@
                  => WidgetT m IO ()
                  -> Text
                  -> OAuth2
-                 -> (Manager -> AccessToken -> IO (Creds m))
+                 -> (Manager -> OAuth2Token -> IO (Creds m))
                  -> AuthPlugin m
 authOAuth2Widget widget name oauth getCreds = AuthPlugin name dispatch login
 
@@ -87,15 +102,15 @@
         tm <- getRouteToParent
         render <- lift getUrlRender
         return oauth
-            { oauthCallback = Just $ encodeUtf8 $ render $ tm url
+            { oauthCallback = Just $ unsafeFromText $ render $ tm url
             , oauthOAuthorizeEndpoint = oauthOAuthorizeEndpoint oauth
-                `appendQuery` "state=" <> encodeUtf8 csrfToken
+                `withQuery` [("state", encodeUtf8 csrfToken)]
             }
 
     dispatch "GET" ["forward"] = do
         csrfToken <- liftIO generateToken
         setSession tokenSessionKey csrfToken
-        authUrl <- bsToText . authorizationUrl <$> withCallback csrfToken
+        authUrl <- toText . authorizationUrl <$> withCallback csrfToken
         lift $ redirect authUrl
 
     dispatch "GET" ["callback"] = do
@@ -106,9 +121,9 @@
         code <- requireGetParam "code"
         oauth' <- withCallback csrfToken
         master <- lift getYesod
-        result <- liftIO $ fetchAccessToken (authHttpManager master) oauth' (encodeUtf8 code)
+        result <- liftIO $ fetchAccessToken (authHttpManager master) oauth' (ExchangeToken code)
         case result of
-            Left _ -> permissionDenied "Unable to retreive OAuth2 token"
+            Left _ -> permissionDenied "Unable to retrieve OAuth2 token"
             Right token -> do
                 creds <- liftIO $ getCreds (authHttpManager master) token
                 lift $ setCredsRedirect creds
@@ -134,25 +149,19 @@
                => Text           -- ^ Plugin name
                -> URI            -- ^ Profile URI
                -> (a -> Creds m) -- ^ Conversion to Creds
-               -> Manager -> AccessToken -> IO (Creds m)
+               -> Manager -> OAuth2Token -> IO (Creds m)
 fromProfileURL name url toCreds manager token = do
-    result <- authGetJSON manager token url
+    result <- authGetJSON manager (accessToken token) url
 
     case result of
         Right profile -> return $ toCreds profile
-        Left err -> throwIO $ InvalidProfileResponse name err
-
-bsToText :: ByteString -> Text
-bsToText = decodeUtf8With lenientDecode
+        Left err -> throwIO $ invalidProfileResponse name err
 
-appendQuery :: ByteString -> ByteString -> ByteString
-appendQuery url query =
-    if '?' `C8.elem` url
-        then url <> "&" <> query
-        else url <> "?" <> query
+-- | A tuple of @scope@ and the given scopes separated by a delimiter
+scopeParam :: Text -> [Text] -> (ByteString, ByteString)
+scopeParam d = ("scope",) . encodeUtf8 . T.intercalate d
 
 -- | A helper for providing an optional value to credsExtra
---
 maybeExtra :: Text -> Maybe Text -> [(Text, Text)]
 maybeExtra k (Just v) = [(k, v)]
 maybeExtra _ Nothing  = []
diff --git a/Yesod/Auth/OAuth2/BattleNet.hs b/Yesod/Auth/OAuth2/BattleNet.hs
--- a/Yesod/Auth/OAuth2/BattleNet.hs
+++ b/Yesod/Auth/OAuth2/BattleNet.hs
@@ -51,30 +51,33 @@
                 -> Text -- ^ User region (e.g. "eu", "cn", "us")
                 -> WidgetT m IO () -- ^ Login widget
                 -> AuthPlugin m
-oAuth2BattleNet clientId clientSecret region widget = authOAuth2Widget widget "battle.net" oAuthData (makeCredentials region)
-  where oAuthData = OAuth2 { oauthClientId            = E.encodeUtf8 clientId
-                           , oauthClientSecret        = E.encodeUtf8 clientSecret
-                           , oauthOAuthorizeEndpoint  = E.encodeUtf8 ("https://" <> host <> "/oauth/authorize")
-                           , oauthAccessTokenEndpoint = E.encodeUtf8 ("https://" <> host <> "/oauth/token")
+oAuth2BattleNet clientId clientSecret region widget = authOAuth2Widget widget "battle.net" oAuthData $ makeCredentials region
+  where oAuthData = OAuth2 { oauthClientId            = clientId
+                           , oauthClientSecret        = clientSecret
+                           , oauthOAuthorizeEndpoint  = fromRelative "https" host "/oauth/authorize"
+                           , oauthAccessTokenEndpoint = fromRelative "https" host "/oauth/token"
                            , oauthCallback            = Nothing
                            }
-        host = let r = T.toLower region in
-                 case r of
-                   "cn" -> "www.battlenet.com.cn"
-                   _    -> r <> ".battle.net"
 
-makeCredentials :: Text -> Manager -> AccessToken -> IO (Creds m)
+        host = wwwHost $ T.toLower region
+
+makeCredentials :: Text -> Manager -> OAuth2Token -> IO (Creds m)
 makeCredentials region manager token = do
-    userResult <- authGetJSON manager token ("https://" <> host <> "/account/user") :: IO (OAuth2Result BattleNetUser)
+    userResult <- authGetJSON manager (accessToken token)
+        $ fromRelative "https" (apiHost $ T.toLower region) "/account/user"
+
     case userResult of
-         Left err -> throwIO $ InvalidProfileResponse "battle.net" err
+         Left err -> throwIO $ invalidProfileResponse "battle.net" err
          Right user -> return Creds
            { credsPlugin = "battle.net"
            , credsIdent  = T.pack $ show $ userId user
            , credsExtra  = [("battletag", battleTag user)]
            }
-  where host :: URI
-        host = let r = T.toLower region in
-                 case r of
-                   "cn" -> "api.battlenet.com.cn"
-                   _    -> E.encodeUtf8 r <> ".api.battle.net"
+
+apiHost :: Text -> Host
+apiHost "cn" = "api.battlenet.com.cn"
+apiHost region = Host $ E.encodeUtf8 $ region <> ".api.battle.net"
+
+wwwHost :: Text -> Host
+wwwHost "cn" = "www.battlenet.com.cn"
+wwwHost region = Host $ E.encodeUtf8 $ region <> ".battle.net"
diff --git a/Yesod/Auth/OAuth2/Bitbucket.hs b/Yesod/Auth/OAuth2/Bitbucket.hs
--- a/Yesod/Auth/OAuth2/Bitbucket.hs
+++ b/Yesod/Auth/OAuth2/Bitbucket.hs
@@ -23,12 +23,10 @@
 import Data.Aeson (FromJSON, Value(Object), parseJSON, (.:), (.:?))
 import Data.Maybe (fromMaybe)
 import Data.List (find)
-import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 import Network.HTTP.Conduit (Manager)
 import Yesod.Auth (YesodAuth, Creds(..), AuthPlugin)
-import Yesod.Auth.OAuth2 (AccessToken, YesodOAuth2Exception(InvalidProfileResponse), OAuth2(..), authOAuth2, maybeExtra, accessToken, authGetJSON)
+import Yesod.Auth.OAuth2
 
 import qualified Data.Text as T
 
@@ -106,24 +104,26 @@
 oauth2BitbucketScoped clientId clientSecret scopes = authOAuth2 "bitbucket" oauth fetchBitbucketProfile
   where
     oauth = OAuth2
-        { oauthClientId = encodeUtf8 clientId
-        , oauthClientSecret = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint = encodeUtf8 $ "https://bitbucket.com/site/oauth2/authorize?scope=" <> T.intercalate "," scopes
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://bitbucket.com/site/oauth2/authorize" `withQuery`
+            [ scopeParam "," scopes
+            ]
         , oauthAccessTokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"
         , oauthCallback = Nothing
         }
 
-fetchBitbucketProfile :: Manager -> AccessToken -> IO (Creds m)
+fetchBitbucketProfile :: Manager -> OAuth2Token -> IO (Creds m)
 fetchBitbucketProfile manager token = do
-    userResult <- authGetJSON manager token "https://api.bitbucket.com/2.0/user"
-    mailResult <- authGetJSON manager token "https://api.bitbucket.com/2.0/user/emails"
+    userResult <- authGetJSON manager (accessToken token) "https://api.bitbucket.com/2.0/user"
+    mailResult <- authGetJSON manager (accessToken token) "https://api.bitbucket.com/2.0/user/emails"
 
     case (userResult, mailResult) of
         (Right user, Right mails) -> return $ toCreds user (bitbucketEmails mails) token
-        (Left err, _) -> throwIO $ InvalidProfileResponse "bitbucket" err
-        (_, Left err) -> throwIO $ InvalidProfileResponse "bitbucket" err
+        (Left err, _) -> throwIO $ invalidProfileResponse "bitbucket" err
+        (_, Left err) -> throwIO $ invalidProfileResponse "bitbucket" err
 
-toCreds :: BitbucketUser -> [BitbucketUserEmail] -> AccessToken -> Creds m
+toCreds :: BitbucketUser -> [BitbucketUserEmail] -> OAuth2Token -> Creds m
 toCreds user userMails token = Creds
     { credsPlugin = "bitbucket"
     , credsIdent = T.pack $ show $ bitbucketUserId user
@@ -131,7 +131,7 @@
         [ ("email", bitbucketUserEmailAddress email)
         , ("login", bitbucketUserLogin user)
         , ("avatar_url", bitbucketLinkHref (bitbucketAvatarLink (bitbucketUserLinks user)))
-        , ("access_token", decodeUtf8 $ accessToken token)
+        , ("access_token", atoken $ accessToken token)
         ]
         ++ maybeExtra "name" (bitbucketUserName user)
         ++ maybeExtra "location" (bitbucketUserLocation user)
diff --git a/Yesod/Auth/OAuth2/EveOnline.hs b/Yesod/Auth/OAuth2/EveOnline.hs
--- a/Yesod/Auth/OAuth2/EveOnline.hs
+++ b/Yesod/Auth/OAuth2/EveOnline.hs
@@ -23,9 +23,7 @@
 import Control.Exception.Lifted
 import Control.Monad (mzero)
 import Data.Aeson
-import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 import Network.HTTP.Conduit (Manager)
 import Yesod.Auth
 import Yesod.Auth.OAuth2
@@ -86,22 +84,25 @@
 
   where
     oauth = OAuth2
-        { oauthClientId = encodeUtf8 clientId
-        , oauthClientSecret = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint = encodeUtf8 $ "https://login.eveonline.com/oauth/authorize?response_type=code&scope=" <> T.intercalate " " scopes
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://login.eveonline.com/oauth/authorize" `withQuery`
+            [ ("response_type", "code")
+            , scopeParam " " scopes
+            ]
         , oauthAccessTokenEndpoint = "https://login.eveonline.com/oauth/token"
         , oauthCallback = Nothing
         }
 
-fetchEveProfile :: Manager -> AccessToken -> IO (Creds m)
+fetchEveProfile :: Manager -> OAuth2Token -> IO (Creds m)
 fetchEveProfile manager token = do
-    userResult <- authGetJSON manager token "https://login.eveonline.com/oauth/verify"
+    userResult <- authGetJSON manager (accessToken token) $ "https://login.eveonline.com/oauth/verify"
 
     case userResult of
         Right user -> return $ toCreds user token
-        Left err-> throwIO $ InvalidProfileResponse "eveonline" err
+        Left err-> throwIO $ invalidProfileResponse "eveonline" err
 
-toCreds :: EveUser -> AccessToken -> Creds m
+toCreds :: EveUser -> OAuth2Token -> Creds m
 toCreds user token = Creds
     { credsPlugin = "eveonline"
     , credsIdent = T.pack $ show $ eveCharOwnerHash user
@@ -110,6 +111,6 @@
         , ("charId", T.pack . show . eveCharId $ user)
         , ("tokenType", eveTokenType user)
         , ("expires", eveUserExpire user)
-        , ("accessToken", decodeUtf8 . accessToken $ token)
+        , ("accessToken", atoken $ accessToken token)
         ]
     }
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
@@ -23,9 +23,7 @@
 import Data.Aeson
 import Data.Maybe (fromMaybe)
 import Data.List (find)
-import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 import Network.HTTP.Conduit (Manager)
 import Yesod.Auth
 import Yesod.Auth.OAuth2
@@ -78,25 +76,27 @@
 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
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://github.com/login/oauth/authorize" `withQuery`
+            [ scopeParam "," scopes
+            ]
         , oauthAccessTokenEndpoint = "https://github.com/login/oauth/access_token"
         , oauthCallback = Nothing
         }
 
-fetchGithubProfile :: Manager -> AccessToken -> IO (Creds m)
+fetchGithubProfile :: Manager -> OAuth2Token -> IO (Creds m)
 fetchGithubProfile manager token = do
-    userResult <- authGetJSON manager token "https://api.github.com/user"
-    mailResult <- authGetJSON manager token "https://api.github.com/user/emails"
+    userResult <- authGetJSON manager (accessToken token) "https://api.github.com/user"
+    mailResult <- authGetJSON manager (accessToken token) "https://api.github.com/user/emails"
 
     case (userResult, mailResult) of
         (Right _, Right []) -> throwIO $ InvalidProfileResponse "github" "no mail address for user"
         (Right user, Right mails) -> return $ toCreds user mails token
-        (Left err, _) -> throwIO $ InvalidProfileResponse "github" err
-        (_, Left err) -> throwIO $ InvalidProfileResponse "github" err
+        (Left err, _) -> throwIO $ invalidProfileResponse "github" err
+        (_, Left err) -> throwIO $ invalidProfileResponse "github" err
 
-toCreds :: GithubUser -> [GithubUserEmail] -> AccessToken -> Creds m
+toCreds :: GithubUser -> [GithubUserEmail] -> OAuth2Token -> Creds m
 toCreds user userMails token = Creds
     { credsPlugin = "github"
     , credsIdent = T.pack $ show $ githubUserId user
@@ -104,7 +104,7 @@
         [ ("email", githubUserEmailAddress email)
         , ("login", githubUserLogin user)
         , ("avatar_url", githubUserAvatarUrl user)
-        , ("access_token", decodeUtf8 $ accessToken token)
+        , ("access_token", atoken $ accessToken token)
         ]
         ++ maybeExtra "name" (githubUserName user)
         ++ maybeExtra "public_email" (githubUserPublicEmail user)
diff --git a/Yesod/Auth/OAuth2/Google.hs b/Yesod/Auth/OAuth2/Google.hs
--- a/Yesod/Auth/OAuth2/Google.hs
+++ b/Yesod/Auth/OAuth2/Google.hs
@@ -30,13 +30,10 @@
 import Data.Aeson
 import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 import Network.HTTP.Conduit (Manager)
 import Yesod.Auth
 import Yesod.Auth.OAuth2
 
-import qualified Data.Text as T
-
 -- | Auth with Google
 --
 -- Requests @openid@ and @email@ scopes and uses email as the @'Creds'@
@@ -67,7 +64,7 @@
 -- See @'emailUid'@ and @'googleUid'@.
 --
 oauth2GoogleScopedWithCustomId :: YesodAuth m
-                               => (GoogleUser -> AccessToken -> Creds m)
+                               => (GoogleUser -> OAuth2Token -> Creds m)
                                -- ^ A function to generate the credentials
                                -> [Text] -- ^ List of scopes to request
                                -> Text -- ^ Client ID
@@ -78,20 +75,21 @@
 
   where
     oauth = OAuth2
-        { oauthClientId = encodeUtf8 clientId
-        , oauthClientSecret = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint = encodeUtf8
-            $ "https://accounts.google.com/o/oauth2/auth?scope=" <> T.intercalate "+" scopes
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth" `withQuery`
+            [ scopeParam "+" scopes
+            ]
         , oauthAccessTokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"
         , oauthCallback = Nothing
         }
 
-fetchGoogleProfile :: (GoogleUser -> AccessToken -> Creds m) -> Manager -> AccessToken -> IO (Creds m)
+fetchGoogleProfile :: (GoogleUser -> OAuth2Token -> Creds m) -> Manager -> OAuth2Token -> IO (Creds m)
 fetchGoogleProfile toCreds manager token = do
-    userInfo <- authGetJSON manager token "https://www.googleapis.com/oauth2/v3/userinfo"
+    userInfo <- authGetJSON manager (accessToken token) "https://www.googleapis.com/oauth2/v3/userinfo"
     case userInfo of
       Right user -> return $ toCreds user token
-      Left err -> throwIO $ InvalidProfileResponse "google" err
+      Left err -> throwIO $ invalidProfileResponse "google" err
 
 data GoogleUser = GoogleUser
     { googleUserId :: Text
@@ -116,14 +114,14 @@
     parseJSON _ = mzero
 
 -- | Build a @'Creds'@ using the user's google-uid as the identifier
-googleUid :: GoogleUser -> AccessToken -> Creds m
+googleUid :: GoogleUser -> OAuth2Token -> Creds m
 googleUid = uidBuilder $ ("google-uid:" <>) . googleUserId
 
 -- | Build a @'Creds'@ using the user's email as the identifier
-emailUid :: GoogleUser -> AccessToken -> Creds m
+emailUid :: GoogleUser -> OAuth2Token -> Creds m
 emailUid = uidBuilder googleUserEmail
 
-uidBuilder :: (GoogleUser -> Text) -> GoogleUser -> AccessToken -> Creds m
+uidBuilder :: (GoogleUser -> Text) -> GoogleUser -> OAuth2Token -> Creds m
 uidBuilder f user token = Creds
     { credsPlugin = "google"
     , credsIdent = f user
@@ -133,7 +131,7 @@
         , ("given_name", googleUserGivenName user)
         , ("family_name", googleUserFamilyName user)
         , ("avatar_url", googleUserPicture user)
-        , ("access_token", decodeUtf8 $ accessToken token)
+        , ("access_token", atoken $ accessToken token)
         ]
         ++ maybeExtra "hosted_domain" (googleUserHostedDomain user)
     }
diff --git a/Yesod/Auth/OAuth2/Nylas.hs b/Yesod/Auth/OAuth2/Nylas.hs
--- a/Yesod/Auth/OAuth2/Nylas.hs
+++ b/Yesod/Auth/OAuth2/Nylas.hs
@@ -13,16 +13,14 @@
 import Control.Monad (mzero)
 import Control.Exception.Lifted (throwIO)
 import Data.Aeson (FromJSON, Value(..), parseJSON, decode, (.:))
-import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
+import Data.Text.Encoding (encodeUtf8)
 import Network.HTTP.Client (applyBasicAuth, httpLbs, parseRequest, responseBody,
                             responseStatus)
 import Network.HTTP.Conduit (Manager)
 import Yesod.Auth (Creds(..), YesodAuth, AuthPlugin)
-import Yesod.Auth.OAuth2 (OAuth2(..), AccessToken(..),
-                          YesodOAuth2Exception(InvalidProfileResponse),
-                          authOAuth2)
+import Yesod.Auth.OAuth2
+
 import qualified Network.HTTP.Types as HT
 
 data NylasAccount = NylasAccount
@@ -48,18 +46,19 @@
             -> AuthPlugin m
 oauth2Nylas clientId clientSecret = authOAuth2 "nylas" oauth fetchCreds
   where
-    authorizeUrl = encodeUtf8 $ "https://api.nylas.com/oauth/authorize" <>
-        "?response_type=code&scope=email&client_id=" <> clientId
-
     oauth = OAuth2
-        { oauthClientId = encodeUtf8 clientId
-        , oauthClientSecret = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint = authorizeUrl
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://api.nylas.com/oauth/authorize" `withQuery`
+            [ ("response_type", "code")
+            , ("scope", "email")
+            , ("client_id", encodeUtf8 clientId)
+            ]
         , oauthAccessTokenEndpoint = "https://api.nylas.com/oauth/token"
         , oauthCallback = Nothing
         }
 
-fetchCreds :: Manager -> AccessToken -> IO (Creds a)
+fetchCreds :: Manager -> OAuth2Token -> IO (Creds a)
 fetchCreds manager token = do
     req <- authorize <$> parseRequest "https://api.nylas.com/account"
     resp <- httpLbs req manager
@@ -69,11 +68,11 @@
             Nothing -> throwIO parseFailure
         else throwIO requestFailure
   where
-    authorize = applyBasicAuth (accessToken token) ""
+    authorize = applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""
     parseFailure = InvalidProfileResponse "nylas" "failed to parse account"
     requestFailure = InvalidProfileResponse "nylas" "failed to get account"
 
-toCreds :: NylasAccount -> AccessToken -> Creds a
+toCreds :: NylasAccount -> OAuth2Token -> Creds a
 toCreds ns token = Creds
     { credsPlugin = "nylas"
     , credsIdent = nylasAccountId ns
@@ -82,6 +81,6 @@
         , ("name", nylasAccountName ns)
         , ("provider", nylasAccountProvider ns)
         , ("organization_unit", nylasAccountOrganizationUnit ns)
-        , ("access_token", decodeUtf8 $ accessToken token)
+        , ("access_token", atoken $ accessToken token)
         ]
     }
diff --git a/Yesod/Auth/OAuth2/Salesforce.hs b/Yesod/Auth/OAuth2/Salesforce.hs
--- a/Yesod/Auth/OAuth2/Salesforce.hs
+++ b/Yesod/Auth/OAuth2/Salesforce.hs
@@ -24,9 +24,7 @@
 import Control.Exception.Lifted
 import Control.Monad (mzero)
 import Data.Aeson
-import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 import Network.HTTP.Conduit (Manager)
 import Yesod.Auth
 import Yesod.Auth.OAuth2
@@ -51,19 +49,21 @@
     authOAuth2 svcName oauth fetchSalesforceUser
   where
     oauth = OAuth2
-        { oauthClientId            = encodeUtf8 clientId
-        , oauthClientSecret        = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint  = encodeUtf8 $ "https://login.salesforce.com/services/oauth2/authorize?scope=" <> T.intercalate " " scopes
+        { oauthClientId            = clientId
+        , oauthClientSecret        = clientSecret
+        , oauthOAuthorizeEndpoint  = "https://login.salesforce.com/services/oauth2/authorize" `withQuery`
+            [ scopeParam " " scopes
+            ]
         , oauthAccessTokenEndpoint = "https://login.salesforce.com/services/oauth2/token"
         , oauthCallback            = Nothing
         }
 
-fetchSalesforceUser :: Manager -> AccessToken -> IO (Creds m)
+fetchSalesforceUser :: Manager -> OAuth2Token -> IO (Creds m)
 fetchSalesforceUser manager token = do
-    result <- authGetJSON manager token "https://login.salesforce.com/services/oauth2/userinfo"
+    result <- authGetJSON manager (accessToken token) "https://login.salesforce.com/services/oauth2/userinfo"
     case result of
         Right user -> return $ toCreds svcName user token
-        Left err -> throwIO $ InvalidProfileResponse svcName err
+        Left err -> throwIO $ invalidProfileResponse svcName err
 
 svcNameSb :: Text
 svcNameSb = "salesforce-sandbox"
@@ -84,19 +84,21 @@
     authOAuth2 svcNameSb oauth fetchSalesforceSandboxUser
   where
     oauth = OAuth2
-        { oauthClientId            = encodeUtf8 clientId
-        , oauthClientSecret        = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint  = encodeUtf8 $ "https://test.salesforce.com/services/oauth2/authorize?scope=" <> T.intercalate " " scopes
+        { oauthClientId            = clientId
+        , oauthClientSecret        = clientSecret
+        , oauthOAuthorizeEndpoint  = "https://test.salesforce.com/services/oauth2/authorize" `withQuery`
+            [ scopeParam " " scopes
+            ]
         , oauthAccessTokenEndpoint = "https://test.salesforce.com/services/oauth2/token"
         , oauthCallback            = Nothing
         }
 
-fetchSalesforceSandboxUser :: Manager -> AccessToken -> IO (Creds m)
+fetchSalesforceSandboxUser :: Manager -> OAuth2Token -> IO (Creds m)
 fetchSalesforceSandboxUser manager token = do
-    result <- authGetJSON manager token "https://test.salesforce.com/services/oauth2/userinfo"
+    result <- authGetJSON manager (accessToken token) $ "https://test.salesforce.com/services/oauth2/userinfo"
     case result of
         Right user -> return $ toCreds svcNameSb user token
-        Left err -> throwIO $ InvalidProfileResponse svcNameSb err
+        Left err -> throwIO $ invalidProfileResponse svcNameSb err
 
 data User = User
     { userId :: Text
@@ -130,7 +132,7 @@
 
     parseJSON _ = mzero
 
-toCreds :: Text -> User -> AccessToken -> Creds m
+toCreds :: Text -> User -> OAuth2Token -> Creds m
 toCreds name user token = Creds
     { credsPlugin = name
     , credsIdent = userId user
@@ -144,9 +146,9 @@
         , ("time_zone", userTimeZone user)
         , ("avatar_url", userPicture user)
         , ("rest_url", userRestUrl user)
-        , ("access_token", decodeUtf8 $ accessToken token)
+        , ("access_token", atoken $ accessToken token)
         ]
-        ++ maybeExtra "refresh_token" (decodeUtf8 <$> refreshToken token)
+        ++ maybeExtra "refresh_token" (rtoken <$> refreshToken token)
         ++ maybeExtra "expires_in" ((T.pack . show) <$> expiresIn token)
         ++ maybeExtra "phone_number" (userPhone user)
     }
diff --git a/Yesod/Auth/OAuth2/Slack.hs b/Yesod/Auth/OAuth2/Slack.hs
--- a/Yesod/Auth/OAuth2/Slack.hs
+++ b/Yesod/Auth/OAuth2/Slack.hs
@@ -18,12 +18,10 @@
 
 import Control.Exception.Lifted (throwIO)
 import Data.Maybe (catMaybes)
-import Data.Monoid ((<>))
 import Data.Text (Text)
-import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Data.Text.Encoding (encodeUtf8)
 import Network.HTTP.Conduit (Manager)
 
-import qualified Data.Text as Text
 import qualified Network.HTTP.Conduit as HTTP
 
 data SlackScope
@@ -86,39 +84,37 @@
     authOAuth2 "slack" oauth fetchSlackProfile
   where
     oauth = OAuth2
-        { oauthClientId = encodeUtf8 clientId
-        , oauthClientSecret = encodeUtf8 clientSecret
-        , oauthOAuthorizeEndpoint =
-            encodeUtf8
-            $ "https://slack.com/oauth/authorize?scope="
-            <> Text.intercalate "," scopeTexts
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://slack.com/oauth/authorize" `withQuery`
+            [ scopeParam "," $ "identity.basic" : map scopeText scopes
+            ]
         , oauthAccessTokenEndpoint = "https://slack.com/api/oauth.access"
         , oauthCallback = Nothing
         }
-    scopeTexts = "identity.basic":map scopeText scopes
 
 scopeText :: SlackScope -> Text
 scopeText SlackEmailScope = "identity.email"
 scopeText SlackTeamScope = "identity.team"
 scopeText SlackAvatarScope = "identity.avatar"
 
-fetchSlackProfile :: Manager -> AccessToken -> IO (Creds m)
+fetchSlackProfile :: Manager -> OAuth2Token -> IO (Creds m)
 fetchSlackProfile manager token = do
     request
-        <- HTTP.setQueryString [("token", Just $ accessToken token)]
-        <$> HTTP.parseUrl "https://slack.com/api/users.identity"
+        <- HTTP.setQueryString [("token", Just $ encodeUtf8 $ atoken $ accessToken token)]
+        <$> HTTP.parseUrlThrow "https://slack.com/api/users.identity"
     body <- HTTP.responseBody <$> HTTP.httpLbs request manager
     case eitherDecode body of
         Left _ -> throwIO $ InvalidProfileResponse "slack" body
         Right u -> return $ toCreds u token
 
-toCreds :: SlackUser -> AccessToken -> Creds m
+toCreds :: SlackUser -> OAuth2Token -> Creds m
 toCreds user token = Creds
     { credsPlugin = "slack"
     , credsIdent = slackUserId user
     , credsExtra = catMaybes
         [ Just ("name", slackUserName user)
-        , Just ("access_token", decodeUtf8 $ accessToken token)
+        , Just ("access_token", atoken $ accessToken token)
         , (,) <$> pure "email" <*> slackUserEmail user
         , (,) <$> pure "avatar" <*> slackUserAvatarUrl user
         , (,) <$> pure "team_name" <*> (slackTeamName <$> slackUserTeam 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
@@ -15,14 +15,12 @@
 
 import Control.Monad (mzero)
 import Data.Aeson
-import Data.ByteString (ByteString)
 import Data.Maybe
 import Data.Text (Text)
 import Data.Text.Encoding (encodeUtf8)
 import Yesod.Auth
 import Yesod.Auth.OAuth2
 
-import qualified Data.ByteString as B
 import qualified Data.Text as T
 
 data SpotifyUserImage = SpotifyUserImage
@@ -66,13 +64,15 @@
 oauth2Spotify :: YesodAuth m
               => Text -- ^ Client ID
               -> Text -- ^ Client Secret
-              -> [ByteString] -- ^ Scopes
+              -> [Text] -- ^ 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)
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
+        , oauthOAuthorizeEndpoint = "https://accounts.spotify.com/authorize" `withQuery`
+            [ ("scope", encodeUtf8 $ T.intercalate " " scope)
+            ]
         , oauthAccessTokenEndpoint = "https://accounts.spotify.com/api/token"
         , oauthCallback = Nothing
         }
diff --git a/Yesod/Auth/OAuth2/Upcase.hs b/Yesod/Auth/OAuth2/Upcase.hs
--- a/Yesod/Auth/OAuth2/Upcase.hs
+++ b/Yesod/Auth/OAuth2/Upcase.hs
@@ -20,7 +20,6 @@
 import Control.Monad (mzero)
 import Data.Aeson
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8)
 import Yesod.Auth
 import Yesod.Auth.OAuth2
 import qualified Data.Text as T
@@ -55,8 +54,8 @@
              -> AuthPlugin m
 oauth2Upcase clientId clientSecret = authOAuth2 "upcase"
     OAuth2
-        { oauthClientId = encodeUtf8 clientId
-        , oauthClientSecret = encodeUtf8 clientSecret
+        { oauthClientId = clientId
+        , oauthClientSecret = clientSecret
         , oauthOAuthorizeEndpoint = "http://upcase.com/oauth/authorize"
         , oauthAccessTokenEndpoint = "http://upcase.com/oauth/token"
         , oauthCallback = Nothing
diff --git a/yesod-auth-oauth2.cabal b/yesod-auth-oauth2.cabal
--- a/yesod-auth-oauth2.cabal
+++ b/yesod-auth-oauth2.cabal
@@ -1,5 +1,5 @@
 name:            yesod-auth-oauth2
-version:         0.2.4
+version:         0.3.0
 license:         BSD3
 license-file:    LICENSE
 author:          Tom Streller
@@ -31,7 +31,7 @@
                    , http-client             >= 0.4.0     && < 0.6
                    , http-conduit            >= 2.0       && < 3.0
                    , http-types              >= 0.8       && < 0.10
-                   , aeson                   >= 0.6       && < 1.1
+                   , aeson                   >= 0.6       && < 1.2
                    , yesod-core              >= 1.2       && < 1.5
                    , authenticate            >= 1.3.2.7   && < 1.4
                    , random
@@ -39,9 +39,11 @@
                    , text                    >= 0.7       && < 2.0
                    , yesod-form              >= 1.3       && < 1.5
                    , transformers            >= 0.2.2     && < 0.6
-                   , hoauth2                 >= 0.4.7     && < 0.6
+                   , hoauth2                 >= 1.3.0     && < 1.4
                    , lifted-base             >= 0.2       && < 0.4
-                   , vector                  >= 0.10      && < 0.12
+                   , vector                  >= 0.10      && < 0.13
+                   , uri-bytestring
+                   , microlens
 
     exposed-modules: Yesod.Auth.OAuth2
                      Yesod.Auth.OAuth2.Github
@@ -54,6 +56,8 @@
                      Yesod.Auth.OAuth2.Salesforce
                      Yesod.Auth.OAuth2.Bitbucket
                      Yesod.Auth.OAuth2.BattleNet
+                     URI.ByteString.Extension
+                     -- ^ exposed for testing
 
     ghc-options:     -Wall
 
@@ -84,6 +88,7 @@
   build-depends:   base
                  , yesod-auth-oauth2
                  , hspec
+                 , uri-bytestring
 
 source-repository head
   type:     git
