Support hoauth2-2.0

The new major version improves the naming of the fields of the OAuth2
record type. This type is central to this library and we leak it freely.

Users who make their own plugins are expected to construct values of
this type to pass into our functions, this makes the new version
disruptive to our code and our users'.

We have two options:

1. Update and release our own new major version

   The major downside is that the current LTS resolver will then not
   update beyond our currently-released version. We have no immediate
   plans for new features in this library, but if we have bugs reported
   to be fixed we would either have to manage a complex backporting or
   ask our Stack users to wait for the next major LTS, which has
   historically been many months.

   Users who wish to use our new version would need to also bring in
   hoauth2, and who knows what else.

2. Release a fully-compatible update

   As mentioned, we leak OAuth2(..) through this library's interface. In
   order to be truly backwards-compatible, we would have to use CCP to
   define an "old style" OAuth2 and use that throughout, such that
   in-the-wild OAuth2 values continue to work as-is.

   This would not be a good long-term solution as it introduces a fair
   amount of naming confusion and will lead to import conflicts for any
   users who also import hoauth2-2.0 modules in the same project.

3. Release a mostly-compatible update

   This is the path this commit explores. We can update our own code to
   be hoauth2-2.0 compatible and use CPP to define the hoauth2-2.0-like
   OAuth2 if we're still on hoauth2-1.x.

   This gets us compiling in either case and "forward functional", with
   the exception of users who define their own plugins (which is rare).

   Because of that use-case, this should technically be a major version
   bump for ourselves (though I'm open to the argument we could treat
   the local-provider use-case differently), however it is still better
   than Option 1 in a few ways:

   - We still compile with hoauth2-1.x, so can be brought in easily as
     an isolated extra-dep
   - If there is a reported bug that we decide to only fix in the newer
     versions, the path for the user is better: they can pull us as an
     extra-dep and likely need no changes. Even if they're doing a
     custom plugin, the required changes are minor
This commit is contained in:
patrick brisbin 2022-01-31 10:53:15 -05:00
parent b7063dc230
commit 8e434df38a
25 changed files with 175 additions and 92 deletions

View File

@ -13,6 +13,7 @@ jobs:
matrix:
stack-yaml:
- stack.yaml
- stack-hoauth2-2.0.yaml
- stack-lts-17.4.yaml
- stack-lts-16.10.yaml
- stack-lts-13.2.yaml

View File

@ -91,11 +91,11 @@ oauth2MySite clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint = "https://mysite.com/oauth/authorize"
, oauthAccessTokenEndpoint = "https://mysite.com/oauth/token"
, oauthCallback = Nothing
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint = "https://mysite.com/oauth/authorize"
, oauth2TokenEndpoint = "https://mysite.com/oauth/token"
, oauth2RedirectUri = Nothing
}
```

View File

@ -0,0 +1,61 @@
{-# LANGUAGE CPP #-}
module Network.OAuth.OAuth2.Compat
( OAuth2(..)
, authorizationUrl
, fetchAccessToken
, fetchAccessToken2
, module Network.OAuth.OAuth2
) where
import Network.HTTP.Conduit (Manager)
import Network.OAuth.OAuth2 hiding
(OAuth2(..), authorizationUrl, fetchAccessToken, fetchAccessToken2)
import qualified Network.OAuth.OAuth2 as OAuth2
import Network.OAuth.OAuth2.TokenRequest (Errors)
import URI.ByteString
#if MIN_VERSION_hoauth2(2,0,0)
import Network.OAuth.OAuth2 (OAuth2(..))
getOAuth2 :: OAuth2 -> OAuth2
getOAuth2 = id
#else
import Data.Text (Text)
data OAuth2 = OAuth2
{ oauth2ClientId :: Text
, oauth2ClientSecret :: Maybe Text
, oauth2AuthorizeEndpoint :: URIRef Absolute
, oauth2TokenEndpoint :: URIRef Absolute
, oauth2RedirectUri :: Maybe (URIRef Absolute)
}
getOAuth2 :: OAuth2 -> OAuth2.OAuth2
getOAuth2 o = OAuth2.OAuth2
{ OAuth2.oauthClientId = oauth2ClientId o
, OAuth2.oauthClientSecret = oauth2ClientSecret o
, OAuth2.oauthOAuthorizeEndpoint = oauth2AuthorizeEndpoint o
, OAuth2.oauthAccessTokenEndpoint = oauth2TokenEndpoint o
, OAuth2.oauthCallback = oauth2RedirectUri o
}
#endif
authorizationUrl :: OAuth2 -> URI
authorizationUrl = OAuth2.authorizationUrl . getOAuth2
fetchAccessToken
:: Manager
-> OAuth2
-> ExchangeToken
-> IO (OAuth2Result Errors OAuth2Token)
fetchAccessToken m = OAuth2.fetchAccessToken m . getOAuth2
fetchAccessToken2
:: Manager
-> OAuth2
-> ExchangeToken
-> IO (OAuth2Result Errors OAuth2Token)
fetchAccessToken2 m = OAuth2.fetchAccessToken2 m . getOAuth2

View File

@ -26,8 +26,7 @@ module Yesod.Auth.OAuth2
, getRefreshToken
, getUserResponse
, getUserResponseJSON
)
where
) where
import Control.Error.Util (note)
import Control.Monad ((<=<))
@ -36,7 +35,7 @@ import Data.ByteString.Lazy (ByteString, fromStrict)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Conduit (Manager)
import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.Compat
import Yesod.Auth
import Yesod.Auth.OAuth2.Dispatch
import Yesod.Core.Widget

View File

@ -45,14 +45,14 @@ oauth2AzureADScoped scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://login.windows.net/common/oauth2/authorize"
`withQuery` [ scopeParam "," scopes
, ("resource", "https://graph.microsoft.com")
]
, oauthAccessTokenEndpoint =
, oauth2TokenEndpoint =
"https://login.windows.net/common/oauth2/token"
, oauthCallback = Nothing
, oauth2RedirectUri = Nothing
}

View File

@ -51,11 +51,11 @@ oauth2BattleNet widget region clientId clientSecret =
where
host = wwwHost $ T.toLower region
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"
, oauthAccessTokenEndpoint = fromRelative "https" host "/oauth/token"
, oauthCallback = Nothing
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"
, oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -52,12 +52,12 @@ oauth2BitbucketScoped scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://bitbucket.com/site/oauth2/authorize"
`withQuery` [scopeParam "," scopes]
, oauthAccessTokenEndpoint =
, oauth2TokenEndpoint =
"https://bitbucket.com/site/oauth2/access_token"
, oauthCallback = Nothing
, oauth2RedirectUri = Nothing
}

View File

@ -40,12 +40,12 @@ oauth2ClassLinkScoped scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://launchpad.classlink.com/oauth2/v2/auth"
`withQuery` [scopeParam "," scopes]
, oauthAccessTokenEndpoint =
, oauth2TokenEndpoint =
"https://launchpad.classlink.com/oauth2/v2/token"
, oauthCallback = Nothing
, oauth2RedirectUri = Nothing
}

View File

@ -17,7 +17,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Conduit (Manager)
import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.Compat
import Network.OAuth.OAuth2.TokenRequest (Errors)
import URI.ByteString.Extension
import UnliftIO.Exception
@ -104,9 +104,9 @@ withCallbackAndState name oauth2 csrf = do
uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender
callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri
pure oauth2
{ oauthCallback = Just callback
, oauthOAuthorizeEndpoint =
oauthOAuthorizeEndpoint oauth2
{ oauth2RedirectUri = Just callback
, oauth2AuthorizeEndpoint =
oauth2AuthorizeEndpoint oauth2
`withQuery` [("state", encodeUtf8 csrf)]
}

View File

@ -72,11 +72,11 @@ oauth2EveScoped scopes widgetType clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://login.eveonline.com/oauth/authorize"
`withQuery` [("response_type", "code"), scopeParam " " scopes]
, oauthAccessTokenEndpoint = "https://login.eveonline.com/oauth/token"
, oauthCallback = Nothing
, oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -46,12 +46,12 @@ oauth2GitHubScoped scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://github.com/login/oauth/authorize"
`withQuery` [scopeParam "," scopes]
, oauthAccessTokenEndpoint =
, oauth2TokenEndpoint =
"https://github.com/login/oauth/access_token"
, oauthCallback = Nothing
, oauth2RedirectUri = Nothing
}

View File

@ -53,12 +53,12 @@ oauth2GitLabHostScopes host scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
host
`withPath` "/oauth/authorize"
`withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint = host `withPath` "/oauth/token"
, oauthCallback = Nothing
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -74,12 +74,12 @@ oauth2GoogleScopedWidget widget scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://accounts.google.com/o/oauth2/auth"
`withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint =
, oauth2TokenEndpoint =
"https://www.googleapis.com/oauth2/v3/token"
, oauthCallback = Nothing
, oauth2RedirectUri = Nothing
}

View File

@ -51,9 +51,9 @@ oauth2Nylas clientId clientSecret =
$ eitherDecode userResponse
where
oauth = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://api.nylas.com/oauth/authorize"
`withQuery` [ ("response_type", "code")
, ( "client_id"
@ -64,6 +64,6 @@ oauth2Nylas clientId clientSecret =
-- its current state, it doesn't matter because it's only one scope.
, scopeParam "," defaultScopes
]
, oauthAccessTokenEndpoint = "https://api.nylas.com/oauth/token"
, oauthCallback = Nothing
, oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -54,8 +54,7 @@ module Yesod.Auth.OAuth2.Prelude
-- * Temporary, until I finish re-structuring modules
, authOAuth2
, authOAuth2Widget
)
where
) where
import Control.Exception.Safe
import Data.Aeson
@ -65,7 +64,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding
import Network.HTTP.Conduit
import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.Compat
import URI.ByteString
import URI.ByteString.Extension
import Yesod.Auth

View File

@ -73,10 +73,10 @@ salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSec
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
authorizeUri `withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint = tokenUri
, oauthCallback = Nothing
, oauth2TokenEndpoint = tokenUri
, oauth2RedirectUri = Nothing
}

View File

@ -65,11 +65,11 @@ oauth2SlackScoped scopes clientId clientSecret =
$ eitherDecode userResponse
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://slack.com/oauth/authorize"
`withQuery` [scopeParam "," $ map scopeText scopes]
, oauthAccessTokenEndpoint = "https://slack.com/api/oauth.access"
, oauthCallback = Nothing
, oauth2TokenEndpoint = "https://slack.com/api/oauth.access"
, oauth2RedirectUri = Nothing
}

View File

@ -34,11 +34,11 @@ oauth2Spotify scopes clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://accounts.spotify.com/authorize"
`withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint = "https://accounts.spotify.com/api/token"
, oauthCallback = Nothing
, oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"
, oauth2RedirectUri = Nothing
}

View File

@ -41,9 +41,9 @@ oauth2Upcase clientId clientSecret =
}
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint = "http://upcase.com/oauth/authorize"
, oauthAccessTokenEndpoint = "http://upcase.com/oauth/token"
, oauthCallback = Nothing
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"
, oauth2TokenEndpoint = "http://upcase.com/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -37,12 +37,12 @@ oauth2WordPressDotCom clientId clientSecret =
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId
, oauth2ClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint =
"https://public-api.wordpress.com/oauth2/authorize"
`withQuery` [scopeParam "," ["auth"]]
, oauthAccessTokenEndpoint =
, oauth2TokenEndpoint =
"https://public-api.wordpress.com/oauth2/token"
, oauthCallback = Nothing
, oauth2RedirectUri = Nothing
}

3
stack-hoauth2-2.0.yaml Normal file
View File

@ -0,0 +1,3 @@
resolver: lts-18.23
extra-deps:
- hoauth2-2.0.0@sha256:4686d776272d4c57d3c8dbeb9e58b04afe4d2b410382011bd78a3d2bfb08a3fe,5662

View File

@ -0,0 +1,19 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files
packages:
- completed:
hackage: hoauth2-2.0.0@sha256:4686d776272d4c57d3c8dbeb9e58b04afe4d2b410382011bd78a3d2bfb08a3fe,5662
pantry-tree:
size: 2171
sha256: 291b3dd90854ef44f270519ec17e34b6778f8430f6d6517bd67b0128bd549553
original:
hackage: hoauth2-2.0.0@sha256:4686d776272d4c57d3c8dbeb9e58b04afe4d2b410382011bd78a3d2bfb08a3fe,5662
snapshots:
- completed:
size: 587819
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/23.yaml
sha256: 7f69bb29a57495586e7e3ed31ecc59c0d2c959cb23bd52b71ca676f254c9beb1
original: lts-18.23

View File

@ -1 +1 @@
resolver: lts-17.4
resolver: lts-18.23

View File

@ -6,7 +6,7 @@
packages: []
snapshots:
- completed:
size: 563103
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/4.yaml
sha256: f11e2153044f5f71ea7b1c9398f4721f517c9bd37642ed769647b896564021f3
original: lts-17.4
size: 587819
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/23.yaml
sha256: 7f69bb29a57495586e7e3ed31ecc59c0d2c959cb23bd52b71ca676f254c9beb1
original: lts-18.23

View File

@ -1,10 +1,10 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.33.0.
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
--
-- hash: 1c0ae94778ce803bde4726d2be1e60cd5dcb94f8e34b7cefbbae7dafb30d1dea
-- hash: 5f7443ec90d4a2884d1d8816ec553f50fe303c51a6d8ee956fc8d62b49333e60
name: yesod-auth-oauth2
version: 0.6.3.4
@ -35,6 +35,7 @@ flag example
library
exposed-modules:
Network.OAuth.OAuth2.Compat
UnliftIO.Except
URI.ByteString.Extension
Yesod.Auth.OAuth2