Compare commits

..

No commits in common. "main" and "v0.6.3.2" have entirely different histories.

58 changed files with 1031 additions and 1560 deletions

View File

@ -1,4 +1,3 @@
# shellcheck disable=SC2034
# #
# Copy this file to .env and update the credentials for the providers you are # Copy this file to .env and update the credentials for the providers you are
# trying to test. These variables must all have non-empty values for the # trying to test. These variables must all have non-empty values for the
@ -6,18 +5,9 @@
# you plan to try. # you plan to try.
# #
### ###
AUTH0_HOST=x
AUTH0_CLIENT_ID=x
AUTH0_CLIENT_SECRET=x
AZURE_AD_CLIENT_ID=x AZURE_AD_CLIENT_ID=x
AZURE_AD_CLIENT_SECRET=x AZURE_AD_CLIENT_SECRET=x
AZURE_ADV2_TENANT_ID=x
AZURE_ADV2_CLIENT_ID=x
AZURE_ADV2_CLIENT_SECRET=x
BATTLE_NET_CLIENT_ID=x BATTLE_NET_CLIENT_ID=x
BATTLE_NET_CLIENT_SECRET=x BATTLE_NET_CLIENT_SECRET=x
@ -51,9 +41,6 @@ SLACK_CLIENT_SECRET=x
SPOTIFY_CLIENT_ID=x SPOTIFY_CLIENT_ID=x
SPOTIFY_CLIENT_SECRET=x SPOTIFY_CLIENT_SECRET=x
TWITCH_CLIENT_ID=x
TWITCH_CLIENT_SECRET=x
UPCASE_CLIENT_ID=x UPCASE_CLIENT_ID=x
UPCASE_CLIENT_SECRET=x UPCASE_CLIENT_SECRET=x

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @freckle/backenders

View File

@ -1,16 +0,0 @@
name: Asana
on:
pull_request:
types: [opened]
jobs:
link-asana-task:
if: ${{ github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: Asana/create-app-attachment-github-action@v1.3
id: postAttachment
with:
asana-secret: ${{ secrets.ASANA_API_ACCESS_KEY }}
- run: echo "Status is ${{ steps.postAttachment.outputs.status }}"

View File

@ -5,45 +5,28 @@ on:
push: push:
branches: main branches: main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs: jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- id: generate
uses: freckle/stack-action/generate-matrix@v5
outputs:
stack-yamls: ${{ steps.generate.outputs.stack-yamls }}
test: test:
needs: generate runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
stack-yaml: ${{ fromJSON(needs.generate.outputs.stack-yamls) }} stack-yaml:
- stack.yaml
- stack-lts-17.4.yaml
- stack-lts-16.10.yaml
- stack-lts-13.2.yaml
fail-fast: false fail-fast: false
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2
- uses: freckle/stack-action@v5 - uses: freckle/stack-cache-action@v1.0.1
with: with:
stack-build-arguments: --flag yesod-auth-oauth2:example stack-yaml: ${{ matrix.stack-yaml }}
env: - run:
STACK_YAML: ${{ matrix.stack-yaml }} curl --output .hlint.yaml https://raw.githubusercontent.com/pbrisbin/dotfiles/master/hlint.yaml
- uses: freckle/stack-action@main
lint: with:
runs-on: ubuntu-latest stack-yaml: ${{ matrix.stack-yaml }}
steps: stack-arguments: --flag yesod-auth-oauth2:example
- uses: actions/checkout@v5 weeder: false
- uses: haskell-actions/hlint-setup@v2
- uses: haskell-actions/hlint-run@v2
with:
fail-on: warning

View File

@ -8,15 +8,16 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2
- id: tag - id: tag
uses: freckle/haskell-tag-action@v1 uses: freckle/haskell-tag-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: steps.tag.outputs.tag - if: steps.tag.outputs.tag
run: stack upload --pvp-bounds lower . uses: freckle/stack-upload-action@main
with:
pvp-bounds: both
env: env:
HACKAGE_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} HACKAGE_API_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }}
# Use minimum LTS to set lowest lower bounds
STACK_YAML: stack-lts21.yaml

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
.stack-work .stack-work
TAGS
# OAuth keys configuration for the example # OAuth keys configuration for the example
.env* .env*

View File

@ -1,4 +0,0 @@
restylers:
- fourmolu
- "!stylish-haskell"
- "*"

View File

@ -1,2 +0,0 @@
[versions]
oldest = lts-21

21
.stylish-haskell.yaml Normal file
View File

@ -0,0 +1,21 @@
steps:
- simple_align:
cases: false
top_level_patterns: false
records: false
- imports:
align: none
list_align: after_alias
pad_module_names: false
long_list_align: new_line_multiline
empty_list_align: right_after
list_padding: 4
separate_lists: false
space_surround: false
- language_pragmas:
style: vertical
align: false
remove_redundant: true
- trailing_whitespace: {}
columns: 80
newline: native

View File

@ -1,75 +1,8 @@
## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.8.0.0...main) ## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.3.2...main)
## [v0.8.0.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.4.0...v0.8.0.0) None
- Drop support for GHC < 9.4 and hoauth2 < 2.8 ## [v0.6.3.2](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.3.1...v0.6.3.2)
- Add support for GHC 9.12 and hoauth2-2.15
- To align our interfaces with hoauth2-2.15:
- Make `OAuth2 {clientSecret}` non-`Maybe`
- Replace `OAuthToken` with `TokenResponse`
- Replace `Errors` with `TokenResponseError`
- Replace `fetchAccessToken{,2}` with `fetchAccessToken{Basic,Post}`
While technically a major version bump, this change should only affect those
users that maintain their own plugins.
## [v0.7.4.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.3.0...v0.7.4.0)
- Add `oauth2AzureADv2Widget` and `oauth2AzureADv2ScopedWidget`
## [v0.7.3.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.2.0...v0.7.3.0)
- Add ORCID provider
- Drop support for LTS-12 / GHC-8.6
- Replace `cryptonite` with `crypton`
## [v0.7.2.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.3...v0.7.2.0)
- Add `oauth2GitHubWidget` and `oauth2GitHubScopedWidget`
[@jaanisfehling](https://github.com/freckle/yesod-auth-oauth2/pull/181)
## [v0.7.1.3](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.2...v0.7.1.3)
- Add support (with caveats) for relative approots
[@cptrodolfox](https://github.com/freckle/yesod-auth-oauth2/pull/178)
## [v0.7.1.2](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.1...v0.7.1.2)
- Support `hoauth2-2.9`.
## [v0.7.1.1](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.0...v0.7.1.1)
- Support `mtl-2.3`, which no longer re-exports `Control.Monad`
## [v0.7.1.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.3...v0.7.1.0)
- Add `AzureADv2` provider
## [v0.7.0.3](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.2...v0.7.0.3)
- Support `hoauth-2.7`. This change is only breaking in the unlikely case of users
using something other than `fetchAccessToken` or `fetchAccessToken2`
## [v0.7.0.2](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.1...v0.7.0.2)
- Add Auth0 provider ([@hw202207](https://github.com/freckle/yesod-auth-oauth2/pull/162))
## [v0.7.0.1](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.0...v0.7.0.1)
- Support `hoauth-2.2` and `2.3`
## [v0.7.0.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.3.4...v0.7.0.0)
- Support `hoauth2-2.0`
The `OAuth2` type's fields have changed. If you are not defining your own
Local Providers (i.e. you're not constructing any `OAuth2` values) you should
not be affected by this change. If you are, you should update to the [new
field names][oauth2].
[oauth2]: https://hackage.haskell.org/package/hoauth2-2.0.0/docs/Network-OAuth-OAuth2-Internal.html#t:OAuth2
## [v0.6.3.4](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.3.1...v0.6.3.4)
- Remove dependencies upper bounds - Remove dependencies upper bounds

View File

@ -1,10 +1,5 @@
# Yesod.Auth.OAuth2 # Yesod.Auth.OAuth2
[![Hackage](https://img.shields.io/hackage/v/yesod-auth-oauth2.svg?style=flat)](https://hackage.haskell.org/package/yesod-auth-oauth2)
[![Stackage Nightly](http://stackage.org/package/yesod-auth-oauth2/badge/nightly)](http://stackage.org/nightly/package/yesod-auth-oauth2)
[![Stackage LTS](http://stackage.org/package/yesod-auth-oauth2/badge/lts)](http://stackage.org/lts/package/yesod-auth-oauth2)
[![CI](https://github.com/freckle/yesod-auth-oauth2/actions/workflows/ci.yml/badge.svg)](https://github.com/pbrisbin/freckle/yesod-auth-oauth2/workflows/ci.yml)
OAuth2 `AuthPlugin`s for Yesod. OAuth2 `AuthPlugin`s for Yesod.
## Usage ## Usage
@ -96,11 +91,11 @@ oauth2MySite clientId clientSecret =
} }
where where
oauth2 = OAuth2 oauth2 = OAuth2
{ oauth2ClientId = clientId { oauthClientId = clientId
, oauth2ClientSecret = Just clientSecret , oauthClientSecret = Just clientSecret
, oauth2AuthorizeEndpoint = "https://mysite.com/oauth/authorize" , oauthOAuthorizeEndpoint = "https://mysite.com/oauth/authorize"
, oauth2TokenEndpoint = "https://mysite.com/oauth/token" , oauthAccessTokenEndpoint = "https://mysite.com/oauth/token"
, oauth2RedirectUri = Nothing , oauthCallback = Nothing
} }
``` ```
@ -117,26 +112,6 @@ stack build --pedantic --test
Please also run HLint and Weeder before submitting PRs. Please also run HLint and Weeder before submitting PRs.
## Example
This project includes an executable that runs a server with (almost) all
supported providers present.
To use:
1. `cp .env.example .env` and edit in secrets for providers you wish to test
Be sure to include `http://localhost:3000/auth/page/{plugin}/callback` as a
valid Redirect URI when configuring the OAuth application.
2. Build with the example: `stack build ... --flag yesod-auth-oauth2:example`
3. Run the example `stack exec yesod-auth-oauth2-example`
4. Visit the example: `$BROWSER http://localhost:3000`
5. Click the log-in link for the provider you configured
If successful, you will be presented with a page that shows the credential and
User response value.
--- ---
[CHANGELOG](./CHANGELOG.md) | [LICENSE](./LICENSE) [CHANGELOG](./CHANGELOG.md) | [LICENSE](./LICENSE)

View File

@ -6,6 +6,19 @@
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
-- |
--
-- This single-file Yesod app uses all plugins defined within this site, as a
-- means of manual verification that they work. When adding a new plugin, add
-- usage of it here and verify locally that it works.
--
-- To do so, see @.env.example@, then:
--
-- > stack build --flag yesod-auth-oauth2:example
-- > stack exec yesod-auth-oauth2-example
-- >
-- > $BROWSER http://localhost:3000
--
module Main where module Main where
import Data.Aeson import Data.Aeson
@ -13,8 +26,7 @@ import Data.Aeson.Encode.Pretty
import Data.ByteString.Lazy (fromStrict, toStrict) import Data.ByteString.Lazy (fromStrict, toStrict)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.String (IsString (fromString)) import Data.Text (Text)
import Data.Text (Text, pack)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
import LoadEnv import LoadEnv
@ -23,9 +35,7 @@ import Network.Wai.Handler.Warp (runEnv)
import System.Environment (getEnv) import System.Environment (getEnv)
import Yesod import Yesod
import Yesod.Auth import Yesod.Auth
import Yesod.Auth.OAuth2.Auth0
import Yesod.Auth.OAuth2.AzureAD import Yesod.Auth.OAuth2.AzureAD
import Yesod.Auth.OAuth2.AzureADv2
import Yesod.Auth.OAuth2.BattleNet import Yesod.Auth.OAuth2.BattleNet
import Yesod.Auth.OAuth2.Bitbucket import Yesod.Auth.OAuth2.Bitbucket
import Yesod.Auth.OAuth2.ClassLink import Yesod.Auth.OAuth2.ClassLink
@ -34,73 +44,68 @@ import Yesod.Auth.OAuth2.GitHub
import Yesod.Auth.OAuth2.GitLab import Yesod.Auth.OAuth2.GitLab
import Yesod.Auth.OAuth2.Google import Yesod.Auth.OAuth2.Google
import Yesod.Auth.OAuth2.Nylas import Yesod.Auth.OAuth2.Nylas
import Yesod.Auth.OAuth2.ORCID
import Yesod.Auth.OAuth2.Salesforce import Yesod.Auth.OAuth2.Salesforce
import Yesod.Auth.OAuth2.Slack import Yesod.Auth.OAuth2.Slack
import Yesod.Auth.OAuth2.Spotify import Yesod.Auth.OAuth2.Spotify
import Yesod.Auth.OAuth2.Twitch
import Yesod.Auth.OAuth2.Upcase import Yesod.Auth.OAuth2.Upcase
import Yesod.Auth.OAuth2.WordPressDotCom import Yesod.Auth.OAuth2.WordPressDotCom
data App = App data App = App
{ appHttpManager :: Manager { appHttpManager :: Manager
, appAuthPlugins :: [AuthPlugin App] , appAuthPlugins :: [AuthPlugin App]
} }
mkYesod mkYesod "App" [parseRoutes|
"App"
[parseRoutes|
/ RootR GET / RootR GET
/auth AuthR Auth getAuth /auth AuthR Auth getAuth
|] |]
instance Yesod App where instance Yesod App where
-- see https://github.com/thoughtbot/yesod-auth-oauth2/issues/87 -- see https://github.com/thoughtbot/yesod-auth-oauth2/issues/87
approot = ApprootStatic "http://localhost:3000" approot = ApprootStatic "http://localhost:3000"
instance YesodAuth App where instance YesodAuth App where
type AuthId App = Text type AuthId App = Text
loginDest _ = RootR loginDest _ = RootR
logoutDest _ = RootR logoutDest _ = RootR
-- Disable any attempt to read persisted authenticated state -- Disable any attempt to read persisted authenticated state
maybeAuthId = return Nothing maybeAuthId = return Nothing
-- Copy the Creds response into the session for viewing after -- Copy the Creds response into the session for viewing after
authenticate c = do authenticate c = do
mapM_ (uncurry setSession) $ mapM_ (uncurry setSession)
[("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)] $ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]
++ credsExtra c ++ credsExtra c
return $ Authenticated "1" return $ Authenticated "1"
authPlugins = appAuthPlugins authPlugins = appAuthPlugins
instance RenderMessage App FormMessage where instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage renderMessage _ _ = defaultFormMessage
-- brittany-disable-next-binding -- brittany-disable-next-binding
getRootR :: Handler Html getRootR :: Handler Html
getRootR = do getRootR = do
sess <- getSession sess <- getSession
let let
prettify = prettify
decodeUtf8 = decodeUtf8
. toStrict . toStrict
. encodePretty . encodePretty
. fromJust . fromJust
. decode @Value . decode @Value
. fromStrict . fromStrict
mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess
mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess
mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess
mUserResponse = prettify <$> M.lookup "userResponse" sess mUserResponse = prettify <$> M.lookup "userResponse" sess
defaultLayout defaultLayout [whamlet|
[whamlet|
<h1>Yesod Auth OAuth2 Example <h1>Yesod Auth OAuth2 Example
<h2> <h2>
<a href=@{AuthR LoginR}>Log in <a href=@{AuthR LoginR}>Log in
@ -121,45 +126,37 @@ getRootR = do
mkFoundation :: IO App mkFoundation :: IO App
mkFoundation = do mkFoundation = do
loadEnv loadEnv
auth0Host <- getEnv "AUTH0_HOST" appHttpManager <- newManager tlsManagerSettings
azureTenant <- getEnv "AZURE_ADV2_TENANT_ID" appAuthPlugins <- sequence
-- When Providers are added, add them here and update .env.example.
-- Nothing else should need changing.
--
-- FIXME: oauth2BattleNet is quite annoying!
--
[ loadPlugin oauth2AzureAD "AZURE_AD"
, loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"
, loadPlugin oauth2Bitbucket "BITBUCKET"
, loadPlugin oauth2ClassLink "CLASSLINK"
, loadPlugin (oauth2Eve Plain) "EVE_ONLINE"
, loadPlugin oauth2GitHub "GITHUB"
, loadPlugin oauth2GitLab "GITLAB"
, loadPlugin oauth2Google "GOOGLE"
, loadPlugin oauth2Nylas "NYLAS"
, loadPlugin oauth2Salesforce "SALES_FORCE"
, loadPlugin oauth2Slack "SLACK"
, loadPlugin (oauth2Spotify []) "SPOTIFY"
, loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"
, loadPlugin oauth2Upcase "UPCASE"
]
appHttpManager <- newManager tlsManagerSettings return App { .. }
appAuthPlugins <- where
sequence loadPlugin f prefix = do
-- When Providers are added, add them here and update .env.example. clientId <- getEnv $ prefix <> "_CLIENT_ID"
-- Nothing else should need changing. clientSecret <- getEnv $ prefix <> "_CLIENT_SECRET"
-- pure $ f (T.pack clientId) (T.pack clientSecret)
-- FIXME: oauth2BattleNet is quite annoying!
--
[ loadPlugin oauth2AzureAD "AZURE_AD"
, loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2"
, loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"
, loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"
, loadPlugin oauth2Bitbucket "BITBUCKET"
, loadPlugin oauth2ClassLink "CLASSLINK"
, loadPlugin (oauth2Eve Plain) "EVE_ONLINE"
, loadPlugin oauth2GitHub "GITHUB"
, loadPlugin oauth2GitLab "GITLAB"
, loadPlugin oauth2Google "GOOGLE"
, loadPlugin oauth2Nylas "NYLAS"
, loadPlugin oauth2Salesforce "SALES_FORCE"
, loadPlugin oauth2Slack "SLACK"
, loadPlugin (oauth2Spotify []) "SPOTIFY"
, loadPlugin oauth2Twitch "TWITCH"
, loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"
, loadPlugin oauth2ORCID "ORCID"
, loadPlugin oauth2Upcase "UPCASE"
]
return App {..}
where
loadPlugin f prefix = do
clientId <- getEnv $ prefix <> "_CLIENT_ID"
clientSecret <- getEnv $ prefix <> "_CLIENT_SECRET"
pure $ f (T.pack clientId) (T.pack clientSecret)
main :: IO () main :: IO ()
main = runEnv 3000 =<< toWaiApp =<< mkFoundation main = runEnv 3000 =<< toWaiApp =<< mkFoundation

View File

@ -1,15 +0,0 @@
indentation: 2
column-limit: 80 # ignored until v12 / ghc-9.6
function-arrows: leading
comma-style: leading # default
import-export-style: leading
indent-wheres: false # default
record-brace-space: true
newlines-between-decls: 1 # default
haddock-style: single-line
let-style: mixed
in-style: left-align
single-constraint-parens: never # ignored until v12 / ghc-9.6
unicode: never # default
respectful: true # default
fixities: [] # default

View File

@ -1,6 +1,6 @@
--- ---
name: yesod-auth-oauth2 name: yesod-auth-oauth2
version: 0.8.0.0 version: 0.6.3.2 # N.B. PVP-compliant Semver: 0.MAJOR.MINOR.PATCH
synopsis: OAuth 2.0 authentication plugins synopsis: OAuth 2.0 authentication plugins
description: Library to authenticate with OAuth 2.0 for Yesod web applications. description: Library to authenticate with OAuth 2.0 for Yesod web applications.
category: Web category: Web
@ -13,7 +13,7 @@ maintainer: engineering@freckle.com
github: freckle/yesod-auth-oauth2 github: freckle/yesod-auth-oauth2
homepage: http://github.com/freckle/yesod-auth-oauth2 homepage: http://github.com/freckle/yesod-auth-oauth2
extra-doc-files: extra-source-files:
- README.md - README.md
- CHANGELOG.md - CHANGELOG.md
@ -27,9 +27,9 @@ library:
dependencies: dependencies:
- aeson >=0.6 - aeson >=0.6
- bytestring >=0.9.1.4 - bytestring >=0.9.1.4
- crypton - cryptonite >=0.25
- errors - errors
- hoauth2 >=2.8.0 # TokenRequestError - hoauth2 >=1.11.0
- http-client >=0.4.0 - http-client >=0.4.0
- http-conduit >=2.0 - http-conduit >=2.0
- http-types >=0.8 - http-types >=0.8
@ -38,7 +38,6 @@ library:
- mtl - mtl
- safe-exceptions - safe-exceptions
- text >=0.7 - text >=0.7
- transformers
- uri-bytestring - uri-bytestring
- yesod-auth >=1.6.0 - yesod-auth >=1.6.0
- yesod-core >=1.6.0 - yesod-core >=1.6.0
@ -57,7 +56,7 @@ executables:
- aeson >=0.6 - aeson >=0.6
- aeson-pretty - aeson-pretty
- bytestring >=0.9.1.4 - bytestring >=0.9.1.4
- containers >=0.6.0.1 - containers
- http-conduit >=2.0 - http-conduit >=2.0
- load-env - load-env
- text >=0.7 - text >=0.7

View File

@ -1,7 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>freckle/renovate-config"
],
"minimumReleaseAge": "0 days"
}

View File

@ -1,118 +0,0 @@
{-# LANGUAGE CPP #-}
module Network.OAuth.OAuth2.Compat
( OAuth2 (..)
, authorizationUrl
, fetchAccessTokenBasic
, fetchAccessTokenPost
, authGetBS
-- * Re-exports
, AccessToken (..)
, ExchangeToken (..)
, RefreshToken (..)
, TokenResponse
, accessToken
, refreshToken
, expiresIn
, tokenType
, idToken
, TokenResponseError
) where
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Trans.Except (runExceptT)
import Data.ByteString.Lazy (ByteString)
import Data.Text (Text)
import Network.HTTP.Conduit (Manager)
import URI.ByteString
#if MIN_VERSION_hoauth2(2,15,0)
import Network.OAuth2
( AccessToken (..)
, ExchangeToken (..)
, RefreshToken (..)
, TokenResponse (..)
, TokenResponseError
)
import qualified Network.OAuth2 as OAuth2
#elif MIN_VERSION_hoauth2(2,9,0)
import Network.OAuth.OAuth2
( AccessToken (..)
, ExchangeToken (..)
, RefreshToken (..)
, OAuth2Token (..)
, TokenResponseError
)
import qualified Network.OAuth.OAuth2 as OAuth2
type TokenResponse = OAuth2Token
#else
-- hoauth2-2.8
import Network.OAuth.OAuth2
( AccessToken (..)
, ExchangeToken (..)
, RefreshToken (..)
, OAuth2Token (..)
)
import Network.OAuth.OAuth2.TokenRequest (TokenRequestError)
import qualified Network.OAuth.OAuth2 as OAuth2
type TokenResponse = OAuth2Token
type TokenResponseError = TokenRequestError
#endif
data OAuth2 = OAuth2
{ oauth2ClientId :: Text
, oauth2ClientSecret :: Text
, oauth2AuthorizeEndpoint :: URIRef Absolute
, oauth2TokenEndpoint :: URIRef Absolute
, oauth2RedirectUri :: Maybe (URIRef Absolute)
}
authorizationUrl :: OAuth2 -> URI
authorizationUrl = OAuth2.authorizationUrl . getOAuth2
fetchAccessTokenBasic
:: Manager
-> OAuth2
-> ExchangeToken
-> IO (Either TokenResponseError TokenResponse)
fetchAccessTokenBasic =
runFetchAccessToken OAuth2.ClientSecretBasic
fetchAccessTokenPost
:: Manager
-> OAuth2
-> ExchangeToken
-> IO (Either TokenResponseError TokenResponse)
fetchAccessTokenPost =
runFetchAccessToken OAuth2.ClientSecretPost
authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString)
authGetBS m a u = runExceptT $ OAuth2.authGetBS m a u
getOAuth2 :: OAuth2 -> OAuth2.OAuth2
getOAuth2 o =
OAuth2.OAuth2
{ OAuth2.oauth2ClientId = oauth2ClientId o
, OAuth2.oauth2ClientSecret = oauth2ClientSecret o
, OAuth2.oauth2AuthorizeEndpoint = oauth2AuthorizeEndpoint o
, OAuth2.oauth2TokenEndpoint = oauth2TokenEndpoint o
, OAuth2.oauth2RedirectUri = case oauth2RedirectUri o of
Nothing ->
error
"programmer error: yesod-auth-oauth2:OAuth2 must have a Just value set as oauth2RedirectUri before using as an hauth2:OAuth2 value"
Just uri -> uri
}
runFetchAccessToken
:: MonadIO m
=> OAuth2.ClientAuthenticationMethod
-> Manager
-> OAuth2
-> ExchangeToken
-> m (Either TokenResponseError TokenResponse)
runFetchAccessToken am m o e = runExceptT $ OAuth2.fetchAccessTokenWithAuthMethod am m (getOAuth2 o) e

View File

@ -1,10 +1,9 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module URI.ByteString.Extension where module URI.ByteString.Extension where
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.String (IsString (..)) import Data.String (IsString(..))
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Lens.Micro import Lens.Micro
@ -14,26 +13,30 @@ import qualified Data.ByteString.Char8 as C8
import URI.ByteString import URI.ByteString
instance IsString Scheme where instance IsString Scheme where
fromString = Scheme . fromString fromString = Scheme . fromString
instance IsString Host where instance IsString Host where
fromString = Host . fromString fromString = Host . fromString
instance IsString (URIRef Absolute) where instance IsString (URIRef Absolute) where
fromString = fromString = either (error . show) id
either (error . show) id . parseURI strictURIParserOptions . C8.pack . parseURI strictURIParserOptions
. C8.pack
instance IsString (URIRef Relative) where instance IsString (URIRef Relative) where
fromString = fromString = either (error . show) id
either (error . show) id . parseRelativeRef strictURIParserOptions . C8.pack . parseRelativeRef strictURIParserOptions
. C8.pack
fromText :: Text -> Maybe URI fromText :: Text -> Maybe URI
fromText = fromText = either (const Nothing) Just
either (const Nothing) Just . parseURI strictURIParserOptions . encodeUtf8 . parseURI strictURIParserOptions
. encodeUtf8
unsafeFromText :: Text -> URI unsafeFromText :: Text -> URI
unsafeFromText = unsafeFromText = either (error . show) id
either (error . show) id . parseURI strictURIParserOptions . encodeUtf8 . parseURI strictURIParserOptions
. encodeUtf8
toText :: URI -> Text toText :: URI -> Text
toText = decodeUtf8 . serializeURIRef' toText = decodeUtf8 . serializeURIRef'
@ -42,12 +45,9 @@ fromRelative :: Scheme -> Host -> RelativeRef -> URI
fromRelative s h = flip withHost h . toAbsolute s fromRelative s h = flip withHost h . toAbsolute s
withHost :: URIRef a -> Host -> URIRef a withHost :: URIRef a -> Host -> URIRef a
withHost u h = withHost u h = u & authorityL %~ maybe
u (Just $ Authority Nothing h Nothing)
& authorityL (\a -> Just $ a & authorityHostL .~ h)
%~ maybe
(Just $ Authority Nothing h Nothing)
(\a -> Just $ a & authorityHostL .~ h)
withPath :: URIRef a -> ByteString -> URIRef a withPath :: URIRef a -> ByteString -> URIRef a
withPath u p = u & pathL .~ p withPath u p = u & pathL .~ p

View File

@ -1,12 +1,12 @@
{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-orphans #-}
module UnliftIO.Except () where module UnliftIO.Except
() where
import Control.Monad ((<=<)) import Control.Monad.Except
import Control.Monad.Except (ExceptT (..), runExceptT)
import UnliftIO import UnliftIO
instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
withRunInIO exceptToIO = ExceptT $ try $ do withRunInIO exceptToIO = ExceptT $ try $ do
withRunInIO $ \runInIO -> withRunInIO $ \runInIO ->
exceptToIO (runInIO . (either throwIO pure <=< runExceptT)) exceptToIO (runInIO . (either throwIO pure <=< runExceptT))

View File

@ -1,32 +1,33 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- Generic OAuth2 plugin for Yesod -- Generic OAuth2 plugin for Yesod
-- --
-- See @"Yesod.Auth.OAuth2.GitHub"@ for example usage. -- See @"Yesod.Auth.OAuth2.GitHub"@ for example usage.
--
module Yesod.Auth.OAuth2 module Yesod.Auth.OAuth2
( OAuth2 (..) ( OAuth2(..)
, FetchCreds , FetchCreds
, Manager , Manager
, TokenResponse , OAuth2Token(..)
, Creds (..) , Creds(..)
, oauth2Url , oauth2Url
, authOAuth2 , authOAuth2
, authOAuth2Widget , authOAuth2Widget
-- * Alternatives that use 'fetchAccessTokenPost' -- * Alternatives that use 'fetchAccessToken2'
, authOAuth2' , authOAuth2'
, authOAuth2Widget' , authOAuth2Widget'
-- * Reading our @'credsExtra'@ keys -- * Reading our @'credsExtra'@ keys
, getAccessToken , getAccessToken
, getRefreshToken , getRefreshToken
, getUserResponse , getUserResponse
, getUserResponseJSON , getUserResponseJSON
) where )
where
import Control.Error.Util (note) import Control.Error.Util (note)
import Control.Monad ((<=<)) import Control.Monad ((<=<))
@ -35,7 +36,7 @@ import Data.ByteString.Lazy (ByteString, fromStrict)
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Conduit (Manager) import Network.HTTP.Conduit (Manager)
import Network.OAuth.OAuth2.Compat import Network.OAuth.OAuth2
import Yesod.Auth import Yesod.Auth
import Yesod.Auth.OAuth2.Dispatch import Yesod.Auth.OAuth2.Dispatch
import Yesod.Core.Widget import Yesod.Core.Widget
@ -46,12 +47,14 @@ oauth2Url name = PluginR name ["forward"]
-- | Create an @'AuthPlugin'@ for the given OAuth2 provider -- | Create an @'AuthPlugin'@ for the given OAuth2 provider
-- --
-- Presents a generic @"Login via #{name}"@ link -- Presents a generic @"Login via #{name}"@ link
--
authOAuth2 :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2 :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m
authOAuth2 name = authOAuth2Widget [whamlet|Login via #{name}|] name authOAuth2 name = authOAuth2Widget [whamlet|Login via #{name}|] name
-- | A version of 'authOAuth2' that uses 'fetchAccessTokenPost' -- | A version of 'authOAuth2' that uses 'fetchAccessToken2'
-- --
-- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129> -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>
--
authOAuth2' :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2' :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m
authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name
@ -59,42 +62,42 @@ authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name
-- --
-- Allows passing a custom widget for the login link. See @'oauth2Eve'@ for an -- Allows passing a custom widget for the login link. See @'oauth2Eve'@ for an
-- example. -- example.
--
authOAuth2Widget authOAuth2Widget
:: YesodAuth m :: YesodAuth m
=> WidgetFor m () => WidgetFor m ()
-> Text -> Text
-> OAuth2 -> OAuth2
-> FetchCreds m -> FetchCreds m
-> AuthPlugin m -> AuthPlugin m
authOAuth2Widget = buildPlugin fetchAccessTokenBasic authOAuth2Widget = buildPlugin fetchAccessToken
-- | A version of 'authOAuth2Widget' that uses 'fetchAccessTokenPost' -- | A version of 'authOAuth2Widget' that uses 'fetchAccessToken2'
-- --
-- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129> -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>
--
authOAuth2Widget' authOAuth2Widget'
:: YesodAuth m :: YesodAuth m
=> WidgetFor m () => WidgetFor m ()
-> Text -> Text
-> OAuth2 -> OAuth2
-> FetchCreds m -> FetchCreds m
-> AuthPlugin m -> AuthPlugin m
authOAuth2Widget' = buildPlugin fetchAccessTokenPost authOAuth2Widget' = buildPlugin fetchAccessToken2
buildPlugin buildPlugin
:: YesodAuth m :: YesodAuth m
=> FetchToken => FetchToken
-> WidgetFor m () -> WidgetFor m ()
-> Text -> Text
-> OAuth2 -> OAuth2
-> FetchCreds m -> FetchCreds m
-> AuthPlugin m -> AuthPlugin m
buildPlugin getToken widget name oauth getCreds = buildPlugin getToken widget name oauth getCreds = AuthPlugin
AuthPlugin
name name
(dispatchAuthRequest name oauth getToken getCreds) (dispatchAuthRequest name oauth getToken getCreds)
login login
where where login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]
login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]
-- | Read the @'AccessToken'@ from the values set via @'setExtra'@ -- | Read the @'AccessToken'@ from the values set via @'setExtra'@
getAccessToken :: Creds m -> Maybe AccessToken getAccessToken :: Creds m -> Maybe AccessToken
@ -103,15 +106,16 @@ getAccessToken = (AccessToken <$>) . lookup "accessToken" . credsExtra
-- | Read the @'RefreshToken'@ from the values set via @'setExtra'@ -- | Read the @'RefreshToken'@ from the values set via @'setExtra'@
-- --
-- N.B. not all providers supply this value. -- N.B. not all providers supply this value.
--
getRefreshToken :: Creds m -> Maybe RefreshToken getRefreshToken :: Creds m -> Maybe RefreshToken
getRefreshToken = (RefreshToken <$>) . lookup "refreshToken" . credsExtra getRefreshToken = (RefreshToken <$>) . lookup "refreshToken" . credsExtra
-- | Read the original profile response from the values set via @'setExtra'@ -- | Read the original profile response from the values set via @'setExtra'@
getUserResponse :: Creds m -> Maybe ByteString getUserResponse :: Creds m -> Maybe ByteString
getUserResponse = getUserResponse =
(fromStrict . encodeUtf8 <$>) . lookup "userResponse" . credsExtra (fromStrict . encodeUtf8 <$>) . lookup "userResponse" . credsExtra
-- | @'getUserResponse'@, and decode as JSON -- | @'getUserResponse'@, and decode as JSON
getUserResponseJSON :: FromJSON a => Creds m -> Either String a getUserResponseJSON :: FromJSON a => Creds m -> Either String a
getUserResponseJSON = getUserResponseJSON =
eitherDecode <=< note "userResponse key not present" . getUserResponse eitherDecode <=< note "userResponse key not present" . getUserResponse

View File

@ -1,60 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- |
-- OAuth2 plugin for <https://auth0.com>
--
-- * Authenticates against specific auth0 tenant
-- * Uses Auth0 user id (a.k.a [sub](https://auth0.com/docs/api/authentication#get-user-info)) as credentials identifier
module Yesod.Auth.OAuth2.Auth0
( oauth2Auth0HostScopes
, oauth2Auth0Host
, defaultAuth0Scopes
) where
import Data.Aeson as Aeson
import qualified Data.Text as T
import Yesod.Auth.OAuth2.Prelude
import Prelude
-- | https://auth0.com/docs/api/authentication#get-user-info
newtype User = User T.Text
instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "sub"
-- | https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes#standard-claims
defaultAuth0Scopes :: [Text]
defaultAuth0Scopes = ["openid"]
pluginName :: Text
pluginName = "auth0"
oauth2Auth0Host :: YesodAuth m => URI -> Text -> Text -> AuthPlugin m
oauth2Auth0Host host = oauth2Auth0HostScopes host defaultAuth0Scopes
oauth2Auth0HostScopes
:: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m
oauth2Auth0HostScopes host scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do
(User uid, userResponse) <-
authGetProfile
pluginName
manager
token
(host `withPath` "/userinfo")
pure
Creds
{ credsPlugin = pluginName
, credsIdent = uid
, credsExtra = setExtra token userResponse
}
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
host `withPath` "/authorize" `withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,23 +1,24 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for Azure AD. -- OAuth2 plugin for Azure AD.
-- --
-- * Authenticates against Azure AD -- * Authenticates against Azure AD
-- * Uses email as credentials identifier -- * Uses email as credentials identifier
--
module Yesod.Auth.OAuth2.AzureAD module Yesod.Auth.OAuth2.AzureAD
( oauth2AzureAD ( oauth2AzureAD
, oauth2AzureADScoped , oauth2AzureADScoped
) where )
where
import Yesod.Auth.OAuth2.Prelude
import Prelude import Prelude
import Yesod.Auth.OAuth2.Prelude
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "mail" parseJSON = withObject "User" $ \o -> User <$> o .: "mail"
pluginName :: Text pluginName :: Text
pluginName = "azuread" pluginName = "azuread"
@ -30,30 +31,28 @@ oauth2AzureAD = oauth2AzureADScoped defaultScopes
oauth2AzureADScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2AzureADScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2AzureADScoped scopes clientId clientSecret = oauth2AzureADScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://graph.microsoft.com/v1.0/me"
"https://graph.microsoft.com/v1.0/me"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = userId
, credsIdent = userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
"https://login.windows.net/common/oauth2/authorize"
`withQuery` [ scopeParam "," scopes
, ("resource", "https://graph.microsoft.com")
]
, oauthAccessTokenEndpoint =
"https://login.windows.net/common/oauth2/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://login.windows.net/common/oauth2/authorize"
`withQuery` [ scopeParam "," scopes
, ("resource", "https://graph.microsoft.com")
]
, oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,104 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
-- |
--
-- OAuth2 plugin for Azure AD using the new v2 endpoints.
--
-- * Authenticates against Azure AD
-- * Uses email as credentials identifier
module Yesod.Auth.OAuth2.AzureADv2
( oauth2AzureADv2
, oauth2AzureADv2Scoped
, oauth2AzureADv2Widget
, oauth2AzureADv2ScopedWidget
) where
import Yesod.Auth.OAuth2.Prelude
import Yesod.Core (WidgetFor, whamlet)
import Prelude
import Data.String
import Data.Text (unpack)
newtype User = User Text
instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "mail"
pluginName :: Text
pluginName = "azureadv2"
defaultScopes :: [Text]
defaultScopes = ["openid", "profile"]
oauth2AzureADv2
:: YesodAuth m
=> Text
-- ^ Tenant Id
--
-- If using a multi-tenant App, @common@ can be given here.
-> Text
-- ^ Client Id
-> Text
-- ^ Client secret
-> AuthPlugin m
oauth2AzureADv2 = oauth2AzureADv2Scoped defaultScopes
oauth2AzureADv2Widget
:: YesodAuth m => WidgetFor m () -> Text -> Text -> Text -> AuthPlugin m
oauth2AzureADv2Widget widget =
oauth2AzureADv2ScopedWidget widget defaultScopes
oauth2AzureADv2Scoped
:: YesodAuth m => [Text] -> Text -> Text -> Text -> AuthPlugin m
oauth2AzureADv2Scoped =
oauth2AzureADv2ScopedWidget [whamlet|Login via #{pluginName}|]
oauth2AzureADv2ScopedWidget
:: YesodAuth m
=> WidgetFor m ()
-- ^ Widget
-> [Text]
-- ^ Scopes
-> Text
-- ^ Tenant Id
--
-- If using a multi-tenant App, @common@ can be given here.
-> Text
-- ^ Client Id
-> Text
-- ^ Client Secret
-> AuthPlugin m
oauth2AzureADv2ScopedWidget widget scopes tenantId clientId clientSecret =
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <-
authGetProfile
pluginName
manager
token
"https://graph.microsoft.com/v1.0/me"
pure
Creds
{ credsPlugin = pluginName
, credsIdent = userId
, credsExtra = setExtra token userResponse
}
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
tenantUrl "/authorize" `withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint = tenantUrl "/token"
, oauth2RedirectUri = Nothing
}
tenantUrl path =
fromString $
"https://login.microsoftonline.com/"
<> unpack tenantId
<> "/oauth2/v2.0"
<> path

View File

@ -7,10 +7,12 @@
-- * Authenticates against battle.net. -- * Authenticates against battle.net.
-- * Uses user's id as credentials identifier. -- * Uses user's id as credentials identifier.
-- * Returns user's battletag in extras. -- * Returns user's battletag in extras.
--
module Yesod.Auth.OAuth2.BattleNet module Yesod.Auth.OAuth2.BattleNet
( oauth2BattleNet ( oauth2BattleNet
, oAuth2BattleNet , oAuth2BattleNet
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -20,44 +22,42 @@ import Yesod.Core.Widget
newtype User = User Int newtype User = User Int
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "id" parseJSON = withObject "User" $ \o -> User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "battle.net" pluginName = "battle.net"
oauth2BattleNet oauth2BattleNet
:: YesodAuth m :: YesodAuth m
=> WidgetFor m () => WidgetFor m () -- ^ Login widget
-- ^ Login widget -> Text -- ^ User region (e.g. "eu", "cn", "us")
-> Text -> Text -- ^ Client ID
-- ^ User region (e.g. "eu", "cn", "us") -> Text -- ^ Client Secret
-> Text -> AuthPlugin m
-- ^ Client ID
-> Text
-- ^ Client Secret
-> AuthPlugin m
oauth2BattleNet widget region clientId clientSecret = oauth2BattleNet widget region clientId clientSecret =
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do authOAuth2Widget widget pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <-
authGetProfile pluginName manager token $ authGetProfile pluginName manager token
fromRelative "https" (apiHost $ T.toLower region) "/account/user" $ fromRelative
"https"
(apiHost $ T.toLower region)
"/account/user"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = T.pack $ show userId
, credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
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
} }
where
host = wwwHost $ T.toLower region
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"
, oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"
, oauth2RedirectUri = Nothing
}
apiHost :: Text -> Host apiHost :: Text -> Host
apiHost "cn" = "api.battlenet.com.cn" apiHost "cn" = "api.battlenet.com.cn"
@ -68,6 +68,6 @@ wwwHost "cn" = "www.battlenet.com.cn"
wwwHost region = Host $ encodeUtf8 $ region <> ".battle.net" wwwHost region = Host $ encodeUtf8 $ region <> ".battle.net"
oAuth2BattleNet oAuth2BattleNet
:: YesodAuth m => Text -> Text -> Text -> WidgetFor m () -> AuthPlugin m :: YesodAuth m => Text -> Text -> Text -> WidgetFor m () -> AuthPlugin m
oAuth2BattleNet i s r w = oauth2BattleNet w r i s oAuth2BattleNet i s r w = oauth2BattleNet w r i s
{-# DEPRECATED oAuth2BattleNet "Use oauth2BattleNet" #-} {-# DEPRECATED oAuth2BattleNet "Use oauth2BattleNet" #-}

View File

@ -1,15 +1,16 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://bitbucket.com -- OAuth2 plugin for http://bitbucket.com
-- --
-- * Authenticates against bitbucket -- * Authenticates against bitbucket
-- * Uses bitbucket uuid as credentials identifier -- * Uses bitbucket uuid as credentials identifier
--
module Yesod.Auth.OAuth2.Bitbucket module Yesod.Auth.OAuth2.Bitbucket
( oauth2Bitbucket ( oauth2Bitbucket
, oauth2BitbucketScoped , oauth2BitbucketScoped
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -18,7 +19,7 @@ import qualified Data.Text as T
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "uuid" parseJSON = withObject "User" $ \o -> User <$> o .: "uuid"
pluginName :: Text pluginName :: Text
pluginName = "bitbucket" pluginName = "bitbucket"
@ -31,34 +32,32 @@ oauth2Bitbucket = oauth2BitbucketScoped defaultScopes
oauth2BitbucketScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2BitbucketScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2BitbucketScoped scopes clientId clientSecret = oauth2BitbucketScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://api.bitbucket.com/2.0/user"
"https://api.bitbucket.com/2.0/user"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName -- FIXME: Preserved bug. This should just be userId (it's already
, -- FIXME: Preserved bug. This should just be userId (it's already -- a Text), but because this code was shipped, folks likely have
-- a Text), but because this code was shipped, folks likely have -- Idents in their database like @"\"...\""@, and if we fixed this
-- Idents in their database like @"\"...\""@, and if we fixed this -- they would need migrating. We're keeping it for now as it's a
-- they would need migrating. We're keeping it for now as it's a -- minor wart. Breaking typed APIs is one thing, causing data to go
-- minor wart. Breaking typed APIs is one thing, causing data to go -- invalid is another.
-- invalid is another. , credsIdent = T.pack $ show userId
credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
"https://bitbucket.com/site/oauth2/authorize"
`withQuery` [scopeParam "," scopes]
, oauthAccessTokenEndpoint =
"https://bitbucket.com/site/oauth2/access_token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://bitbucket.com/site/oauth2/authorize"
`withQuery` [scopeParam "," scopes]
, oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,9 +1,10 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OAuth2.ClassLink module Yesod.Auth.OAuth2.ClassLink
( oauth2ClassLink ( oauth2ClassLink
, oauth2ClassLinkScoped , oauth2ClassLinkScoped
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -12,7 +13,7 @@ import qualified Data.Text as T
newtype User = User Int newtype User = User Int
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "UserId" parseJSON = withObject "User" $ \o -> User <$> o .: "UserId"
pluginName :: Text pluginName :: Text
pluginName = "classlink" pluginName = "classlink"
@ -25,28 +26,26 @@ oauth2ClassLink = oauth2ClassLinkScoped defaultScopes
oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2ClassLinkScoped scopes clientId clientSecret = oauth2ClassLinkScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://nodeapi.classlink.com/v2/my/info"
"https://nodeapi.classlink.com/v2/my/info"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = T.pack $ show userId
, credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
"https://launchpad.classlink.com/oauth2/v2/auth"
`withQuery` [scopeParam "," scopes]
, oauthAccessTokenEndpoint =
"https://launchpad.classlink.com/oauth2/v2/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://launchpad.classlink.com/oauth2/v2/auth"
`withQuery` [scopeParam "," scopes]
, oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"
, oauth2RedirectUri = Nothing
}

View File

@ -5,20 +5,20 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
module Yesod.Auth.OAuth2.Dispatch module Yesod.Auth.OAuth2.Dispatch
( FetchToken ( FetchToken
, fetchAccessTokenBasic , fetchAccessToken
, fetchAccessTokenPost , fetchAccessToken2
, FetchCreds , FetchCreds
, dispatchAuthRequest , dispatchAuthRequest
) where ) where
import Control.Monad (unless) import Control.Monad.Except
import Control.Monad.Except (MonadError (..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Conduit (Manager) import Network.HTTP.Conduit (Manager)
import Network.OAuth.OAuth2.Compat import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.TokenRequest (Errors)
import URI.ByteString.Extension import URI.ByteString.Extension
import UnliftIO.Exception import UnliftIO.Exception
import Yesod.Auth hiding (ServerError) import Yesod.Auth hiding (ServerError)
@ -30,130 +30,119 @@ import Yesod.Core hiding (ErrorResponse)
-- | How to fetch an @'OAuth2Token'@ -- | How to fetch an @'OAuth2Token'@
-- --
-- This will be 'fetchAccessToken' or 'fetchAccessToken2' -- This will be 'fetchAccessToken' or 'fetchAccessToken2'
type FetchToken = --
Manager type FetchToken
-> OAuth2 = Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)
-> ExchangeToken
-> IO (Either TokenResponseError TokenResponse)
-- | How to take an @'OAuth2Token'@ and retrieve user credentials -- | How to take an @'OAuth2Token'@ and retrieve user credentials
type FetchCreds m = Manager -> TokenResponse -> IO (Creds m) type FetchCreds m = Manager -> OAuth2Token -> IO (Creds m)
-- | Dispatch the various OAuth2 handshake routes -- | Dispatch the various OAuth2 handshake routes
dispatchAuthRequest dispatchAuthRequest
:: Text :: Text -- ^ Name
-- ^ Name -> OAuth2 -- ^ Service details
-> OAuth2 -> FetchToken -- ^ How to get a token
-- ^ Service details -> FetchCreds m -- ^ How to get credentials
-> FetchToken -> Text -- ^ Method
-- ^ How to get a token -> [Text] -- ^ Path pieces
-> FetchCreds m -> AuthHandler m TypedContent
-- ^ How to get credentials
-> Text
-- ^ Method
-> [Text]
-- ^ Path pieces
-> AuthHandler m TypedContent
dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] = dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] =
handleDispatchError $ dispatchForward name oauth2 handleDispatchError $ dispatchForward name oauth2
dispatchAuthRequest name oauth2 getToken getCreds "GET" ["callback"] = dispatchAuthRequest name oauth2 getToken getCreds "GET" ["callback"] =
handleDispatchError $ dispatchCallback name oauth2 getToken getCreds handleDispatchError $ dispatchCallback name oauth2 getToken getCreds
dispatchAuthRequest _ _ _ _ _ _ = notFound dispatchAuthRequest _ _ _ _ _ _ = notFound
-- | Handle @GET \/forward@ -- | Handle @GET \/forward@
-- --
-- 1. Set a random CSRF token in our session -- 1. Set a random CSRF token in our session
-- 2. Redirect to the Provider's authorization URL -- 2. Redirect to the Provider's authorization URL
--
dispatchForward dispatchForward
:: (MonadError DispatchError m, MonadAuthHandler site m) :: (MonadError DispatchError m, MonadAuthHandler site m)
=> Text => Text
-> OAuth2 -> OAuth2
-> m TypedContent -> m TypedContent
dispatchForward name oauth2 = do dispatchForward name oauth2 = do
csrf <- setSessionCSRF $ tokenSessionKey name csrf <- setSessionCSRF $ tokenSessionKey name
oauth2' <- withCallbackAndState name oauth2 csrf oauth2' <- withCallbackAndState name oauth2 csrf
redirect $ toText $ authorizationUrl oauth2' redirect $ toText $ authorizationUrl oauth2'
-- | Handle @GET \/callback@ -- | Handle @GET \/callback@
-- --
-- 1. Verify the URL's CSRF token matches our session -- 1. Verify the URL's CSRF token matches our session
-- 2. Use the code parameter to fetch an AccessToken for the Provider -- 2. Use the code parameter to fetch an AccessToken for the Provider
-- 3. Use the AccessToken to construct a @'Creds'@ value for the Provider -- 3. Use the AccessToken to construct a @'Creds'@ value for the Provider
--
dispatchCallback dispatchCallback
:: (MonadError DispatchError m, MonadAuthHandler site m) :: (MonadError DispatchError m, MonadAuthHandler site m)
=> Text => Text
-> OAuth2 -> OAuth2
-> FetchToken -> FetchToken
-> FetchCreds site -> FetchCreds site
-> m TypedContent -> m TypedContent
dispatchCallback name oauth2 getToken getCreds = do dispatchCallback name oauth2 getToken getCreds = do
onErrorResponse $ throwError . OAuth2HandshakeError onErrorResponse $ throwError . OAuth2HandshakeError
csrf <- verifySessionCSRF $ tokenSessionKey name csrf <- verifySessionCSRF $ tokenSessionKey name
code <- requireGetParam "code" code <- requireGetParam "code"
manager <- authHttpManager manager <- authHttpManager
oauth2' <- withCallbackAndState name oauth2 csrf oauth2' <- withCallbackAndState name oauth2 csrf
token <- token <- either (throwError . OAuth2ResultError) pure
either (throwError . OAuth2ResultError) pure =<< liftIO (getToken manager oauth2' $ ExchangeToken code)
=<< liftIO (getToken manager oauth2' $ ExchangeToken code) creds <-
creds <- liftIO (getCreds manager token)
liftIO (getCreds manager token) `catch` (throwError . FetchCredsIOException)
`catch` (throwError . FetchCredsIOException) `catch` (throwError . FetchCredsYesodOAuth2Exception)
`catch` (throwError . FetchCredsYesodOAuth2Exception) setCredsRedirect creds
setCredsRedirect creds
withCallbackAndState withCallbackAndState
:: (MonadError DispatchError m, MonadAuthHandler site m) :: (MonadError DispatchError m, MonadAuthHandler site m)
=> Text => Text
-> OAuth2 -> OAuth2
-> Text -> Text
-> m OAuth2 -> m OAuth2
withCallbackAndState name oauth2 csrf = do withCallbackAndState name oauth2 csrf = do
callback <- maybe defaultCallback pure $ oauth2RedirectUri oauth2
pure
oauth2
{ oauth2RedirectUri = Just callback
, oauth2AuthorizeEndpoint =
oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)]
}
where
defaultCallback = do
uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender
maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri
pure oauth2
{ oauthCallback = Just callback
, oauthOAuthorizeEndpoint =
oauthOAuthorizeEndpoint oauth2
`withQuery` [("state", encodeUtf8 csrf)]
}
getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text) getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text)
getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent
-- | Set a random, ~64-byte value in the session -- | Set a random, ~30-character value in the session
-- --
-- Some (but not all) providers decode a @+@ in the state token as a space when -- Some (but not all) providers decode a @+@ in the state token as a space when
-- sending it back to us. We don't expect this and fail. And if we did code for -- sending it back to us. We don't expect this and fail. And if we did code for
-- it, we'd then fail on the providers that /don't/ do that. -- it, we'd then fail on the providers that /don't/ do that.
-- --
-- Therefore, we just exclude @+@ in our tokens, which means this function may -- Therefore, we just exclude @+@ in our tokens, which means this function may
-- return slightly fewer than 64 bytes. -- return slightly less than 30 characters.
--
setSessionCSRF :: MonadHandler m => Text -> m Text setSessionCSRF :: MonadHandler m => Text -> m Text
setSessionCSRF sessionKey = do setSessionCSRF sessionKey = do
csrfToken <- liftIO randomToken csrfToken <- liftIO randomToken
csrfToken <$ setSession sessionKey csrfToken csrfToken <$ setSession sessionKey csrfToken
where where randomToken = T.filter (/= '+') <$> randomText 64
randomToken = T.filter (/= '+') <$> randomText 64
-- | Verify the callback provided the same CSRF token as in our session -- | Verify the callback provided the same CSRF token as in our session
verifySessionCSRF verifySessionCSRF
:: (MonadError DispatchError m, MonadHandler m) => Text -> m Text :: (MonadError DispatchError m, MonadHandler m) => Text -> m Text
verifySessionCSRF sessionKey = do verifySessionCSRF sessionKey = do
token <- requireGetParam "state" token <- requireGetParam "state"
sessionToken <- lookupSession sessionKey sessionToken <- lookupSession sessionKey
deleteSession sessionKey deleteSession sessionKey
token token <$ unless
<$ unless (sessionToken == Just token)
(sessionToken == Just token) (throwError $ InvalidStateToken sessionToken token)
(throwError $ InvalidStateToken sessionToken token)
requireGetParam requireGetParam
:: (MonadError DispatchError m, MonadHandler m) => Text -> m Text :: (MonadError DispatchError m, MonadHandler m) => Text -> m Text
requireGetParam key = requireGetParam key =
maybe (throwError $ MissingParameter key) pure =<< lookupGetParam key maybe (throwError $ MissingParameter key) pure =<< lookupGetParam key
tokenSessionKey :: Text -> Text tokenSessionKey :: Text -> Text
tokenSessionKey name = "_yesod_oauth2_" <> name tokenSessionKey name = "_yesod_oauth2_" <> name

View File

@ -9,14 +9,15 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
module Yesod.Auth.OAuth2.DispatchError module Yesod.Auth.OAuth2.DispatchError
( DispatchError (..) ( DispatchError(..)
, handleDispatchError , handleDispatchError
, onDispatchError , onDispatchError
) where ) where
import Control.Monad.Except import Control.Monad.Except
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Network.OAuth.OAuth2.Compat (TokenResponseError) import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.TokenRequest (Errors)
import UnliftIO.Except () import UnliftIO.Except ()
import UnliftIO.Exception import UnliftIO.Exception
import Yesod.Auth hiding (ServerError) import Yesod.Auth hiding (ServerError)
@ -26,55 +27,55 @@ import Yesod.Auth.OAuth2.Random
import Yesod.Core hiding (ErrorResponse) import Yesod.Core hiding (ErrorResponse)
data DispatchError data DispatchError
= MissingParameter Text = MissingParameter Text
| InvalidStateToken (Maybe Text) Text | InvalidStateToken (Maybe Text) Text
| InvalidCallbackUri Text | InvalidCallbackUri Text
| OAuth2HandshakeError ErrorResponse | OAuth2HandshakeError ErrorResponse
| OAuth2ResultError TokenResponseError | OAuth2ResultError (OAuth2Error Errors)
| FetchCredsIOException IOException | FetchCredsIOException IOException
| FetchCredsYesodOAuth2Exception YesodOAuth2Exception | FetchCredsYesodOAuth2Exception YesodOAuth2Exception
| OtherDispatchError Text | OtherDispatchError Text
deriving stock (Show) deriving stock Show
deriving anyclass (Exception) deriving anyclass Exception
-- | User-friendly message for any given 'DispatchError' -- | User-friendly message for any given 'DispatchError'
-- --
-- Most of these are opaque to the user. The exception details are present for -- Most of these are opaque to the user. The exception details are present for
-- the server logs. -- the server logs.
--
dispatchErrorMessage :: DispatchError -> Text dispatchErrorMessage :: DispatchError -> Text
dispatchErrorMessage = \case dispatchErrorMessage = \case
MissingParameter name -> MissingParameter name ->
"Parameter '" <> name <> "' is required, but not present in the URL" "Parameter '" <> name <> "' is required, but not present in the URL"
InvalidStateToken {} -> "State token is invalid, please try again" InvalidStateToken{} -> "State token is invalid, please try again"
InvalidCallbackUri {} -> InvalidCallbackUri{}
"Callback URI was not valid, this server may be misconfigured (no approot)" -> "Callback URI was not valid, this server may be misconfigured (no approot)"
OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er
OAuth2ResultError {} -> "Login failed, please try again" OAuth2ResultError{} -> "Login failed, please try again"
FetchCredsIOException {} -> "Login failed, please try again" FetchCredsIOException{} -> "Login failed, please try again"
FetchCredsYesodOAuth2Exception {} -> "Login failed, please try again" FetchCredsYesodOAuth2Exception{} -> "Login failed, please try again"
OtherDispatchError {} -> "Login failed, please try again" OtherDispatchError{} -> "Login failed, please try again"
handleDispatchError handleDispatchError
:: MonadAuthHandler site m :: MonadAuthHandler site m
=> ExceptT DispatchError m TypedContent => ExceptT DispatchError m TypedContent
-> m TypedContent -> m TypedContent
handleDispatchError f = do handleDispatchError f = do
result <- runExceptT f result <- runExceptT f
either onDispatchError pure result either onDispatchError pure result
onDispatchError :: MonadAuthHandler site m => DispatchError -> m TypedContent onDispatchError :: MonadAuthHandler site m => DispatchError -> m TypedContent
onDispatchError err = do onDispatchError err = do
errorId <- liftIO $ randomText 16 errorId <- liftIO $ randomText 16
let suffix = " [errorId=" <> errorId <> "]" let suffix = " [errorId=" <> errorId <> "]"
$(logError) $ pack (displayException err) <> suffix $(logError) $ pack (displayException err) <> suffix
let let message = dispatchErrorMessage err <> suffix
message = dispatchErrorMessage err <> suffix messageValue =
messageValue = object ["error" .= object ["id" .= errorId, "message" .= message]]
object ["error" .= object ["id" .= errorId, "message" .= message]]
loginR <- ($ LoginR) <$> getRouteToParent loginR <- ($ LoginR) <$> getRouteToParent
selectRep $ do selectRep $ do
provideRep @_ @Html $ onErrorHtml loginR message provideRep @_ @Html $ onErrorHtml loginR message
provideRep @_ @Value $ pure messageValue provideRep @_ @Value $ pure messageValue

View File

@ -1,15 +1,16 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | OAuth callback error response -- | OAuth callback error response
-- --
-- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1> -- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>
--
module Yesod.Auth.OAuth2.ErrorResponse module Yesod.Auth.OAuth2.ErrorResponse
( ErrorResponse (..) ( ErrorResponse(..)
, erUserMessage , erUserMessage
, ErrorName (..) , ErrorName(..)
, onErrorResponse , onErrorResponse
, unknownError , unknownError
) where )
where
import Data.Foldable (traverse_) import Data.Foldable (traverse_)
import Data.Text (Text) import Data.Text (Text)
@ -17,54 +18,58 @@ import Data.Traversable (for)
import Yesod.Core (MonadHandler, lookupGetParam) import Yesod.Core (MonadHandler, lookupGetParam)
data ErrorName data ErrorName
= InvalidRequest = InvalidRequest
| UnauthorizedClient | UnauthorizedClient
| AccessDenied | AccessDenied
| UnsupportedResponseType | UnsupportedResponseType
| InvalidScope | InvalidScope
| ServerError | ServerError
| TemporarilyUnavailable | TemporarilyUnavailable
| Unknown Text | Unknown Text
deriving (Show) deriving Show
data ErrorResponse = ErrorResponse data ErrorResponse = ErrorResponse
{ erName :: ErrorName { erName :: ErrorName
, erDescription :: Maybe Text , erDescription :: Maybe Text
, erURI :: Maybe Text , erURI :: Maybe Text
} }
deriving (Show) deriving Show
-- | Textual value suitable for display to a User -- | Textual value suitable for display to a User
erUserMessage :: ErrorResponse -> Text erUserMessage :: ErrorResponse -> Text
erUserMessage err = case erName err of erUserMessage err = case erName err of
InvalidRequest -> "Invalid request" InvalidRequest -> "Invalid request"
UnauthorizedClient -> "Unauthorized client" UnauthorizedClient -> "Unauthorized client"
AccessDenied -> "Access denied" AccessDenied -> "Access denied"
UnsupportedResponseType -> "Unsupported response type" UnsupportedResponseType -> "Unsupported response type"
InvalidScope -> "Invalid scope" InvalidScope -> "Invalid scope"
ServerError -> "Server error" ServerError -> "Server error"
TemporarilyUnavailable -> "Temporarily unavailable" TemporarilyUnavailable -> "Temporarily unavailable"
Unknown _ -> "Unknown error" Unknown _ -> "Unknown error"
unknownError :: Text -> ErrorResponse unknownError :: Text -> ErrorResponse
unknownError x = unknownError x = ErrorResponse
ErrorResponse {erName = Unknown x, erDescription = Nothing, erURI = Nothing} { erName = Unknown x
, erDescription = Nothing
, erURI = Nothing
}
-- | Check query parameters for an error, if found run the given action -- | Check query parameters for an error, if found run the given action
-- --
-- The action is expected to use a short-circuit response function like -- The action is expected to use a short-circuit response function like
-- @'permissionDenied'@, hence this returning @()@. -- @'permissionDenied'@, hence this returning @()@.
--
onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m () onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m ()
onErrorResponse f = traverse_ f =<< checkErrorResponse onErrorResponse f = traverse_ f =<< checkErrorResponse
checkErrorResponse :: MonadHandler m => m (Maybe ErrorResponse) checkErrorResponse :: MonadHandler m => m (Maybe ErrorResponse)
checkErrorResponse = do checkErrorResponse = do
merror <- lookupGetParam "error" merror <- lookupGetParam "error"
for merror $ \err -> for merror $ \err ->
ErrorResponse (readErrorName err) ErrorResponse (readErrorName err)
<$> lookupGetParam "error_description" <$> lookupGetParam "error_description"
<*> lookupGetParam "error_uri" <*> lookupGetParam "error_uri"
readErrorName :: Text -> ErrorName readErrorName :: Text -> ErrorName
readErrorName "invalid_request" = InvalidRequest readErrorName "invalid_request" = InvalidRequest

View File

@ -1,17 +1,18 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- OAuth2 plugin for http://eveonline.com -- OAuth2 plugin for http://eveonline.com
-- --
-- * Authenticates against eveonline -- * Authenticates against eveonline
-- * Uses EVEs unique account-user-char-hash as credentials identifier -- * Uses EVEs unique account-user-char-hash as credentials identifier
--
module Yesod.Auth.OAuth2.EveOnline module Yesod.Auth.OAuth2.EveOnline
( oauth2Eve ( oauth2Eve
, oauth2EveScoped , oauth2EveScoped
, WidgetType (..) , WidgetType(..)
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -21,27 +22,26 @@ import Yesod.Core.Widget
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash" parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash"
data WidgetType m data WidgetType m
= -- | Simple "Login via eveonline" text = Plain -- ^ Simple "Login via eveonline" text
Plain | BigWhite
| BigWhite | SmallWhite
| SmallWhite | BigBlack
| BigBlack | SmallBlack
| SmallBlack | Custom (WidgetFor m ())
| Custom (WidgetFor m ())
asWidget :: YesodAuth m => WidgetType m -> WidgetFor m () asWidget :: YesodAuth m => WidgetType m -> WidgetFor m ()
asWidget Plain = [whamlet|Login via eveonline|] asWidget Plain = [whamlet|Login via eveonline|]
asWidget BigWhite = asWidget BigWhite =
[whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45">|] [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45">|]
asWidget BigBlack = asWidget BigBlack
[whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|] = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|]
asWidget SmallWhite = asWidget SmallWhite
[whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|] = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|]
asWidget SmallBlack = asWidget SmallBlack
[whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|] = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|]
asWidget (Custom a) = a asWidget (Custom a) = a
pluginName :: Text pluginName :: Text
@ -54,32 +54,29 @@ oauth2Eve :: YesodAuth m => WidgetType m -> Text -> Text -> AuthPlugin m
oauth2Eve = oauth2EveScoped defaultScopes oauth2Eve = oauth2EveScoped defaultScopes
oauth2EveScoped oauth2EveScoped
:: YesodAuth m => [Text] -> WidgetType m -> Text -> Text -> AuthPlugin m :: YesodAuth m => [Text] -> WidgetType m -> Text -> Text -> AuthPlugin m
oauth2EveScoped scopes widgetType clientId clientSecret = oauth2EveScoped scopes widgetType clientId clientSecret =
authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token -> authOAuth2Widget (asWidget widgetType) pluginName oauth2
do $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://login.eveonline.com/oauth/verify"
"https://login.eveonline.com/oauth/verify"
pure pure Creds
Creds { credsPlugin = "eveonline"
{ credsPlugin = "eveonline" -- FIXME: Preserved bug. See similar comment in Bitbucket provider.
, -- FIXME: Preserved bug. See similar comment in Bitbucket provider. , credsIdent = T.pack $ show userId
credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
} where
where oauth2 = OAuth2
oauth2 = { oauthClientId = clientId
OAuth2 , oauthClientSecret = Just clientSecret
{ oauth2ClientId = clientId , oauthOAuthorizeEndpoint =
, oauth2ClientSecret = clientSecret "https://login.eveonline.com/oauth/authorize"
, oauth2AuthorizeEndpoint = `withQuery` [("response_type", "code"), scopeParam " " scopes]
"https://login.eveonline.com/oauth/authorize" , oauthAccessTokenEndpoint = "https://login.eveonline.com/oauth/token"
`withQuery` [("response_type", "code"), scopeParam " " scopes] , oauthCallback = Nothing
, oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token" }
, oauth2RedirectUri = Nothing
}

View File

@ -1,24 +1,29 @@
{-# LANGUAGE DeriveDataTypeable #-}
module Yesod.Auth.OAuth2.Exception module Yesod.Auth.OAuth2.Exception
( YesodOAuth2Exception (..) ( YesodOAuth2Exception(..)
) where ) where
import Control.Exception.Safe import Control.Exception.Safe
import Data.ByteString.Lazy (ByteString) import Data.ByteString.Lazy (ByteString)
import Data.Text (Text) import Data.Text (Text)
data YesodOAuth2Exception data YesodOAuth2Exception
= -- | HTTP error during OAuth2 handshake = OAuth2Error Text ByteString
-- ^ HTTP error during OAuth2 handshake
-- --
-- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@. -- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.
OAuth2Error Text ByteString --
| -- | User profile was not as expected | JSONDecodingError Text String
-- ^ User profile was not as expected
-- --
-- Plugin name and Aeson parse error message. -- Plugin name and Aeson parse error message.
JSONDecodingError Text String --
| -- | Other error conditions | GenericError Text String
-- ^ Other error conditions
-- --
-- Plugin name and error message. -- Plugin name and error message.
GenericError Text String --
deriving (Show) deriving (Show, Typeable)
instance Exception YesodOAuth2Exception instance Exception YesodOAuth2Exception

View File

@ -1,27 +1,25 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- OAuth2 plugin for http://github.com -- OAuth2 plugin for http://github.com
-- --
-- * Authenticates against github -- * Authenticates against github
-- * Uses github user id as credentials identifier -- * Uses github user id as credentials identifier
--
module Yesod.Auth.OAuth2.GitHub module Yesod.Auth.OAuth2.GitHub
( oauth2GitHub ( oauth2GitHub
, oauth2GitHubWidget , oauth2GitHubScoped
, oauth2GitHubScoped )
, oauth2GitHubScopedWidget where
) where
import Yesod.Auth.OAuth2.Prelude
import qualified Data.Text as T import qualified Data.Text as T
import Yesod.Auth.OAuth2.Prelude
import Yesod.Core (WidgetFor, whamlet)
newtype User = User Int newtype User = User Int
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "id" parseJSON = withObject "User" $ \o -> User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "github" pluginName = "github"
@ -32,39 +30,28 @@ defaultScopes = ["user:email"]
oauth2GitHub :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2GitHub :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2GitHub = oauth2GitHubScoped defaultScopes oauth2GitHub = oauth2GitHubScoped defaultScopes
oauth2GitHubWidget
:: YesodAuth m => WidgetFor m () -> Text -> Text -> AuthPlugin m
oauth2GitHubWidget widget = oauth2GitHubScopedWidget widget defaultScopes
oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2GitHubScoped = oauth2GitHubScoped scopes clientId clientSecret =
oauth2GitHubScopedWidget [whamlet|Login via #{pluginName}|] authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile
pluginName
manager
token
"https://api.github.com/user"
oauth2GitHubScopedWidget pure Creds
:: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m { credsPlugin = pluginName
oauth2GitHubScopedWidget widget scopes clientId clientSecret = , credsIdent = T.pack $ show userId
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do , credsExtra = setExtra token userResponse
(User userId, userResponse) <- }
authGetProfile where
pluginName oauth2 = OAuth2
manager { oauthClientId = clientId
token , oauthClientSecret = Just clientSecret
"https://api.github.com/user" , oauthOAuthorizeEndpoint =
"https://github.com/login/oauth/authorize"
pure `withQuery` [scopeParam "," scopes]
Creds , oauthAccessTokenEndpoint =
{ credsPlugin = pluginName "https://github.com/login/oauth/access_token"
, credsIdent = T.pack $ show userId , oauthCallback = Nothing
, credsExtra = setExtra token userResponse
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://github.com/login/oauth/authorize"
`withQuery` [scopeParam "," scopes]
, oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,11 +1,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OAuth2.GitLab module Yesod.Auth.OAuth2.GitLab
( oauth2GitLab ( oauth2GitLab
, oauth2GitLabHostScopes , oauth2GitLabHostScopes
, defaultHost , defaultHost
, defaultScopes , defaultScopes
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -14,7 +14,7 @@ import qualified Data.Text as T
newtype User = User Int newtype User = User Int
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "id" parseJSON = withObject "User" $ \o -> User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "gitlab" pluginName = "gitlab"
@ -33,29 +33,32 @@ defaultScopes = ["read_user"]
-- --
-- > oauth2GitLabHostScopes defaultHost ["api", "read_user"] -- > oauth2GitLabHostScopes defaultHost ["api", "read_user"]
-- > oauth2GitLabHostScopes "https://gitlab.example.com" defaultScopes -- > oauth2GitLabHostScopes "https://gitlab.example.com" defaultScopes
--
oauth2GitLab :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2GitLab :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2GitLab = oauth2GitLabHostScopes defaultHost defaultScopes oauth2GitLab = oauth2GitLabHostScopes defaultHost defaultScopes
oauth2GitLabHostScopes oauth2GitLabHostScopes
:: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m :: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m
oauth2GitLabHostScopes host scopes clientId clientSecret = oauth2GitLabHostScopes host scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <-
authGetProfile pluginName manager token $ host `withPath` "/api/v4/user" authGetProfile pluginName manager token
$ host
`withPath` "/api/v4/user"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = T.pack $ show userId
, credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
host
`withPath` "/oauth/authorize"
`withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint = host `withPath` "/oauth/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,6 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- OAuth2 plugin for http://www.google.com -- OAuth2 plugin for http://www.google.com
@ -24,12 +23,14 @@
-- > updatedCreds = creds { credsIdent = email } -- > updatedCreds = creds { credsIdent = email }
-- > -- >
-- > -- continue normally with updatedCreds -- > -- continue normally with updatedCreds
--
module Yesod.Auth.OAuth2.Google module Yesod.Auth.OAuth2.Google
( oauth2Google ( oauth2Google
, oauth2GoogleWidget , oauth2GoogleWidget
, oauth2GoogleScoped , oauth2GoogleScoped
, oauth2GoogleScopedWidget , oauth2GoogleScopedWidget
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
import Yesod.Core (WidgetFor, whamlet) import Yesod.Core (WidgetFor, whamlet)
@ -37,10 +38,10 @@ import Yesod.Core (WidgetFor, whamlet)
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = parseJSON =
withObject "User" $ \o -> withObject "User" $ \o -> User
-- Required for data backwards-compatibility -- Required for data backwards-compatibility
User . ("google-uid:" <>) <$> o .: "sub" <$> (("google-uid:" <>) <$> o .: "sub")
pluginName :: Text pluginName :: Text
pluginName = "google" pluginName = "google"
@ -51,39 +52,34 @@ defaultScopes = ["openid", "email"]
oauth2Google :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Google :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Google = oauth2GoogleScoped defaultScopes oauth2Google = oauth2GoogleScoped defaultScopes
oauth2GoogleWidget oauth2GoogleWidget :: YesodAuth m => WidgetFor m () -> Text -> Text -> AuthPlugin m
:: YesodAuth m => WidgetFor m () -> Text -> Text -> AuthPlugin m
oauth2GoogleWidget widget = oauth2GoogleScopedWidget widget defaultScopes oauth2GoogleWidget widget = oauth2GoogleScopedWidget widget defaultScopes
oauth2GoogleScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2GoogleScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2GoogleScoped = oauth2GoogleScoped = oauth2GoogleScopedWidget [whamlet|Login via #{pluginName}|]
oauth2GoogleScopedWidget [whamlet|Login via #{pluginName}|]
oauth2GoogleScopedWidget oauth2GoogleScopedWidget :: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m
:: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m
oauth2GoogleScopedWidget widget scopes clientId clientSecret = oauth2GoogleScopedWidget widget scopes clientId clientSecret =
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do authOAuth2Widget widget pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://www.googleapis.com/oauth2/v3/userinfo"
"https://www.googleapis.com/oauth2/v3/userinfo"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = userId
, credsIdent = userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
"https://accounts.google.com/o/oauth2/auth"
`withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint =
"https://www.googleapis.com/oauth2/v3/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://accounts.google.com/o/oauth2/auth"
`withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,8 +1,9 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OAuth2.Nylas module Yesod.Auth.OAuth2.Nylas
( oauth2Nylas ( oauth2Nylas
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -15,7 +16,7 @@ import qualified Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "id" parseJSON = withObject "User" $ \o -> User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "nylas" pluginName = "nylas"
@ -25,46 +26,44 @@ defaultScopes = ["email"]
oauth2Nylas :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Nylas :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Nylas clientId clientSecret = oauth2Nylas clientId clientSecret =
authOAuth2 pluginName oauth $ \manager token -> do authOAuth2 pluginName oauth $ \manager token -> do
req <- req <- applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""
applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) "" <$> parseRequest "https://api.nylas.com/account"
<$> parseRequest "https://api.nylas.com/account" resp <- httpLbs req manager
resp <- httpLbs req manager let userResponse = responseBody resp
let userResponse = responseBody resp
-- FIXME: was this working? I'm 95% sure that the client will throw its -- FIXME: was this working? I'm 95% sure that the client will throw its
-- own exception on unsuccessful status codes. -- own exception on unsuccessful status codes.
unless (HT.statusIsSuccessful $ responseStatus resp) $ unless (HT.statusIsSuccessful $ responseStatus resp)
throwIO $ $ throwIO
YesodOAuth2Exception.GenericError pluginName $ $ YesodOAuth2Exception.GenericError pluginName
"Unsuccessful HTTP response: " $ "Unsuccessful HTTP response: "
<> BL8.unpack userResponse <> BL8.unpack userResponse
either either
(throwIO . YesodOAuth2Exception.JSONDecodingError pluginName) (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)
( \(User userId) -> (\(User userId) -> pure Creds
pure { credsPlugin = pluginName
Creds , credsIdent = userId
{ credsPlugin = pluginName , credsExtra = setExtra token userResponse
, credsIdent = userId }
, credsExtra = setExtra token userResponse )
} $ eitherDecode userResponse
) where
$ eitherDecode userResponse oauth = OAuth2
where { oauthClientId = clientId
oauth = , oauthClientSecret = Just clientSecret
OAuth2 , oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId "https://api.nylas.com/oauth/authorize"
, oauth2ClientSecret = clientSecret `withQuery` [ ("response_type", "code")
, oauth2AuthorizeEndpoint = , ( "client_id"
"https://api.nylas.com/oauth/authorize" , encodeUtf8 clientId
`withQuery` [ ("response_type", "code") )
, ("client_id", encodeUtf8 clientId) -- N.B. The scopes delimeter is unknown/untested. Verify that before
, -- N.B. The scopes delimeter is unknown/untested. Verify that before -- extracting this to an argument and offering a Scoped function. In
-- extracting this to an argument and offering a Scoped function. In -- its current state, it doesn't matter because it's only one scope.
-- its current state, it doesn't matter because it's only one scope. , scopeParam "," defaultScopes
scopeParam "," defaultScopes ]
] , oauthAccessTokenEndpoint = "https://api.nylas.com/oauth/token"
, oauth2TokenEndpoint = "https://api.nylas.com/oauth/token" , oauthCallback = Nothing
, oauth2RedirectUri = Nothing }
}

View File

@ -1,50 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OAuth2.ORCID
( oauth2ORCID
) where
import qualified Data.Text as T
import Yesod.Auth.OAuth2.Prelude
pluginName :: Text
pluginName = "orcid"
newtype User = User Text
instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "sub"
oauth2ORCID
:: YesodAuth m
=> Text
-- ^ Client Id
-> Text
-- ^ Client Secret
-> AuthPlugin m
oauth2ORCID clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <-
authGetProfile
pluginName
manager
token
"https://orcid.org/oauth/userinfo"
pure
Creds
{ credsPlugin = pluginName
, credsIdent = T.pack $ show userId
, credsExtra = setExtra token userResponse
}
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://orcid.org/oauth/authorize"
`withQuery` [scopeParam " " ["openid"]]
, oauth2TokenEndpoint = "https://orcid.org/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,63 +1,61 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
-- | -- |
-- --
-- Modules and support functions required by most or all provider -- Modules and support functions required by most or all provider
-- implementations. May also be useful for writing local providers. -- implementations. May also be useful for writing local providers.
--
module Yesod.Auth.OAuth2.Prelude module Yesod.Auth.OAuth2.Prelude
( authGetProfile (
, scopeParam -- * Provider helpers
, setExtra authGetProfile
, scopeParam
, setExtra
-- * Text -- * Text
, Text , Text
, decodeUtf8 , decodeUtf8
, encodeUtf8 , encodeUtf8
-- * JSON -- * JSON
, (.:) , (.:)
, (.:?) , (.:?)
, (.=) , (.=)
, (<>) , (<>)
, FromJSON (..) , FromJSON(..)
, ToJSON (..) , ToJSON(..)
, eitherDecode , eitherDecode
, withObject , withObject
-- * Exceptions -- * Exceptions
, throwIO , throwIO
-- * OAuth2 -- * OAuth2
, OAuth2 (..) , OAuth2(..)
, TokenResponse , OAuth2Token(..)
, accessToken , AccessToken(..)
, refreshToken , RefreshToken(..)
, expiresIn
, tokenType
, idToken
, AccessToken (..)
, RefreshToken (..)
-- * HTTP -- * HTTP
, Manager , Manager
-- * Yesod -- * Yesod
, YesodAuth (..) , YesodAuth(..)
, AuthPlugin (..) , AuthPlugin(..)
, Creds (..) , Creds(..)
-- * Bytestring URI types -- * Bytestring URI types
, URI , URI
, Host (..) , Host(..)
-- * Bytestring URI extensions -- * Bytestring URI extensions
, module URI.ByteString.Extension , module URI.ByteString.Extension
-- * Temporary, until I finish re-structuring modules -- * Temporary, until I finish re-structuring modules
, authOAuth2 , authOAuth2
, authOAuth2Widget , authOAuth2Widget
) where )
where
import Control.Exception.Safe import Control.Exception.Safe
import Data.Aeson import Data.Aeson
@ -67,7 +65,7 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding import Data.Text.Encoding
import Network.HTTP.Conduit import Network.HTTP.Conduit
import Network.OAuth.OAuth2.Compat import Network.OAuth.OAuth2
import URI.ByteString import URI.ByteString
import URI.ByteString.Extension import URI.ByteString.Extension
import Yesod.Auth import Yesod.Auth
@ -79,33 +77,34 @@ import qualified Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
-- The response should be parsed only far enough to read the required -- The response should be parsed only far enough to read the required
-- @'credsIdent'@. Additional information should either be re-parsed by or -- @'credsIdent'@. Additional information should either be re-parsed by or
-- fetched via additional requests by consumers. -- fetched via additional requests by consumers.
--
authGetProfile authGetProfile
:: FromJSON a :: FromJSON a
=> Text => Text
-> Manager -> Manager
-> TokenResponse -> OAuth2Token
-> URI -> URI
-> IO (a, BL.ByteString) -> IO (a, BL.ByteString)
authGetProfile name manager token url = do authGetProfile name manager token url = do
resp <- fromAuthGet name =<< authGetBS manager (accessToken token) url resp <- fromAuthGet name =<< authGetBS manager (accessToken token) url
decoded <- fromAuthJSON name resp decoded <- fromAuthJSON name resp
pure (decoded, resp) pure (decoded, resp)
-- | Throws a @Left@ result as an @'YesodOAuth2Exception'@ -- | Throws a @Left@ result as an @'YesodOAuth2Exception'@
fromAuthGet :: Text -> Either BL.ByteString BL.ByteString -> IO BL.ByteString fromAuthGet :: Text -> Either BL.ByteString BL.ByteString -> IO BL.ByteString
fromAuthGet _ (Right bs) = pure bs -- nice fromAuthGet _ (Right bs) = pure bs -- nice
fromAuthGet name (Left err) = fromAuthGet name (Left err) =
throwIO $ YesodOAuth2Exception.OAuth2Error name err throwIO $ YesodOAuth2Exception.OAuth2Error name err
-- | Throws a decoding error as an @'YesodOAuth2Exception'@ -- | Throws a decoding error as an @'YesodOAuth2Exception'@
fromAuthJSON :: FromJSON a => Text -> BL.ByteString -> IO a fromAuthJSON :: FromJSON a => Text -> BL.ByteString -> IO a
fromAuthJSON name = fromAuthJSON name =
either (throwIO . YesodOAuth2Exception.JSONDecodingError name) pure either (throwIO . YesodOAuth2Exception.JSONDecodingError name) pure
. eitherDecode . eitherDecode
-- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter -- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter
scopeParam :: Text -> [Text] -> (ByteString, ByteString) scopeParam :: Text -> [Text] -> (ByteString, ByteString)
scopeParam d = ("scope",) . encodeUtf8 . T.intercalate d scopeParam d = ("scope", ) . encodeUtf8 . T.intercalate d
-- brittany-disable-next-binding -- brittany-disable-next-binding
@ -119,9 +118,10 @@ scopeParam d = ("scope",) . encodeUtf8 . T.intercalate d
-- May set the following keys: -- May set the following keys:
-- --
-- - @refreshToken@: if the provider supports refreshing the @accessToken@ -- - @refreshToken@: if the provider supports refreshing the @accessToken@
setExtra :: TokenResponse -> BL.ByteString -> [(Text, Text)] --
setExtra :: OAuth2Token -> BL.ByteString -> [(Text, Text)]
setExtra token userResponse = setExtra token userResponse =
[ ("accessToken", atoken $ accessToken token) [ ("accessToken", atoken $ accessToken token)
, ("userResponse", decodeUtf8 $ BL.toStrict userResponse) , ("userResponse", decodeUtf8 $ BL.toStrict userResponse)
] ]
<> maybe [] (pure . ("refreshToken",) . rtoken) (refreshToken token) <> maybe [] (pure . ("refreshToken", ) . rtoken) (refreshToken token)

View File

@ -1,19 +1,19 @@
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
module Yesod.Auth.OAuth2.Random module Yesod.Auth.OAuth2.Random
( randomText ( randomText
) where ) where
import Crypto.Random (MonadRandom, getRandomBytes) import Crypto.Random (MonadRandom, getRandomBytes)
import Data.ByteArray.Encoding (Base (Base64), convertToBase) import Data.ByteArray.Encoding (Base(Base64), convertToBase)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
randomText randomText
:: MonadRandom m :: MonadRandom m
=> Int => Int
-- ^ Size in Bytes (not necessarily characters) -- ^ Size in Bytes (note necessarily characters)
-> m Text -> m Text
randomText size = randomText size =
decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size

View File

@ -1,24 +1,25 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://login.salesforce.com -- OAuth2 plugin for http://login.salesforce.com
-- --
-- * Authenticates against Salesforce (or sandbox) -- * Authenticates against Salesforce (or sandbox)
-- * Uses Salesforce user id as credentials identifier -- * Uses Salesforce user id as credentials identifier
--
module Yesod.Auth.OAuth2.Salesforce module Yesod.Auth.OAuth2.Salesforce
( oauth2Salesforce ( oauth2Salesforce
, oauth2SalesforceScoped , oauth2SalesforceScoped
, oauth2SalesforceSandbox , oauth2SalesforceSandbox
, oauth2SalesforceSandboxScoped , oauth2SalesforceSandboxScoped
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "user_id" parseJSON = withObject "User" $ \o -> User <$> o .: "user_id"
pluginName :: Text pluginName :: Text
pluginName = "salesforce" pluginName = "salesforce"
@ -30,8 +31,7 @@ oauth2Salesforce :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Salesforce = oauth2SalesforceScoped defaultScopes oauth2Salesforce = oauth2SalesforceScoped defaultScopes
oauth2SalesforceScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2SalesforceScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2SalesforceScoped = oauth2SalesforceScoped = salesforceHelper
salesforceHelper
pluginName pluginName
"https://login.salesforce.com/services/oauth2/userinfo" "https://login.salesforce.com/services/oauth2/userinfo"
"https://login.salesforce.com/services/oauth2/authorize" "https://login.salesforce.com/services/oauth2/authorize"
@ -41,43 +41,42 @@ oauth2SalesforceSandbox :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2SalesforceSandbox = oauth2SalesforceSandboxScoped defaultScopes oauth2SalesforceSandbox = oauth2SalesforceSandboxScoped defaultScopes
oauth2SalesforceSandboxScoped oauth2SalesforceSandboxScoped
:: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2SalesforceSandboxScoped = oauth2SalesforceSandboxScoped = salesforceHelper
salesforceHelper
(pluginName <> "-sandbox") (pluginName <> "-sandbox")
"https://test.salesforce.com/services/oauth2/userinfo" "https://test.salesforce.com/services/oauth2/userinfo"
"https://test.salesforce.com/services/oauth2/authorize" "https://test.salesforce.com/services/oauth2/authorize"
"https://test.salesforce.com/services/oauth2/token" "https://test.salesforce.com/services/oauth2/token"
salesforceHelper salesforceHelper
:: YesodAuth m :: YesodAuth m
=> Text => Text
-> URI -> URI -- ^ User profile
-- ^ User profile -> URI -- ^ Authorize
-> URI -> URI -- ^ Token
-- ^ Authorize -> [Text]
-> URI -> Text
-- ^ Token -> Text
-> [Text] -> AuthPlugin m
-> Text salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret
-> Text = authOAuth2 name oauth2 $ \manager token -> do
-> AuthPlugin m (User userId, userResponse) <- authGetProfile
salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret = name
authOAuth2 name oauth2 $ \manager token -> do manager
(User userId, userResponse) <- authGetProfile name manager token profileUri token
profileUri
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = userId
, credsIdent = userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
authorizeUri `withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint = tokenUri
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint = tokenUri
, oauth2RedirectUri = Nothing
}

View File

@ -1,31 +1,28 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- OAuth2 plugin for https://slack.com/ -- OAuth2 plugin for https://slack.com/
-- --
-- * Authenticates against slack -- * Authenticates against slack
-- * Uses slack user id as credentials identifier -- * Uses slack user id as credentials identifier
--
module Yesod.Auth.OAuth2.Slack module Yesod.Auth.OAuth2.Slack
( SlackScope (..) ( SlackScope(..)
, oauth2Slack , oauth2Slack
, oauth2SlackScoped , oauth2SlackScoped
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
import Network.HTTP.Client import Network.HTTP.Client
( httpLbs (httpLbs, parseUrlThrow, responseBody, setQueryString)
, parseUrlThrow
, responseBody
, setQueryString
)
import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
data SlackScope data SlackScope
= SlackBasicScope = SlackBasicScope
| SlackEmailScope | SlackEmailScope
| SlackTeamScope | SlackTeamScope
| SlackAvatarScope | SlackAvatarScope
scopeText :: SlackScope -> Text scopeText :: SlackScope -> Text
scopeText SlackBasicScope = "identity.basic" scopeText SlackBasicScope = "identity.basic"
@ -36,9 +33,9 @@ scopeText SlackAvatarScope = "identity.avatar"
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \root -> do parseJSON = withObject "User" $ \root -> do
o <- root .: "user" o <- root .: "user"
User <$> o .: "id" User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "slack" pluginName = "slack"
@ -49,35 +46,30 @@ defaultScopes = [SlackBasicScope]
oauth2Slack :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Slack :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Slack = oauth2SlackScoped defaultScopes oauth2Slack = oauth2SlackScoped defaultScopes
oauth2SlackScoped oauth2SlackScoped :: YesodAuth m => [SlackScope] -> Text -> Text -> AuthPlugin m
:: YesodAuth m => [SlackScope] -> Text -> Text -> AuthPlugin m
oauth2SlackScoped scopes clientId clientSecret = oauth2SlackScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
let param = encodeUtf8 $ atoken $ accessToken token let param = encodeUtf8 $ atoken $ accessToken token
req <- req <- setQueryString [("token", Just param)]
setQueryString [("token", Just param)] <$> parseUrlThrow "https://slack.com/api/users.identity"
<$> parseUrlThrow "https://slack.com/api/users.identity" userResponse <- responseBody <$> httpLbs req manager
userResponse <- responseBody <$> httpLbs req manager
either either
(throwIO . YesodOAuth2Exception.JSONDecodingError pluginName) (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)
( \(User userId) -> (\(User userId) -> pure Creds
pure { credsPlugin = pluginName
Creds , credsIdent = userId
{ credsPlugin = pluginName , credsExtra = setExtra token userResponse
, credsIdent = userId }
, credsExtra = setExtra token userResponse )
} $ eitherDecode userResponse
) where
$ eitherDecode userResponse oauth2 = OAuth2
where { oauthClientId = clientId
oauth2 = , oauthClientSecret = Just clientSecret
OAuth2 , oauthOAuthorizeEndpoint =
{ oauth2ClientId = clientId "https://slack.com/oauth/authorize"
, oauth2ClientSecret = clientSecret `withQuery` [scopeParam "," $ map scopeText scopes]
, oauth2AuthorizeEndpoint = , oauthAccessTokenEndpoint = "https://slack.com/api/oauth.access"
"https://slack.com/oauth/authorize" , oauthCallback = Nothing
`withQuery` [scopeParam "," $ map scopeText scopes] }
, oauth2TokenEndpoint = "https://slack.com/api/oauth.access"
, oauth2RedirectUri = Nothing
}

View File

@ -1,46 +1,44 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://spotify.com -- OAuth2 plugin for http://spotify.com
--
module Yesod.Auth.OAuth2.Spotify module Yesod.Auth.OAuth2.Spotify
( oauth2Spotify ( oauth2Spotify
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
newtype User = User Text newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "id" parseJSON = withObject "User" $ \o -> User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "spotify" pluginName = "spotify"
oauth2Spotify :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2Spotify :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2Spotify scopes clientId clientSecret = oauth2Spotify scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://api.spotify.com/v1/me"
"https://api.spotify.com/v1/me"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = userId
, credsIdent = userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
"https://accounts.spotify.com/authorize"
`withQuery` [scopeParam " " scopes]
, oauthAccessTokenEndpoint = "https://accounts.spotify.com/api/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://accounts.spotify.com/authorize"
`withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,62 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- |
--
-- OAuth2 plugin for http://twitch.tv
--
-- * Authenticates against twitch
-- * Uses twitch user id as credentials identifier
module Yesod.Auth.OAuth2.Twitch
( oauth2Twitch
, oauth2TwitchScoped
) where
import Yesod.Auth.OAuth2.Prelude
import qualified Data.Text.Encoding as T
newtype User = User Text
instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "user_id"
pluginName :: Text
pluginName = "twitch"
defaultScopes :: [Text]
defaultScopes = ["user:read:email"]
oauth2Twitch :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Twitch = oauth2TwitchScoped defaultScopes
oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2TwitchScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <-
authGetProfile
pluginName
manager
token
"https://id.twitch.tv/oauth2/validate"
pure
Creds
{ credsPlugin = pluginName
, credsIdent = userId
, credsExtra = setExtra token userResponse
}
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://id.twitch.tv/oauth2/authorize"
`withQuery` [scopeParam " " scopes]
, oauth2TokenEndpoint =
"https://id.twitch.tv/oauth2/token"
`withQuery` [ ("client_id", T.encodeUtf8 clientId)
, ("client_secret", T.encodeUtf8 clientSecret)
]
, oauth2RedirectUri = Nothing
}

View File

@ -1,14 +1,15 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://upcase.com -- OAuth2 plugin for http://upcase.com
-- --
-- * Authenticates against upcase -- * Authenticates against upcase
-- * Uses upcase user id as credentials identifier -- * Uses upcase user id as credentials identifier
--
module Yesod.Auth.OAuth2.Upcase module Yesod.Auth.OAuth2.Upcase
( oauth2Upcase ( oauth2Upcase
) where )
where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -17,35 +18,32 @@ import qualified Data.Text as T
newtype User = User Int newtype User = User Int
instance FromJSON User where instance FromJSON User where
parseJSON = withObject "User" $ \root -> do parseJSON = withObject "User" $ \root -> do
o <- root .: "user" o <- root .: "user"
User <$> o .: "id" User <$> o .: "id"
pluginName :: Text pluginName :: Text
pluginName = "upcase" pluginName = "upcase"
oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Upcase clientId clientSecret = oauth2Upcase clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "http://upcase.com/api/v1/me.json"
"http://upcase.com/api/v1/me.json"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = T.pack $ show userId
, credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint = "http://upcase.com/oauth/authorize"
, oauthAccessTokenEndpoint = "http://upcase.com/oauth/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"
, oauth2TokenEndpoint = "http://upcase.com/oauth/token"
, oauth2RedirectUri = Nothing
}

View File

@ -1,8 +1,9 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OAuth2.WordPressDotCom module Yesod.Auth.OAuth2.WordPressDotCom
( oauth2WordPressDotCom ( oauth2WordPressDotCom
) where )
where
import qualified Data.Text as T import qualified Data.Text as T
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -13,38 +14,35 @@ pluginName = "WordPress.com"
newtype WpUser = WpUser Int newtype WpUser = WpUser Int
instance FromJSON WpUser where instance FromJSON WpUser where
parseJSON = withObject "WpUser" $ \o -> WpUser <$> o .: "ID" parseJSON = withObject "WpUser" $ \o -> WpUser <$> o .: "ID"
oauth2WordPressDotCom oauth2WordPressDotCom
:: YesodAuth m :: (YesodAuth m)
=> Text => Text -- ^ Client Id
-- ^ Client Id -> Text -- ^ Client Secret
-> Text -> AuthPlugin m
-- ^ Client Secret
-> AuthPlugin m
oauth2WordPressDotCom clientId clientSecret = oauth2WordPressDotCom clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(WpUser userId, userResponse) <- (WpUser userId, userResponse) <- authGetProfile
authGetProfile pluginName
pluginName manager
manager token
token "https://public-api.wordpress.com/rest/v1/me/"
"https://public-api.wordpress.com/rest/v1/me/"
pure pure Creds
Creds { credsPlugin = pluginName
{ credsPlugin = pluginName , credsIdent = T.pack $ show userId
, credsIdent = T.pack $ show userId , credsExtra = setExtra token userResponse
, credsExtra = setExtra token userResponse }
where
oauth2 = OAuth2
{ oauthClientId = clientId
, oauthClientSecret = Just clientSecret
, oauthOAuthorizeEndpoint =
"https://public-api.wordpress.com/oauth2/authorize"
`withQuery` [scopeParam "," ["auth"]]
, oauthAccessTokenEndpoint =
"https://public-api.wordpress.com/oauth2/token"
, oauthCallback = Nothing
} }
where
oauth2 =
OAuth2
{ oauth2ClientId = clientId
, oauth2ClientSecret = clientSecret
, oauth2AuthorizeEndpoint =
"https://public-api.wordpress.com/oauth2/authorize"
`withQuery` [scopeParam "," ["auth"]]
, oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"
, oauth2RedirectUri = Nothing
}

11
stack-lts-13.2.yaml Normal file
View File

@ -0,0 +1,11 @@
---
resolver: lts-13.2
extra-deps:
- hoauth2-1.14.0@sha256:fcb4284fc78950c91d5b548317c51bd99a5ced84f4bb9e6153624b5783e4215f,5628
# Fix for weeder with stack-2
ghc-options:
"$locals":
-ddump-to-file
-ddump-hi

19
stack-lts-13.2.yaml.lock Normal file
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-1.14.0@sha256:fcb4284fc78950c91d5b548317c51bd99a5ced84f4bb9e6153624b5783e4215f,5628
pantry-tree:
size: 2046
sha256: f25e2c2c101312196159dad5a3e2a4c8f549ed2d036d9566b66786d758db7dba
original:
hackage: hoauth2-1.14.0@sha256:fcb4284fc78950c91d5b548317c51bd99a5ced84f4bb9e6153624b5783e4215f,5628
snapshots:
- completed:
size: 492864
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/2.yaml
sha256: 586534518d3e7be8617d97ea296f05f497c0b4bb006f100367d66f5c45ae6268
original: lts-13.2

8
stack-lts-16.10.yaml Normal file
View File

@ -0,0 +1,8 @@
---
resolver: lts-16.10
# Fix for weeder with stack-2
ghc-options:
"$locals":
-ddump-to-file
-ddump-hi

12
stack-lts-16.10.yaml.lock Normal file
View File

@ -0,0 +1,12 @@
# 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: []
snapshots:
- completed:
size: 532383
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/10.yaml
sha256: 469d781ab6d2a4eceed6b31b6e4ec842dcd3cd1d11577972e86902603dce24df
original: lts-16.10

1
stack-lts-17.4.yaml Normal file
View File

@ -0,0 +1 @@
resolver: lts-17.4

12
stack-lts-17.4.yaml.lock Normal file
View File

@ -0,0 +1,12 @@
# 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: []
snapshots:
- completed:
size: 563103
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/4.yaml
sha256: f11e2153044f5f71ea7b1c9398f4721f517c9bd37642ed769647b896564021f3
original: lts-17.4

View File

@ -1 +0,0 @@
resolver: lts-21.25

View File

@ -1 +0,0 @@
resolver: lts-22.44

View File

@ -1 +0,0 @@
resolver: lts-23.28

View File

@ -1 +0,0 @@
resolver: lts-24.26

View File

@ -1,4 +0,0 @@
resolver: nightly-2026-01-05
extra-deps:
- cryptonite-0.30
- yesod-auth-1.6.11.3

View File

@ -1 +0,0 @@
stack-lts24.yaml

1
stack.yaml Normal file
View File

@ -0,0 +1 @@
resolver: lts-17.4

View File

@ -1,12 +1,12 @@
# This file was autogenerated by Stack. # This file was autogenerated by Stack.
# You should not edit this file by hand. # You should not edit this file by hand.
# For more information, please see the documentation at: # For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/topics/lock_files # https://docs.haskellstack.org/en/stable/lock_files
packages: [] packages: []
snapshots: snapshots:
- completed: - completed:
sha256: d90eb1418667a225998b173817300e5ae2e1500ed03c0a9457cc2a0e78a0122a size: 563103
size: 726337 url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/4.yaml
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/26.yaml sha256: f11e2153044f5f71ea7b1c9398f4721f517c9bd37642ed769647b896564021f3
original: lts-24.26 original: lts-17.4

View File

@ -1,9 +1,8 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
module URI.ByteString.ExtensionSpec module URI.ByteString.ExtensionSpec
( spec ( spec
) where ) where
import Test.Hspec import Test.Hspec
@ -15,66 +14,65 @@ import URI.ByteString.QQ
spec :: Spec spec :: Spec
spec = do spec = do
describe "IsString Scheme" $ it "works" $ do describe "IsString Scheme" $ it "works" $ do
"https" `shouldBe` Scheme "https" "https" `shouldBe` Scheme "https"
describe "IsString Host" $ it "works" $ do describe "IsString Host" $ it "works" $ do
"example.com" `shouldBe` Host "example.com" "example.com" `shouldBe` Host "example.com"
describe "IsString URIRef Relative" $ it "works" $ do describe "IsString URIRef Relative" $ it "works" $ do
"example.com/foo?bar=baz" `shouldBe` [relativeRef|example.com/foo?bar=baz|] "example.com/foo?bar=baz"
`shouldBe` [relativeRef|example.com/foo?bar=baz|]
describe "IsString URIRef Absolute" $ it "works" $ do describe "IsString URIRef Absolute" $ it "works" $ do
"https://example.com/foo?bar=baz" "https://example.com/foo?bar=baz"
`shouldBe` [uri|https://example.com/foo?bar=baz|] `shouldBe` [uri|https://example.com/foo?bar=baz|]
describe "fromText" $ do describe "fromText" $ do
it "returns Just a URI for valid values, as the quasi-quoter would" $ do it "returns Just a URI for valid values, as the quasi-quoter would" $ do
fromText "http://example.com/foo?bar=baz" fromText "http://example.com/foo?bar=baz"
`shouldBe` Just [uri|http://example.com/foo?bar=baz|] `shouldBe` Just [uri|http://example.com/foo?bar=baz|]
it "returns Nothing for invalid values" $ do it "returns Nothing for invalid values" $ do
fromText "Oh my, what did I do?" `shouldBe` Nothing fromText "Oh my, what did I do?" `shouldBe` Nothing
describe "unsafeFromText" $ do describe "unsafeFromText" $ do
it "returns a URI for valid values, as the quasi-quoter would" $ do it "returns a URI for valid values, as the quasi-quoter would" $ do
unsafeFromText "http://example.com/foo?bar=baz" unsafeFromText "http://example.com/foo?bar=baz"
`shouldBe` [uri|http://example.com/foo?bar=baz|] `shouldBe` [uri|http://example.com/foo?bar=baz|]
it "raises for invalid values" $ do it "raises for invalid values" $ do
evaluate (unsafeFromText "Oh my, what did I do?") evaluate (unsafeFromText "Oh my, what did I do?")
`shouldThrow` errorContaining "MissingColon" `shouldThrow` errorContaining "MissingColon"
describe "toText" $ do describe "toText" $ do
it "serializes the URI to text" $ do it "serializes the URI to text" $ do
toText [uri|https://example.com/foo?bar=baz|] toText [uri|https://example.com/foo?bar=baz|]
`shouldBe` "https://example.com/foo?bar=baz" `shouldBe` "https://example.com/foo?bar=baz"
describe "fromRelative" $ do describe "fromRelative" $ do
it "makes a URI absolute with a given host" $ do it "makes a URI absolute with a given host" $ do
fromRelative "ftp" "foo.com" [relativeRef|/bar?baz=bat|] fromRelative "ftp" "foo.com" [relativeRef|/bar?baz=bat|]
`shouldBe` [uri|ftp://foo.com/bar?baz=bat|] `shouldBe` [uri|ftp://foo.com/bar?baz=bat|]
describe "withQuery" $ do describe "withQuery" $ do
it "appends a query to a URI" $ do it "appends a query to a URI" $ do
let uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar")] let uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar")]
uriWithQuery `shouldBe` [uri|http://example.com?foo=bar|] uriWithQuery `shouldBe` [uri|http://example.com?foo=bar|]
it "handles a URI with an existing query" $ do it "handles a URI with an existing query" $ do
let uriWithQuery = let uriWithQuery = [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]
[uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]
uriWithQuery `shouldBe` [uri|http://example.com?foo=bar&baz=bat|] uriWithQuery `shouldBe` [uri|http://example.com?foo=bar&baz=bat|]
-- This is arguably testing the internals of another package, but IMO -- This is arguably testing the internals of another package, but IMO
-- it's worthwhile to show that you don't (and can't) pre-sanitize when -- it's worthwhile to show that you don't (and can't) pre-sanitize when
-- using this function. -- using this function.
it "handles santization of the query" $ do it "handles santization of the query" $ do
let uriWithQuery = let uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar baz")]
[uri|http://example.com|] `withQuery` [("foo", "bar baz")]
toText uriWithQuery `shouldBe` "http://example.com?foo=bar%20baz" toText uriWithQuery `shouldBe` "http://example.com?foo=bar%20baz"
errorContaining :: String -> Selector ErrorCall errorContaining :: String -> Selector ErrorCall
errorContaining msg = (msg `isInfixOf`) . show errorContaining msg = (msg `isInfixOf`) . show

View File

@ -1,13 +1,13 @@
cabal-version: 1.18 cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.38.1. -- This file has been generated from package.yaml by hpack version 0.33.0.
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
-- --
-- hash: d595b9569ed34feddc8c41cf6f1f8cabbd8a37fa14b6afeeb24ad651ca689011 -- hash: 559101339cff93d29a5ea1439df2fa4e9d34133db852862ad86977d109053cd0
name: yesod-auth-oauth2 name: yesod-auth-oauth2
version: 0.8.0.0 version: 0.6.3.2
synopsis: OAuth 2.0 authentication plugins synopsis: OAuth 2.0 authentication plugins
description: Library to authenticate with OAuth 2.0 for Yesod web applications. description: Library to authenticate with OAuth 2.0 for Yesod web applications.
category: Web category: Web
@ -20,7 +20,7 @@ maintainer: engineering@freckle.com
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
build-type: Simple build-type: Simple
extra-doc-files: extra-source-files:
README.md README.md
CHANGELOG.md CHANGELOG.md
@ -35,13 +35,10 @@ flag example
library library
exposed-modules: exposed-modules:
Network.OAuth.OAuth2.Compat
UnliftIO.Except UnliftIO.Except
URI.ByteString.Extension URI.ByteString.Extension
Yesod.Auth.OAuth2 Yesod.Auth.OAuth2
Yesod.Auth.OAuth2.Auth0
Yesod.Auth.OAuth2.AzureAD Yesod.Auth.OAuth2.AzureAD
Yesod.Auth.OAuth2.AzureADv2
Yesod.Auth.OAuth2.BattleNet Yesod.Auth.OAuth2.BattleNet
Yesod.Auth.OAuth2.Bitbucket Yesod.Auth.OAuth2.Bitbucket
Yesod.Auth.OAuth2.ClassLink Yesod.Auth.OAuth2.ClassLink
@ -54,13 +51,11 @@ library
Yesod.Auth.OAuth2.GitLab Yesod.Auth.OAuth2.GitLab
Yesod.Auth.OAuth2.Google Yesod.Auth.OAuth2.Google
Yesod.Auth.OAuth2.Nylas Yesod.Auth.OAuth2.Nylas
Yesod.Auth.OAuth2.ORCID
Yesod.Auth.OAuth2.Prelude Yesod.Auth.OAuth2.Prelude
Yesod.Auth.OAuth2.Random Yesod.Auth.OAuth2.Random
Yesod.Auth.OAuth2.Salesforce Yesod.Auth.OAuth2.Salesforce
Yesod.Auth.OAuth2.Slack Yesod.Auth.OAuth2.Slack
Yesod.Auth.OAuth2.Spotify Yesod.Auth.OAuth2.Spotify
Yesod.Auth.OAuth2.Twitch
Yesod.Auth.OAuth2.Upcase Yesod.Auth.OAuth2.Upcase
Yesod.Auth.OAuth2.WordPressDotCom Yesod.Auth.OAuth2.WordPressDotCom
other-modules: other-modules:
@ -72,9 +67,9 @@ library
aeson >=0.6 aeson >=0.6
, base >=4.9.0.0 && <5 , base >=4.9.0.0 && <5
, bytestring >=0.9.1.4 , bytestring >=0.9.1.4
, crypton , cryptonite >=0.25
, errors , errors
, hoauth2 >=2.8.0 , hoauth2 >=1.11.0
, http-client >=0.4.0 , http-client >=0.4.0
, http-conduit >=2.0 , http-conduit >=2.0
, http-types >=0.8 , http-types >=0.8
@ -83,7 +78,6 @@ library
, mtl , mtl
, safe-exceptions , safe-exceptions
, text >=0.7 , text >=0.7
, transformers
, unliftio , unliftio
, uri-bytestring , uri-bytestring
, yesod-auth >=1.6.0 , yesod-auth >=1.6.0
@ -102,7 +96,7 @@ executable yesod-auth-oauth2-example
, aeson-pretty , aeson-pretty
, base >=4.9.0.0 && <5 , base >=4.9.0.0 && <5
, bytestring >=0.9.1.4 , bytestring >=0.9.1.4
, containers >=0.6.0.1 , containers
, http-conduit >=2.0 , http-conduit >=2.0
, load-env , load-env
, text >=0.7 , text >=0.7
@ -110,9 +104,9 @@ executable yesod-auth-oauth2-example
, yesod , yesod
, yesod-auth >=1.6.0 , yesod-auth >=1.6.0
, yesod-auth-oauth2 , yesod-auth-oauth2
default-language: Haskell2010
if !(flag(example)) if !(flag(example))
buildable: False buildable: False
default-language: Haskell2010
test-suite test test-suite test
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0