mirror of
https://github.com/freckle/yesod-auth-oauth2.git
synced 2026-02-03 14:50:26 +01:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4c3d9f049 | ||
|
|
3dd2318067 | ||
|
|
3e5dbdec77 | ||
|
|
a8de561848 | ||
|
|
a179049522 | ||
|
|
36bc61fa27 | ||
|
|
09cda079d4 | ||
|
|
560647fb01 | ||
|
|
7f6096079f | ||
|
|
4e123a9482 | ||
|
|
a916af9688 | ||
|
|
3b55cee63b | ||
|
|
fa54bc36aa | ||
|
|
e79d174821 | ||
|
|
51c6574183 | ||
|
|
50cc0ea49b | ||
|
|
56c2d0a30d | ||
|
|
624b2be5aa | ||
|
|
87a0231a6d | ||
|
|
7b0d4f6243 | ||
|
|
07c6ea6875 | ||
|
|
433e8b324b | ||
|
|
acb69f8da4 | ||
|
|
0b4f249bf4 | ||
|
|
f9f7e1b73b | ||
|
|
94ba2ebeab | ||
|
|
3624b7f2d5 | ||
|
|
8cc82e919c | ||
|
|
f968e42da6 | ||
|
|
11948a65c4 | ||
|
|
7d913b6fea | ||
|
|
d238c1f3b5 | ||
|
|
3700a89ada | ||
|
|
1aa3f29509 | ||
|
|
79a955edd0 | ||
|
|
cebba91cb0 | ||
|
|
cd3d377e83 | ||
|
|
3daf382e46 | ||
|
|
08d0f0eaa4 | ||
|
|
5d4e4f8d7b | ||
|
|
48a0ea64b2 | ||
|
|
4627cf1fdc | ||
|
|
940c0fc0a5 | ||
|
|
3a333df1ce | ||
|
|
fb1b506606 | ||
|
|
1e68d6b02c | ||
|
|
ac1e48db97 | ||
|
|
8b46e82981 | ||
|
|
15a75ff6f9 | ||
|
|
d34ed2d4b9 | ||
|
|
714467b4d1 | ||
|
|
514a59e00b | ||
|
|
33aa6f4c7b | ||
|
|
8eeca895be | ||
|
|
d34efc18ca | ||
|
|
e3730ab99c | ||
|
|
3c15ecd871 | ||
|
|
36805f0580 | ||
|
|
ab73e2fe20 |
@ -1,3 +1,4 @@
|
|||||||
|
# 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
|
||||||
@ -13,6 +14,10 @@ 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
|
||||||
|
|
||||||
|
|||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* @freckle/backenders
|
||||||
16
.github/workflows/add-asana-comment.yml.bak
vendored
Normal file
16
.github/workflows/add-asana-comment.yml.bak
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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 }}"
|
||||||
53
.github/workflows/ci.yml
vendored
53
.github/workflows/ci.yml
vendored
@ -5,40 +5,45 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: main
|
branches: main
|
||||||
|
|
||||||
jobs:
|
concurrency:
|
||||||
test:
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
runs-on: ubuntu-latest
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
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:
|
||||||
|
needs: generate
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
stack-yaml:
|
stack-yaml: ${{ fromJSON(needs.generate.outputs.stack-yamls) }}
|
||||||
- stack-nightly.yaml
|
|
||||||
- stack.yaml
|
|
||||||
- stack-lts-17.4.yaml
|
|
||||||
- stack-lts-16.10.yaml
|
|
||||||
- stack-lts-13.2.yaml
|
|
||||||
- stack-hoauth2-2.0.yaml
|
|
||||||
- stack-hoauth2-2.2.yaml
|
|
||||||
- stack-hoauth2-2.3.yaml
|
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v5
|
||||||
- uses: freckle/stack-cache-action@v2
|
- uses: freckle/stack-action@v5
|
||||||
with:
|
with:
|
||||||
stack-yaml: ${{ matrix.stack-yaml }}
|
stack-build-arguments: --flag yesod-auth-oauth2:example
|
||||||
- uses: freckle/stack-action@v3
|
env:
|
||||||
with:
|
STACK_YAML: ${{ matrix.stack-yaml }}
|
||||||
stack-yaml: ${{ matrix.stack-yaml }}
|
|
||||||
stack-arguments: --flag yesod-auth-oauth2:example
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v5
|
||||||
- run:
|
- uses: haskell-actions/hlint-setup@v2
|
||||||
curl --output .hlint.yaml https://raw.githubusercontent.com/pbrisbin/dotfiles/master/hlint.yaml
|
- uses: haskell-actions/hlint-run@v2
|
||||||
- uses: rwe/actions-hlint-setup@v1
|
|
||||||
- uses: rwe/actions-hlint-run@v2
|
|
||||||
with:
|
with:
|
||||||
fail-on: warning
|
fail-on: warning
|
||||||
|
|||||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@ -8,16 +8,15 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- id: tag
|
- id: tag
|
||||||
uses: freckle/haskell-tag-action@main
|
uses: freckle/haskell-tag-action@v1
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- if: steps.tag.outputs.tag
|
- if: steps.tag.outputs.tag
|
||||||
uses: freckle/stack-upload-action@main
|
run: stack upload --pvp-bounds lower .
|
||||||
with:
|
|
||||||
pvp-bounds: lower
|
|
||||||
env:
|
env:
|
||||||
HACKAGE_API_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }}
|
HACKAGE_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }}
|
||||||
|
|
||||||
|
# Use minimum LTS to set lowest lower bounds
|
||||||
|
STACK_YAML: stack-lts21.yaml
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
restylers:
|
restylers:
|
||||||
- brittany:
|
- fourmolu
|
||||||
include:
|
- "!stylish-haskell"
|
||||||
- "**/*.hs"
|
|
||||||
- "!src/Network/OAuth/OAuth2/Compat.hs" # CPP
|
|
||||||
- stylish-haskell:
|
|
||||||
include:
|
|
||||||
- "**/*.hs"
|
|
||||||
- "!src/Network/OAuth/OAuth2/Compat.hs" # CPP
|
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
2
.stack-all
Normal file
2
.stack-all
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[versions]
|
||||||
|
oldest = lts-21
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
52
CHANGELOG.md
52
CHANGELOG.md
@ -1,4 +1,54 @@
|
|||||||
## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.2...main)
|
## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.8.0.0...main)
|
||||||
|
|
||||||
|
## [v0.8.0.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.4.0...v0.8.0.0)
|
||||||
|
|
||||||
|
- Drop support for GHC < 9.4 and hoauth2 < 2.8
|
||||||
|
- 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)
|
## [v0.7.0.2](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.1...v0.7.0.2)
|
||||||
|
|
||||||
|
|||||||
25
README.md
25
README.md
@ -1,5 +1,10 @@
|
|||||||
# Yesod.Auth.OAuth2
|
# Yesod.Auth.OAuth2
|
||||||
|
|
||||||
|
[](https://hackage.haskell.org/package/yesod-auth-oauth2)
|
||||||
|
[](http://stackage.org/nightly/package/yesod-auth-oauth2)
|
||||||
|
[](http://stackage.org/lts/package/yesod-auth-oauth2)
|
||||||
|
[](https://github.com/pbrisbin/freckle/yesod-auth-oauth2/workflows/ci.yml)
|
||||||
|
|
||||||
OAuth2 `AuthPlugin`s for Yesod.
|
OAuth2 `AuthPlugin`s for Yesod.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -112,6 +117,26 @@ 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)
|
||||||
|
|||||||
106
example/Main.hs
106
example/Main.hs
@ -6,19 +6,6 @@
|
|||||||
{-# 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
|
||||||
@ -26,8 +13,8 @@ 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.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
|
||||||
@ -38,6 +25,7 @@ import Yesod
|
|||||||
import Yesod.Auth
|
import Yesod.Auth
|
||||||
import Yesod.Auth.OAuth2.Auth0
|
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
|
||||||
@ -46,6 +34,7 @@ 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
|
||||||
@ -58,13 +47,15 @@ data App = App
|
|||||||
, appAuthPlugins :: [AuthPlugin App]
|
, appAuthPlugins :: [AuthPlugin App]
|
||||||
}
|
}
|
||||||
|
|
||||||
mkYesod "App" [parseRoutes|
|
mkYesod
|
||||||
|
"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
|
||||||
@ -77,9 +68,9 @@ instance YesodAuth App where
|
|||||||
|
|
||||||
-- 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"
|
||||||
|
|
||||||
@ -92,23 +83,24 @@ instance RenderMessage App FormMessage where
|
|||||||
|
|
||||||
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 [whamlet|
|
defaultLayout
|
||||||
|
[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
|
||||||
@ -131,37 +123,41 @@ mkFoundation :: IO App
|
|||||||
mkFoundation = do
|
mkFoundation = do
|
||||||
loadEnv
|
loadEnv
|
||||||
|
|
||||||
auth0Host <- getEnv "AUTH0_HOST"
|
auth0Host <- getEnv "AUTH0_HOST"
|
||||||
|
azureTenant <- getEnv "AZURE_ADV2_TENANT_ID"
|
||||||
|
|
||||||
appHttpManager <- newManager tlsManagerSettings
|
appHttpManager <- newManager tlsManagerSettings
|
||||||
appAuthPlugins <- sequence
|
appAuthPlugins <-
|
||||||
|
sequence
|
||||||
-- When Providers are added, add them here and update .env.example.
|
-- When Providers are added, add them here and update .env.example.
|
||||||
-- Nothing else should need changing.
|
-- Nothing else should need changing.
|
||||||
--
|
--
|
||||||
-- FIXME: oauth2BattleNet is quite annoying!
|
-- FIXME: oauth2BattleNet is quite annoying!
|
||||||
--
|
--
|
||||||
[ loadPlugin oauth2AzureAD "AZURE_AD"
|
[ loadPlugin oauth2AzureAD "AZURE_AD"
|
||||||
, loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"
|
, loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2"
|
||||||
, loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"
|
, loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"
|
||||||
, loadPlugin oauth2Bitbucket "BITBUCKET"
|
, loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"
|
||||||
, loadPlugin oauth2ClassLink "CLASSLINK"
|
, loadPlugin oauth2Bitbucket "BITBUCKET"
|
||||||
, loadPlugin (oauth2Eve Plain) "EVE_ONLINE"
|
, loadPlugin oauth2ClassLink "CLASSLINK"
|
||||||
, loadPlugin oauth2GitHub "GITHUB"
|
, loadPlugin (oauth2Eve Plain) "EVE_ONLINE"
|
||||||
, loadPlugin oauth2GitLab "GITLAB"
|
, loadPlugin oauth2GitHub "GITHUB"
|
||||||
, loadPlugin oauth2Google "GOOGLE"
|
, loadPlugin oauth2GitLab "GITLAB"
|
||||||
, loadPlugin oauth2Nylas "NYLAS"
|
, loadPlugin oauth2Google "GOOGLE"
|
||||||
, loadPlugin oauth2Salesforce "SALES_FORCE"
|
, loadPlugin oauth2Nylas "NYLAS"
|
||||||
, loadPlugin oauth2Slack "SLACK"
|
, loadPlugin oauth2Salesforce "SALES_FORCE"
|
||||||
, loadPlugin (oauth2Spotify []) "SPOTIFY"
|
, loadPlugin oauth2Slack "SLACK"
|
||||||
, loadPlugin oauth2Twitch "TWITCH"
|
, loadPlugin (oauth2Spotify []) "SPOTIFY"
|
||||||
, loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"
|
, loadPlugin oauth2Twitch "TWITCH"
|
||||||
, loadPlugin oauth2Upcase "UPCASE"
|
, loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"
|
||||||
]
|
, loadPlugin oauth2ORCID "ORCID"
|
||||||
|
, loadPlugin oauth2Upcase "UPCASE"
|
||||||
|
]
|
||||||
|
|
||||||
return App { .. }
|
return App {..}
|
||||||
where
|
where
|
||||||
loadPlugin f prefix = do
|
loadPlugin f prefix = do
|
||||||
clientId <- getEnv $ prefix <> "_CLIENT_ID"
|
clientId <- getEnv $ prefix <> "_CLIENT_ID"
|
||||||
clientSecret <- getEnv $ prefix <> "_CLIENT_SECRET"
|
clientSecret <- getEnv $ prefix <> "_CLIENT_SECRET"
|
||||||
pure $ f (T.pack clientId) (T.pack clientSecret)
|
pure $ f (T.pack clientId) (T.pack clientSecret)
|
||||||
|
|
||||||
|
|||||||
15
fourmolu.yaml
Normal file
15
fourmolu.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: yesod-auth-oauth2
|
name: yesod-auth-oauth2
|
||||||
version: 0.7.0.2
|
version: 0.8.0.0
|
||||||
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
|
||||||
@ -27,9 +27,9 @@ library:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- aeson >=0.6
|
- aeson >=0.6
|
||||||
- bytestring >=0.9.1.4
|
- bytestring >=0.9.1.4
|
||||||
- cryptonite >=0.25
|
- crypton
|
||||||
- errors
|
- errors
|
||||||
- hoauth2 >=1.11.0
|
- hoauth2 >=2.8.0 # TokenRequestError
|
||||||
- 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
|
||||||
|
|||||||
7
renovate.json
Normal file
7
renovate.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"local>freckle/renovate-config"
|
||||||
|
],
|
||||||
|
"minimumReleaseAge": "0 days"
|
||||||
|
}
|
||||||
@ -1,154 +1,118 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
|
|
||||||
module Network.OAuth.OAuth2.Compat
|
module Network.OAuth.OAuth2.Compat
|
||||||
( OAuth2(..)
|
( OAuth2 (..)
|
||||||
, OAuth2Result
|
, authorizationUrl
|
||||||
, authorizationUrl
|
, fetchAccessTokenBasic
|
||||||
, fetchAccessToken
|
, fetchAccessTokenPost
|
||||||
, fetchAccessToken2
|
, authGetBS
|
||||||
, authGetBS
|
|
||||||
|
|
||||||
-- * Re-exports
|
-- * Re-exports
|
||||||
, module Network.OAuth.OAuth2
|
, AccessToken (..)
|
||||||
) where
|
, 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.ByteString.Lazy (ByteString)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Network.HTTP.Conduit (Manager)
|
import Network.HTTP.Conduit (Manager)
|
||||||
import Network.OAuth.OAuth2
|
|
||||||
( AccessToken(..)
|
|
||||||
, ExchangeToken(..)
|
|
||||||
, OAuth2Error
|
|
||||||
, OAuth2Token(..)
|
|
||||||
, RefreshToken(..)
|
|
||||||
)
|
|
||||||
import qualified Network.OAuth.OAuth2 as OAuth2
|
|
||||||
import Network.OAuth.OAuth2.TokenRequest (Errors)
|
|
||||||
import URI.ByteString
|
import URI.ByteString
|
||||||
|
|
||||||
#if MIN_VERSION_hoauth2(2,2,0)
|
#if MIN_VERSION_hoauth2(2,15,0)
|
||||||
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
import Network.OAuth2
|
||||||
import Data.Maybe (fromMaybe)
|
( 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
|
#endif
|
||||||
|
|
||||||
data OAuth2 = OAuth2
|
data OAuth2 = OAuth2
|
||||||
{ oauth2ClientId :: Text
|
{ oauth2ClientId :: Text
|
||||||
, oauth2ClientSecret :: Maybe Text
|
, oauth2ClientSecret :: Text
|
||||||
, oauth2AuthorizeEndpoint :: URIRef Absolute
|
, oauth2AuthorizeEndpoint :: URIRef Absolute
|
||||||
, oauth2TokenEndpoint :: URIRef Absolute
|
, oauth2TokenEndpoint :: URIRef Absolute
|
||||||
, oauth2RedirectUri :: Maybe (URIRef Absolute)
|
, oauth2RedirectUri :: Maybe (URIRef Absolute)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuth2Result err a = Either (OAuth2Error err) a
|
|
||||||
|
|
||||||
authorizationUrl :: OAuth2 -> URI
|
authorizationUrl :: OAuth2 -> URI
|
||||||
authorizationUrl = OAuth2.authorizationUrl . getOAuth2
|
authorizationUrl = OAuth2.authorizationUrl . getOAuth2
|
||||||
|
|
||||||
fetchAccessToken
|
|
||||||
:: Manager
|
|
||||||
-> OAuth2
|
|
||||||
-> ExchangeToken
|
|
||||||
-> IO (OAuth2Result Errors OAuth2Token)
|
|
||||||
fetchAccessToken = fetchAccessTokenBasic
|
|
||||||
|
|
||||||
fetchAccessToken2
|
|
||||||
:: Manager
|
|
||||||
-> OAuth2
|
|
||||||
-> ExchangeToken
|
|
||||||
-> IO (OAuth2Result Errors OAuth2Token)
|
|
||||||
fetchAccessToken2 = fetchAccessTokenPost
|
|
||||||
|
|
||||||
authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString)
|
|
||||||
authGetBS m a u = runOAuth2 $ OAuth2.authGetBS m a u
|
|
||||||
|
|
||||||
-- Normalize the rename of record fields at hoauth2-2.0. Our type is the newer
|
|
||||||
-- names and we up-convert if hoauth2-1.x is in use. getClientSecret and
|
|
||||||
-- getRedirectUri handle the differences in hoauth2-2.2 and 2.3.
|
|
||||||
|
|
||||||
#if MIN_VERSION_hoauth2(2,0,0)
|
|
||||||
getOAuth2 :: OAuth2 -> OAuth2.OAuth2
|
|
||||||
getOAuth2 o = OAuth2.OAuth2
|
|
||||||
{ OAuth2.oauth2ClientId = oauth2ClientId o
|
|
||||||
, OAuth2.oauth2ClientSecret = getClientSecret $ oauth2ClientSecret o
|
|
||||||
, OAuth2.oauth2AuthorizeEndpoint = oauth2AuthorizeEndpoint o
|
|
||||||
, OAuth2.oauth2TokenEndpoint = oauth2TokenEndpoint o
|
|
||||||
, OAuth2.oauth2RedirectUri = getRedirectUri $ oauth2RedirectUri o
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
getOAuth2 :: OAuth2 -> OAuth2.OAuth2
|
|
||||||
getOAuth2 o = OAuth2.OAuth2
|
|
||||||
{ OAuth2.oauthClientId = oauth2ClientId o
|
|
||||||
, OAuth2.oauthClientSecret = getClientSecret $ oauth2ClientSecret o
|
|
||||||
, OAuth2.oauthOAuthorizeEndpoint = oauth2AuthorizeEndpoint o
|
|
||||||
, OAuth2.oauthAccessTokenEndpoint = oauth2TokenEndpoint o
|
|
||||||
, OAuth2.oauthCallback = getRedirectUri $ oauth2RedirectUri o
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
-- hoauth2-2.2 made oauth2ClientSecret non-Maybe, after 2.0 had just made it
|
|
||||||
-- Maybe so we have to adjust, twice. TODO: change ours type to non-Maybe (major
|
|
||||||
-- bump) and reverse this to up-convert with Just in pre-2.2.
|
|
||||||
|
|
||||||
#if MIN_VERSION_hoauth2(2,2,0)
|
|
||||||
getClientSecret :: Maybe Text -> Text
|
|
||||||
getClientSecret =
|
|
||||||
fromMaybe $ error "Cannot use OAuth2.oauth2ClientSecret with Nothing"
|
|
||||||
#else
|
|
||||||
getClientSecret :: Maybe Text -> Maybe Text
|
|
||||||
getClientSecret = id
|
|
||||||
#endif
|
|
||||||
|
|
||||||
-- hoauth2-2.3 then made oauth2RedirectUri non-Maybe too. We logically rely on
|
|
||||||
-- instantiating with Nothing at definition-time, then setting it to the
|
|
||||||
-- callback at use-time, which means we can't just change our type and invert
|
|
||||||
-- this shim; we'll have to do something much more pervasive to avoid this
|
|
||||||
-- fromMaybe.
|
|
||||||
|
|
||||||
#if MIN_VERSION_hoauth2(2,3,0)
|
|
||||||
getRedirectUri :: Maybe (URIRef Absolute) -> (URIRef Absolute)
|
|
||||||
getRedirectUri =
|
|
||||||
fromMaybe $ error "Cannot use OAuth2.oauth2RedirectUri with Nothing"
|
|
||||||
#else
|
|
||||||
getRedirectUri :: Maybe (URIRef Absolute) -> Maybe (URIRef Absolute)
|
|
||||||
getRedirectUri = id
|
|
||||||
#endif
|
|
||||||
|
|
||||||
-- hoauth-2.2 moved most IO-Either functions to ExceptT. This reverses that.
|
|
||||||
|
|
||||||
#if MIN_VERSION_hoauth2(2,2,0)
|
|
||||||
runOAuth2 :: ExceptT e m a -> m (Either e a)
|
|
||||||
runOAuth2 = runExceptT
|
|
||||||
#else
|
|
||||||
runOAuth2 :: IO (Either e a) -> IO (Either e a)
|
|
||||||
runOAuth2 = id
|
|
||||||
#endif
|
|
||||||
|
|
||||||
-- The fetchAccessToken functions grew a nicer interface in hoauth2-2.3. This
|
|
||||||
-- up-converts the older ones. We should update our code to use these functions
|
|
||||||
-- directly.
|
|
||||||
|
|
||||||
fetchAccessTokenBasic
|
fetchAccessTokenBasic
|
||||||
:: Manager
|
:: Manager
|
||||||
-> OAuth2
|
-> OAuth2
|
||||||
-> ExchangeToken
|
-> ExchangeToken
|
||||||
-> IO (OAuth2Result Errors OAuth2Token)
|
-> IO (Either TokenResponseError TokenResponse)
|
||||||
fetchAccessTokenBasic m o e = runOAuth2 $ f m (getOAuth2 o) e
|
fetchAccessTokenBasic =
|
||||||
where
|
runFetchAccessToken OAuth2.ClientSecretBasic
|
||||||
#if MIN_VERSION_hoauth2(2,3,0)
|
|
||||||
f = OAuth2.fetchAccessTokenInternal OAuth2.ClientSecretBasic
|
|
||||||
#else
|
|
||||||
f = OAuth2.fetchAccessToken
|
|
||||||
#endif
|
|
||||||
|
|
||||||
fetchAccessTokenPost
|
fetchAccessTokenPost
|
||||||
:: Manager
|
:: Manager
|
||||||
-> OAuth2
|
-> OAuth2
|
||||||
-> ExchangeToken
|
-> ExchangeToken
|
||||||
-> IO (OAuth2Result Errors OAuth2Token)
|
-> IO (Either TokenResponseError TokenResponse)
|
||||||
fetchAccessTokenPost m o e = runOAuth2 $ f m (getOAuth2 o) e
|
fetchAccessTokenPost =
|
||||||
where
|
runFetchAccessToken OAuth2.ClientSecretPost
|
||||||
#if MIN_VERSION_hoauth2(2,3,0)
|
|
||||||
f = OAuth2.fetchAccessTokenInternal OAuth2.ClientSecretPost
|
authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString)
|
||||||
#else
|
authGetBS m a u = runExceptT $ OAuth2.authGetBS m a u
|
||||||
f = OAuth2.fetchAccessToken2
|
|
||||||
#endif
|
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
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
{-# 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
|
||||||
@ -13,30 +14,26 @@ 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 = either (error . show) id
|
fromString =
|
||||||
. parseURI strictURIParserOptions
|
either (error . show) id . parseURI strictURIParserOptions . C8.pack
|
||||||
. C8.pack
|
|
||||||
|
|
||||||
instance IsString (URIRef Relative) where
|
instance IsString (URIRef Relative) where
|
||||||
fromString = either (error . show) id
|
fromString =
|
||||||
. parseRelativeRef strictURIParserOptions
|
either (error . show) id . parseRelativeRef strictURIParserOptions . C8.pack
|
||||||
. C8.pack
|
|
||||||
|
|
||||||
fromText :: Text -> Maybe URI
|
fromText :: Text -> Maybe URI
|
||||||
fromText = either (const Nothing) Just
|
fromText =
|
||||||
. parseURI strictURIParserOptions
|
either (const Nothing) Just . parseURI strictURIParserOptions . encodeUtf8
|
||||||
. encodeUtf8
|
|
||||||
|
|
||||||
unsafeFromText :: Text -> URI
|
unsafeFromText :: Text -> URI
|
||||||
unsafeFromText = either (error . show) id
|
unsafeFromText =
|
||||||
. parseURI strictURIParserOptions
|
either (error . show) id . parseURI strictURIParserOptions . encodeUtf8
|
||||||
. encodeUtf8
|
|
||||||
|
|
||||||
toText :: URI -> Text
|
toText :: URI -> Text
|
||||||
toText = decodeUtf8 . serializeURIRef'
|
toText = decodeUtf8 . serializeURIRef'
|
||||||
@ -45,9 +42,12 @@ 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 = u & authorityL %~ maybe
|
withHost u h =
|
||||||
(Just $ Authority Nothing h Nothing)
|
u
|
||||||
(\a -> Just $ a & authorityHostL .~ h)
|
& authorityL
|
||||||
|
%~ 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
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||||
|
|
||||||
module UnliftIO.Except
|
module UnliftIO.Except () where
|
||||||
() where
|
|
||||||
|
|
||||||
import Control.Monad.Except
|
import Control.Monad ((<=<))
|
||||||
|
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))
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
{-# 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
|
||||||
, OAuth2Token(..)
|
, TokenResponse
|
||||||
, Creds(..)
|
, Creds (..)
|
||||||
, oauth2Url
|
, oauth2Url
|
||||||
, authOAuth2
|
, authOAuth2
|
||||||
, authOAuth2Widget
|
, authOAuth2Widget
|
||||||
|
|
||||||
-- * Alternatives that use 'fetchAccessToken2'
|
-- * Alternatives that use 'fetchAccessTokenPost'
|
||||||
, authOAuth2'
|
, authOAuth2'
|
||||||
, authOAuth2Widget'
|
, authOAuth2Widget'
|
||||||
|
|
||||||
@ -46,14 +46,12 @@ 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 'fetchAccessToken2'
|
-- | A version of 'authOAuth2' that uses 'fetchAccessTokenPost'
|
||||||
--
|
--
|
||||||
-- 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
|
||||||
|
|
||||||
@ -61,7 +59,6 @@ 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 ()
|
||||||
@ -69,12 +66,11 @@ authOAuth2Widget
|
|||||||
-> OAuth2
|
-> OAuth2
|
||||||
-> FetchCreds m
|
-> FetchCreds m
|
||||||
-> AuthPlugin m
|
-> AuthPlugin m
|
||||||
authOAuth2Widget = buildPlugin fetchAccessToken
|
authOAuth2Widget = buildPlugin fetchAccessTokenBasic
|
||||||
|
|
||||||
-- | A version of 'authOAuth2Widget' that uses 'fetchAccessToken2'
|
-- | A version of 'authOAuth2Widget' that uses 'fetchAccessTokenPost'
|
||||||
--
|
--
|
||||||
-- 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 ()
|
||||||
@ -82,7 +78,7 @@ authOAuth2Widget'
|
|||||||
-> OAuth2
|
-> OAuth2
|
||||||
-> FetchCreds m
|
-> FetchCreds m
|
||||||
-> AuthPlugin m
|
-> AuthPlugin m
|
||||||
authOAuth2Widget' = buildPlugin fetchAccessToken2
|
authOAuth2Widget' = buildPlugin fetchAccessTokenPost
|
||||||
|
|
||||||
buildPlugin
|
buildPlugin
|
||||||
:: YesodAuth m
|
:: YesodAuth m
|
||||||
@ -92,11 +88,13 @@ buildPlugin
|
|||||||
-> OAuth2
|
-> OAuth2
|
||||||
-> FetchCreds m
|
-> FetchCreds m
|
||||||
-> AuthPlugin m
|
-> AuthPlugin m
|
||||||
buildPlugin getToken widget name oauth getCreds = AuthPlugin
|
buildPlugin getToken widget name oauth getCreds =
|
||||||
name
|
AuthPlugin
|
||||||
(dispatchAuthRequest name oauth getToken getCreds)
|
name
|
||||||
login
|
(dispatchAuthRequest name oauth getToken getCreds)
|
||||||
where login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]
|
login
|
||||||
|
where
|
||||||
|
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
|
||||||
@ -105,7 +103,6 @@ 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
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- OAuth2 plugin for <https://auth0.com>
|
-- OAuth2 plugin for <https://auth0.com>
|
||||||
--
|
--
|
||||||
-- * Authenticates against specific auth0 tenant
|
-- * 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
|
-- * 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
|
module Yesod.Auth.OAuth2.Auth0
|
||||||
( oauth2Auth0HostScopes
|
( oauth2Auth0HostScopes
|
||||||
, oauth2Auth0Host
|
, oauth2Auth0Host
|
||||||
@ -13,8 +13,8 @@ module Yesod.Auth.OAuth2.Auth0
|
|||||||
|
|
||||||
import Data.Aeson as Aeson
|
import Data.Aeson as Aeson
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Prelude
|
|
||||||
import Yesod.Auth.OAuth2.Prelude
|
import Yesod.Auth.OAuth2.Prelude
|
||||||
|
import Prelude
|
||||||
|
|
||||||
-- | https://auth0.com/docs/api/authentication#get-user-info
|
-- | https://auth0.com/docs/api/authentication#get-user-info
|
||||||
newtype User = User T.Text
|
newtype User = User T.Text
|
||||||
@ -36,21 +36,25 @@ oauth2Auth0HostScopes
|
|||||||
:: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m
|
:: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m
|
||||||
oauth2Auth0HostScopes host scopes clientId clientSecret =
|
oauth2Auth0HostScopes host scopes clientId clientSecret =
|
||||||
authOAuth2 pluginName oauth2 $ \manager token -> do
|
authOAuth2 pluginName oauth2 $ \manager token -> do
|
||||||
(User uid, userResponse) <- authGetProfile pluginName
|
(User uid, userResponse) <-
|
||||||
manager
|
authGetProfile
|
||||||
token
|
pluginName
|
||||||
(host `withPath` "/userinfo")
|
manager
|
||||||
pure Creds { credsPlugin = pluginName
|
token
|
||||||
, credsIdent = uid
|
(host `withPath` "/userinfo")
|
||||||
, credsExtra = setExtra token userResponse
|
pure
|
||||||
}
|
Creds
|
||||||
|
{ credsPlugin = pluginName
|
||||||
|
, credsIdent = uid
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = host
|
, oauth2ClientSecret = clientSecret
|
||||||
`withPath` "/authorize"
|
, oauth2AuthorizeEndpoint =
|
||||||
`withQuery` [scopeParam " " scopes]
|
host `withPath` "/authorize" `withQuery` [scopeParam " " scopes]
|
||||||
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
|
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2RedirectUri = Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
{-# 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 Prelude
|
|
||||||
import Yesod.Auth.OAuth2.Prelude
|
import Yesod.Auth.OAuth2.Prelude
|
||||||
|
import Prelude
|
||||||
|
|
||||||
newtype User = User Text
|
newtype User = User Text
|
||||||
|
|
||||||
@ -31,25 +31,29 @@ 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) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://graph.microsoft.com/v1.0/me"
|
token
|
||||||
|
"https://graph.microsoft.com/v1.0/me"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint =
|
, oauth2ClientSecret = clientSecret
|
||||||
"https://login.windows.net/common/oauth2/authorize"
|
, oauth2AuthorizeEndpoint =
|
||||||
`withQuery` [ scopeParam "," scopes
|
"https://login.windows.net/common/oauth2/authorize"
|
||||||
, ("resource", "https://graph.microsoft.com")
|
`withQuery` [ scopeParam "," scopes
|
||||||
]
|
, ("resource", "https://graph.microsoft.com")
|
||||||
, oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"
|
]
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"
|
||||||
}
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
104
src/Yesod/Auth/OAuth2/AzureADv2.hs
Normal file
104
src/Yesod/Auth/OAuth2/AzureADv2.hs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{-# 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
|
||||||
@ -7,7 +7,6 @@
|
|||||||
-- * 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
|
||||||
@ -28,38 +27,44 @@ pluginName = "battle.net"
|
|||||||
|
|
||||||
oauth2BattleNet
|
oauth2BattleNet
|
||||||
:: YesodAuth m
|
:: YesodAuth m
|
||||||
=> WidgetFor m () -- ^ Login widget
|
=> WidgetFor m ()
|
||||||
-> Text -- ^ User region (e.g. "eu", "cn", "us")
|
-- ^ Login widget
|
||||||
-> Text -- ^ Client ID
|
-> Text
|
||||||
-> Text -- ^ Client Secret
|
-- ^ User region (e.g. "eu", "cn", "us")
|
||||||
|
-> Text
|
||||||
|
-- ^ Client ID
|
||||||
|
-> Text
|
||||||
|
-- ^ Client Secret
|
||||||
-> AuthPlugin m
|
-> 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 Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = T.pack $ show userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
host = wwwHost $ T.toLower region
|
host = wwwHost $ T.toLower region
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
, oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"
|
, oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"
|
||||||
, oauth2RedirectUri = Nothing
|
, 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"
|
||||||
apiHost region = Host $ encodeUtf8 $ region <> ".api.battle.net"
|
apiHost region = Host $ encodeUtf8 $ region <> ".api.battle.net"
|
||||||
|
|
||||||
wwwHost :: Text -> Host
|
wwwHost :: Text -> Host
|
||||||
wwwHost "cn" = "www.battlenet.com.cn"
|
wwwHost "cn" = "www.battlenet.com.cn"
|
||||||
wwwHost region = Host $ encodeUtf8 $ region <> ".battle.net"
|
wwwHost region = Host $ encodeUtf8 $ region <> ".battle.net"
|
||||||
|
|
||||||
oAuth2BattleNet
|
oAuth2BattleNet
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{-# 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
|
||||||
@ -32,28 +32,33 @@ 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) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://api.bitbucket.com/2.0/user"
|
token
|
||||||
|
"https://api.bitbucket.com/2.0/user"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
-- FIXME: Preserved bug. This should just be userId (it's already
|
Creds
|
||||||
-- a Text), but because this code was shipped, folks likely have
|
{ credsPlugin = pluginName
|
||||||
-- Idents in their database like @"\"...\""@, and if we fixed this
|
, -- FIXME: Preserved bug. This should just be userId (it's already
|
||||||
-- they would need migrating. We're keeping it for now as it's a
|
-- a Text), but because this code was shipped, folks likely have
|
||||||
-- minor wart. Breaking typed APIs is one thing, causing data to go
|
-- Idents in their database like @"\"...\""@, and if we fixed this
|
||||||
-- invalid is another.
|
-- they would need migrating. We're keeping it for now as it's a
|
||||||
, credsIdent = T.pack $ show userId
|
-- minor wart. Breaking typed APIs is one thing, causing data to go
|
||||||
, credsExtra = setExtra token userResponse
|
-- invalid is another.
|
||||||
}
|
credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://bitbucket.com/site/oauth2/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [scopeParam "," scopes]
|
, oauth2AuthorizeEndpoint =
|
||||||
, oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"
|
"https://bitbucket.com/site/oauth2/authorize"
|
||||||
, oauth2RedirectUri = Nothing
|
`withQuery` [scopeParam "," scopes]
|
||||||
}
|
, oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"
|
||||||
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -26,22 +26,27 @@ 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) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://nodeapi.classlink.com/v2/my/info"
|
token
|
||||||
|
"https://nodeapi.classlink.com/v2/my/info"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = T.pack $ show userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://launchpad.classlink.com/oauth2/v2/auth"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [scopeParam "," scopes]
|
, oauth2AuthorizeEndpoint =
|
||||||
, oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"
|
"https://launchpad.classlink.com/oauth2/v2/auth"
|
||||||
, oauth2RedirectUri = Nothing
|
`withQuery` [scopeParam "," scopes]
|
||||||
}
|
, oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"
|
||||||
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -6,19 +6,19 @@
|
|||||||
|
|
||||||
module Yesod.Auth.OAuth2.Dispatch
|
module Yesod.Auth.OAuth2.Dispatch
|
||||||
( FetchToken
|
( FetchToken
|
||||||
, fetchAccessToken
|
, fetchAccessTokenBasic
|
||||||
, fetchAccessToken2
|
, fetchAccessTokenPost
|
||||||
, FetchCreds
|
, FetchCreds
|
||||||
, dispatchAuthRequest
|
, dispatchAuthRequest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad.Except
|
import Control.Monad (unless)
|
||||||
|
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.Compat
|
||||||
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,21 +30,29 @@ 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 =
|
||||||
type FetchToken
|
Manager
|
||||||
= Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)
|
-> OAuth2
|
||||||
|
-> 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 -> OAuth2Token -> IO (Creds m)
|
type FetchCreds m = Manager -> TokenResponse -> IO (Creds m)
|
||||||
|
|
||||||
-- | Dispatch the various OAuth2 handshake routes
|
-- | Dispatch the various OAuth2 handshake routes
|
||||||
dispatchAuthRequest
|
dispatchAuthRequest
|
||||||
:: Text -- ^ Name
|
:: Text
|
||||||
-> OAuth2 -- ^ Service details
|
-- ^ Name
|
||||||
-> FetchToken -- ^ How to get a token
|
-> OAuth2
|
||||||
-> FetchCreds m -- ^ How to get credentials
|
-- ^ Service details
|
||||||
-> Text -- ^ Method
|
-> FetchToken
|
||||||
-> [Text] -- ^ Path pieces
|
-- ^ How to get a token
|
||||||
|
-> FetchCreds m
|
||||||
|
-- ^ How to get credentials
|
||||||
|
-> Text
|
||||||
|
-- ^ Method
|
||||||
|
-> [Text]
|
||||||
|
-- ^ Path pieces
|
||||||
-> AuthHandler m TypedContent
|
-> AuthHandler m TypedContent
|
||||||
dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] =
|
dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] =
|
||||||
handleDispatchError $ dispatchForward name oauth2
|
handleDispatchError $ dispatchForward name oauth2
|
||||||
@ -56,14 +64,13 @@ dispatchAuthRequest _ _ _ _ _ _ = notFound
|
|||||||
--
|
--
|
||||||
-- 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'
|
||||||
|
|
||||||
@ -72,7 +79,6 @@ dispatchForward name oauth2 = do
|
|||||||
-- 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
|
||||||
@ -82,16 +88,17 @@ dispatchCallback
|
|||||||
-> 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 <- either (throwError . OAuth2ResultError) pure
|
token <-
|
||||||
=<< liftIO (getToken manager oauth2' $ ExchangeToken code)
|
either (throwError . OAuth2ResultError) pure
|
||||||
|
=<< 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
|
||||||
@ -101,41 +108,47 @@ withCallbackAndState
|
|||||||
-> Text
|
-> Text
|
||||||
-> m OAuth2
|
-> m OAuth2
|
||||||
withCallbackAndState name oauth2 csrf = do
|
withCallbackAndState name oauth2 csrf = do
|
||||||
uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender
|
callback <- maybe defaultCallback pure $ oauth2RedirectUri oauth2
|
||||||
callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri
|
pure
|
||||||
pure oauth2
|
oauth2
|
||||||
{ oauth2RedirectUri = Just callback
|
{ oauth2RedirectUri = Just callback
|
||||||
, oauth2AuthorizeEndpoint = oauth2AuthorizeEndpoint oauth2
|
, oauth2AuthorizeEndpoint =
|
||||||
`withQuery` [("state", encodeUtf8 csrf)]
|
oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)]
|
||||||
}
|
}
|
||||||
|
where
|
||||||
|
defaultCallback = do
|
||||||
|
uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender
|
||||||
|
maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri
|
||||||
|
|
||||||
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, ~30-character value in the session
|
-- | Set a random, ~64-byte 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 less than 30 characters.
|
-- return slightly fewer than 64 bytes.
|
||||||
--
|
|
||||||
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 randomToken = T.filter (/= '+') <$> randomText 64
|
where
|
||||||
|
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 <$ unless (sessionToken == Just token)
|
token
|
||||||
(throwError $ InvalidStateToken sessionToken token)
|
<$ unless
|
||||||
|
(sessionToken == Just token)
|
||||||
|
(throwError $ InvalidStateToken sessionToken token)
|
||||||
|
|
||||||
requireGetParam
|
requireGetParam
|
||||||
:: (MonadError DispatchError m, MonadHandler m) => Text -> m Text
|
:: (MonadError DispatchError m, MonadHandler m) => Text -> m Text
|
||||||
|
|||||||
@ -9,15 +9,14 @@
|
|||||||
{-# 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
|
import Network.OAuth.OAuth2.Compat (TokenResponseError)
|
||||||
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)
|
||||||
@ -27,55 +26,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 (OAuth2Error Errors)
|
| OAuth2ResultError TokenResponseError
|
||||||
| 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 message = dispatchErrorMessage err <> suffix
|
let
|
||||||
messageValue =
|
message = dispatchErrorMessage err <> suffix
|
||||||
object ["error" .= object ["id" .= errorId, "message" .= message]]
|
messageValue =
|
||||||
|
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
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
{-# 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)
|
||||||
@ -18,58 +17,54 @@ 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 = ErrorResponse
|
unknownError x =
|
||||||
{ erName = Unknown x
|
ErrorResponse {erName = Unknown x, erDescription = Nothing, erURI = Nothing}
|
||||||
, 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
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
{-# 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
|
||||||
@ -24,23 +24,24 @@ instance FromJSON User where
|
|||||||
parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash"
|
parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash"
|
||||||
|
|
||||||
data WidgetType m
|
data WidgetType m
|
||||||
= Plain -- ^ Simple "Login via eveonline" text
|
= -- | Simple "Login via eveonline" text
|
||||||
| BigWhite
|
Plain
|
||||||
| SmallWhite
|
| BigWhite
|
||||||
| BigBlack
|
| SmallWhite
|
||||||
| SmallBlack
|
| BigBlack
|
||||||
| Custom (WidgetFor m ())
|
| SmallBlack
|
||||||
|
| 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
|
||||||
@ -57,25 +58,28 @@ oauth2EveScoped
|
|||||||
oauth2EveScoped scopes widgetType clientId clientSecret =
|
oauth2EveScoped scopes widgetType clientId clientSecret =
|
||||||
authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token ->
|
authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token ->
|
||||||
do
|
do
|
||||||
(User userId, userResponse) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://login.eveonline.com/oauth/verify"
|
token
|
||||||
|
"https://login.eveonline.com/oauth/verify"
|
||||||
|
|
||||||
pure Creds { credsPlugin = "eveonline"
|
pure
|
||||||
-- FIXME: Preserved bug. See similar comment in Bitbucket provider.
|
Creds
|
||||||
, credsIdent = T.pack $ show userId
|
{ credsPlugin = "eveonline"
|
||||||
, credsExtra = setExtra token userResponse
|
, -- FIXME: Preserved bug. See similar comment in Bitbucket provider.
|
||||||
}
|
credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://login.eveonline.com/oauth/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [ ("response_type", "code")
|
, oauth2AuthorizeEndpoint =
|
||||||
, scopeParam " " scopes
|
"https://login.eveonline.com/oauth/authorize"
|
||||||
]
|
`withQuery` [("response_type", "code"), scopeParam " " scopes]
|
||||||
, oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"
|
, oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2RedirectUri = Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,24 @@
|
|||||||
{-# 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
|
||||||
= OAuth2Error Text ByteString
|
= -- | HTTP error during OAuth2 handshake
|
||||||
-- ^ HTTP error during OAuth2 handshake
|
|
||||||
--
|
--
|
||||||
-- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.
|
-- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.
|
||||||
--
|
OAuth2Error Text ByteString
|
||||||
| JSONDecodingError Text String
|
| -- | User profile was not as expected
|
||||||
-- ^ User profile was not as expected
|
|
||||||
--
|
--
|
||||||
-- Plugin name and Aeson parse error message.
|
-- Plugin name and Aeson parse error message.
|
||||||
--
|
JSONDecodingError Text String
|
||||||
| GenericError Text String
|
| -- | Other error conditions
|
||||||
-- ^ Other error conditions
|
|
||||||
--
|
--
|
||||||
-- Plugin name and error message.
|
-- Plugin name and error message.
|
||||||
--
|
GenericError Text String
|
||||||
deriving (Show, Typeable)
|
deriving (Show)
|
||||||
|
|
||||||
instance Exception YesodOAuth2Exception
|
instance Exception YesodOAuth2Exception
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
{-# 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
|
||||||
|
|
||||||
@ -29,25 +32,39 @@ defaultScopes = ["user:email"]
|
|||||||
oauth2GitHub :: YesodAuth m => Text -> Text -> AuthPlugin m
|
oauth2GitHub :: YesodAuth m => Text -> Text -> AuthPlugin m
|
||||||
oauth2GitHub = oauth2GitHubScoped defaultScopes
|
oauth2GitHub = oauth2GitHubScoped defaultScopes
|
||||||
|
|
||||||
oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
|
oauth2GitHubWidget
|
||||||
oauth2GitHubScoped scopes clientId clientSecret =
|
:: YesodAuth m => WidgetFor m () -> Text -> Text -> AuthPlugin m
|
||||||
authOAuth2 pluginName oauth2 $ \manager token -> do
|
oauth2GitHubWidget widget = oauth2GitHubScopedWidget widget defaultScopes
|
||||||
(User userId, userResponse) <- authGetProfile
|
|
||||||
pluginName
|
|
||||||
manager
|
|
||||||
token
|
|
||||||
"https://api.github.com/user"
|
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
|
||||||
, credsIdent = T.pack $ show userId
|
oauth2GitHubScoped =
|
||||||
, credsExtra = setExtra token userResponse
|
oauth2GitHubScopedWidget [whamlet|Login via #{pluginName}|]
|
||||||
}
|
|
||||||
|
oauth2GitHubScopedWidget
|
||||||
|
:: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m
|
||||||
|
oauth2GitHubScopedWidget widget scopes clientId clientSecret =
|
||||||
|
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do
|
||||||
|
(User userId, userResponse) <-
|
||||||
|
authGetProfile
|
||||||
|
pluginName
|
||||||
|
manager
|
||||||
|
token
|
||||||
|
"https://api.github.com/user"
|
||||||
|
|
||||||
|
pure
|
||||||
|
Creds
|
||||||
|
{ credsPlugin = pluginName
|
||||||
|
, credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://github.com/login/oauth/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [scopeParam "," scopes]
|
, oauth2AuthorizeEndpoint =
|
||||||
, oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"
|
"https://github.com/login/oauth/authorize"
|
||||||
, oauth2RedirectUri = Nothing
|
`withQuery` [scopeParam "," scopes]
|
||||||
}
|
, oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"
|
||||||
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Yesod.Auth.OAuth2.GitLab
|
module Yesod.Auth.OAuth2.GitLab
|
||||||
( oauth2GitLab
|
( oauth2GitLab
|
||||||
, oauth2GitLabHostScopes
|
, oauth2GitLabHostScopes
|
||||||
@ -32,7 +33,6 @@ 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
|
||||||
|
|
||||||
@ -43,17 +43,19 @@ oauth2GitLabHostScopes host scopes clientId clientSecret =
|
|||||||
(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 Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = T.pack $ show userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = host
|
, oauth2ClientSecret = clientSecret
|
||||||
`withPath` "/oauth/authorize"
|
, oauth2AuthorizeEndpoint =
|
||||||
`withQuery` [scopeParam " " scopes]
|
host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes]
|
||||||
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
|
, oauth2TokenEndpoint = host `withPath` "/oauth/token"
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2RedirectUri = Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
--
|
--
|
||||||
-- OAuth2 plugin for http://www.google.com
|
-- OAuth2 plugin for http://www.google.com
|
||||||
@ -23,7 +24,6 @@
|
|||||||
-- > 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
|
||||||
@ -38,9 +38,9 @@ newtype User = User Text
|
|||||||
|
|
||||||
instance FromJSON User where
|
instance FromJSON User where
|
||||||
parseJSON =
|
parseJSON =
|
||||||
withObject "User" $ \o -> User
|
withObject "User" $ \o ->
|
||||||
-- Required for data backwards-compatibility
|
-- Required for data backwards-compatibility
|
||||||
<$> (("google-uid:" <>) <$> o .: "sub")
|
User . ("google-uid:" <>) <$> o .: "sub"
|
||||||
|
|
||||||
pluginName :: Text
|
pluginName :: Text
|
||||||
pluginName = "google"
|
pluginName = "google"
|
||||||
@ -63,22 +63,27 @@ 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) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://www.googleapis.com/oauth2/v3/userinfo"
|
token
|
||||||
|
"https://www.googleapis.com/oauth2/v3/userinfo"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [scopeParam " " scopes]
|
, oauth2AuthorizeEndpoint =
|
||||||
, oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"
|
"https://accounts.google.com/o/oauth2/auth"
|
||||||
, oauth2RedirectUri = Nothing
|
`withQuery` [scopeParam " " scopes]
|
||||||
}
|
, oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"
|
||||||
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -26,41 +26,45 @@ 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 <- applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""
|
req <-
|
||||||
<$> parseRequest "https://api.nylas.com/account"
|
applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""
|
||||||
|
<$> 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) -> pure Creds { credsPlugin = pluginName
|
( \(User userId) ->
|
||||||
, credsIdent = userId
|
pure
|
||||||
, credsExtra = setExtra token userResponse
|
Creds
|
||||||
}
|
{ credsPlugin = pluginName
|
||||||
)
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
|
)
|
||||||
$ eitherDecode userResponse
|
$ eitherDecode userResponse
|
||||||
where
|
where
|
||||||
oauth = OAuth2
|
oauth =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "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
|
||||||
]
|
]
|
||||||
, oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"
|
, oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2RedirectUri = Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/Yesod/Auth/OAuth2/ORCID.hs
Normal file
50
src/Yesod/Auth/OAuth2/ORCID.hs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{-# 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
|
||||||
|
}
|
||||||
@ -1,14 +1,12 @@
|
|||||||
{-# 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
|
||||||
-- * Provider helpers
|
|
||||||
authGetProfile
|
|
||||||
, scopeParam
|
, scopeParam
|
||||||
, setExtra
|
, setExtra
|
||||||
|
|
||||||
@ -22,8 +20,8 @@ module Yesod.Auth.OAuth2.Prelude
|
|||||||
, (.:?)
|
, (.:?)
|
||||||
, (.=)
|
, (.=)
|
||||||
, (<>)
|
, (<>)
|
||||||
, FromJSON(..)
|
, FromJSON (..)
|
||||||
, ToJSON(..)
|
, ToJSON (..)
|
||||||
, eitherDecode
|
, eitherDecode
|
||||||
, withObject
|
, withObject
|
||||||
|
|
||||||
@ -31,22 +29,27 @@ module Yesod.Auth.OAuth2.Prelude
|
|||||||
, throwIO
|
, throwIO
|
||||||
|
|
||||||
-- * OAuth2
|
-- * OAuth2
|
||||||
, OAuth2(..)
|
, OAuth2 (..)
|
||||||
, OAuth2Token(..)
|
, TokenResponse
|
||||||
, 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
|
||||||
@ -76,16 +79,15 @@ 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
|
||||||
-> OAuth2Token
|
-> TokenResponse
|
||||||
-> 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)
|
||||||
|
|
||||||
@ -103,7 +105,7 @@ fromAuthJSON name =
|
|||||||
|
|
||||||
-- | 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
|
||||||
|
|
||||||
@ -117,10 +119,9 @@ 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)
|
||||||
|
|||||||
@ -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 (note necessarily characters)
|
-- ^ Size in Bytes (not necessarily characters)
|
||||||
-> m Text
|
-> m Text
|
||||||
randomText size =
|
randomText size =
|
||||||
decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size
|
decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{-# 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
|
||||||
@ -30,46 +30,54 @@ 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 = salesforceHelper
|
oauth2SalesforceScoped =
|
||||||
pluginName
|
salesforceHelper
|
||||||
"https://login.salesforce.com/services/oauth2/userinfo"
|
pluginName
|
||||||
"https://login.salesforce.com/services/oauth2/authorize"
|
"https://login.salesforce.com/services/oauth2/userinfo"
|
||||||
"https://login.salesforce.com/services/oauth2/token"
|
"https://login.salesforce.com/services/oauth2/authorize"
|
||||||
|
"https://login.salesforce.com/services/oauth2/token"
|
||||||
|
|
||||||
oauth2SalesforceSandbox :: YesodAuth m => Text -> Text -> AuthPlugin m
|
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 = salesforceHelper
|
oauth2SalesforceSandboxScoped =
|
||||||
(pluginName <> "-sandbox")
|
salesforceHelper
|
||||||
"https://test.salesforce.com/services/oauth2/userinfo"
|
(pluginName <> "-sandbox")
|
||||||
"https://test.salesforce.com/services/oauth2/authorize"
|
"https://test.salesforce.com/services/oauth2/userinfo"
|
||||||
"https://test.salesforce.com/services/oauth2/token"
|
"https://test.salesforce.com/services/oauth2/authorize"
|
||||||
|
"https://test.salesforce.com/services/oauth2/token"
|
||||||
|
|
||||||
salesforceHelper
|
salesforceHelper
|
||||||
:: YesodAuth m
|
:: YesodAuth m
|
||||||
=> Text
|
=> Text
|
||||||
-> URI -- ^ User profile
|
-> URI
|
||||||
-> URI -- ^ Authorize
|
-- ^ User profile
|
||||||
-> URI -- ^ Token
|
-> URI
|
||||||
|
-- ^ Authorize
|
||||||
|
-> URI
|
||||||
|
-- ^ Token
|
||||||
-> [Text]
|
-> [Text]
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
-> AuthPlugin m
|
-> AuthPlugin m
|
||||||
salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret
|
salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret =
|
||||||
= authOAuth2 name oauth2 $ \manager token -> do
|
authOAuth2 name oauth2 $ \manager token -> do
|
||||||
(User userId, userResponse) <- authGetProfile name manager token profileUri
|
(User userId, userResponse) <- authGetProfile name manager token profileUri
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]
|
, oauth2ClientSecret = clientSecret
|
||||||
, oauth2TokenEndpoint = tokenUri
|
, oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2TokenEndpoint = tokenUri
|
||||||
}
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
{-# 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
|
||||||
@ -14,19 +14,23 @@ module Yesod.Auth.OAuth2.Slack
|
|||||||
import Yesod.Auth.OAuth2.Prelude
|
import Yesod.Auth.OAuth2.Prelude
|
||||||
|
|
||||||
import Network.HTTP.Client
|
import Network.HTTP.Client
|
||||||
(httpLbs, parseUrlThrow, responseBody, setQueryString)
|
( httpLbs
|
||||||
|
, 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"
|
||||||
scopeText SlackEmailScope = "identity.email"
|
scopeText SlackEmailScope = "identity.email"
|
||||||
scopeText SlackTeamScope = "identity.team"
|
scopeText SlackTeamScope = "identity.team"
|
||||||
scopeText SlackAvatarScope = "identity.avatar"
|
scopeText SlackAvatarScope = "identity.avatar"
|
||||||
|
|
||||||
newtype User = User Text
|
newtype User = User Text
|
||||||
@ -50,26 +54,30 @@ oauth2SlackScoped
|
|||||||
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 <- setQueryString [("token", Just param)]
|
req <-
|
||||||
<$> parseUrlThrow "https://slack.com/api/users.identity"
|
setQueryString [("token", Just param)]
|
||||||
|
<$> 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) -> pure Creds { credsPlugin = pluginName
|
( \(User userId) ->
|
||||||
, credsIdent = userId
|
pure
|
||||||
, credsExtra = setExtra token userResponse
|
Creds
|
||||||
}
|
{ credsPlugin = pluginName
|
||||||
)
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
|
)
|
||||||
$ eitherDecode userResponse
|
$ eitherDecode userResponse
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://slack.com/oauth/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [ scopeParam ","
|
, oauth2AuthorizeEndpoint =
|
||||||
$ map scopeText scopes
|
"https://slack.com/oauth/authorize"
|
||||||
]
|
`withQuery` [scopeParam "," $ map scopeText scopes]
|
||||||
, oauth2TokenEndpoint = "https://slack.com/api/oauth.access"
|
, oauth2TokenEndpoint = "https://slack.com/api/oauth.access"
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2RedirectUri = Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{-# 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
|
||||||
@ -20,22 +20,27 @@ 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) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://api.spotify.com/v1/me"
|
token
|
||||||
|
"https://api.spotify.com/v1/me"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://accounts.spotify.com/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [scopeParam " " scopes]
|
, oauth2AuthorizeEndpoint =
|
||||||
, oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"
|
"https://accounts.spotify.com/authorize"
|
||||||
, oauth2RedirectUri = Nothing
|
`withQuery` [scopeParam " " scopes]
|
||||||
}
|
, oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"
|
||||||
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
--
|
--
|
||||||
-- OAuth2 plugin for http://twitch.tv
|
-- OAuth2 plugin for http://twitch.tv
|
||||||
--
|
--
|
||||||
-- * Authenticates against twitch
|
-- * Authenticates against twitch
|
||||||
-- * Uses twitch user id as credentials identifier
|
-- * Uses twitch user id as credentials identifier
|
||||||
--
|
|
||||||
module Yesod.Auth.OAuth2.Twitch
|
module Yesod.Auth.OAuth2.Twitch
|
||||||
( oauth2Twitch
|
( oauth2Twitch
|
||||||
, oauth2TwitchScoped
|
, oauth2TwitchScoped
|
||||||
@ -32,25 +32,31 @@ oauth2Twitch = oauth2TwitchScoped defaultScopes
|
|||||||
oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
|
oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
|
||||||
oauth2TwitchScoped scopes clientId clientSecret =
|
oauth2TwitchScoped scopes clientId clientSecret =
|
||||||
authOAuth2 pluginName oauth2 $ \manager token -> do
|
authOAuth2 pluginName oauth2 $ \manager token -> do
|
||||||
(User userId, userResponse) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://id.twitch.tv/oauth2/validate"
|
token
|
||||||
|
"https://id.twitch.tv/oauth2/validate"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "https://id.twitch.tv/oauth2/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
`withQuery` [scopeParam " " scopes]
|
, oauth2AuthorizeEndpoint =
|
||||||
, oauth2TokenEndpoint = "https://id.twitch.tv/oauth2/token"
|
"https://id.twitch.tv/oauth2/authorize"
|
||||||
`withQuery` [ ("client_id", T.encodeUtf8 clientId)
|
`withQuery` [scopeParam " " scopes]
|
||||||
, ("client_secret", T.encodeUtf8 clientSecret)
|
, oauth2TokenEndpoint =
|
||||||
]
|
"https://id.twitch.tv/oauth2/token"
|
||||||
, oauth2RedirectUri = Nothing
|
`withQuery` [ ("client_id", T.encodeUtf8 clientId)
|
||||||
}
|
, ("client_secret", T.encodeUtf8 clientSecret)
|
||||||
|
]
|
||||||
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{-# 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
|
||||||
@ -27,21 +27,25 @@ 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) <- authGetProfile
|
(User userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"http://upcase.com/api/v1/me.json"
|
token
|
||||||
|
"http://upcase.com/api/v1/me.json"
|
||||||
|
|
||||||
pure Creds { credsPlugin = pluginName
|
pure
|
||||||
, credsIdent = T.pack $ show userId
|
Creds
|
||||||
, credsExtra = setExtra token userResponse
|
{ credsPlugin = pluginName
|
||||||
}
|
, credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"
|
, oauth2ClientSecret = clientSecret
|
||||||
, oauth2TokenEndpoint = "http://upcase.com/oauth/token"
|
, oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2TokenEndpoint = "http://upcase.com/oauth/token"
|
||||||
}
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -16,30 +16,35 @@ 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 -- ^ Client Id
|
=> Text
|
||||||
-> Text -- ^ Client Secret
|
-- ^ Client Id
|
||||||
|
-> Text
|
||||||
|
-- ^ Client Secret
|
||||||
-> AuthPlugin m
|
-> AuthPlugin m
|
||||||
oauth2WordPressDotCom clientId clientSecret =
|
oauth2WordPressDotCom clientId clientSecret =
|
||||||
authOAuth2 pluginName oauth2 $ \manager token -> do
|
authOAuth2 pluginName oauth2 $ \manager token -> do
|
||||||
(WpUser userId, userResponse) <- authGetProfile
|
(WpUser userId, userResponse) <-
|
||||||
pluginName
|
authGetProfile
|
||||||
manager
|
pluginName
|
||||||
token
|
manager
|
||||||
"https://public-api.wordpress.com/rest/v1/me/"
|
token
|
||||||
|
"https://public-api.wordpress.com/rest/v1/me/"
|
||||||
pure Creds { credsPlugin = pluginName
|
|
||||||
, credsIdent = T.pack $ show userId
|
|
||||||
, credsExtra = setExtra token userResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
|
pure
|
||||||
|
Creds
|
||||||
|
{ credsPlugin = pluginName
|
||||||
|
, credsIdent = T.pack $ show userId
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
where
|
where
|
||||||
oauth2 = OAuth2
|
oauth2 =
|
||||||
{ oauth2ClientId = clientId
|
OAuth2
|
||||||
, oauth2ClientSecret = Just clientSecret
|
{ oauth2ClientId = clientId
|
||||||
, oauth2AuthorizeEndpoint =
|
, oauth2ClientSecret = clientSecret
|
||||||
"https://public-api.wordpress.com/oauth2/authorize"
|
, oauth2AuthorizeEndpoint =
|
||||||
`withQuery` [scopeParam "," ["auth"]]
|
"https://public-api.wordpress.com/oauth2/authorize"
|
||||||
, oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"
|
`withQuery` [scopeParam "," ["auth"]]
|
||||||
, oauth2RedirectUri = Nothing
|
, oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"
|
||||||
}
|
, oauth2RedirectUri = Nothing
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
resolver: lts-18.23
|
|
||||||
extra-deps:
|
|
||||||
- hoauth2-2.0.0@sha256:4686d776272d4c57d3c8dbeb9e58b04afe4d2b410382011bd78a3d2bfb08a3fe,5662
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# This file was autogenerated by Stack.
|
|
||||||
# You should not edit this file by hand.
|
|
||||||
# For more information, please see the documentation at:
|
|
||||||
# https://docs.haskellstack.org/en/stable/lock_files
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- completed:
|
|
||||||
hackage: hoauth2-2.0.0@sha256:4686d776272d4c57d3c8dbeb9e58b04afe4d2b410382011bd78a3d2bfb08a3fe,5662
|
|
||||||
pantry-tree:
|
|
||||||
size: 2171
|
|
||||||
sha256: 291b3dd90854ef44f270519ec17e34b6778f8430f6d6517bd67b0128bd549553
|
|
||||||
original:
|
|
||||||
hackage: hoauth2-2.0.0@sha256:4686d776272d4c57d3c8dbeb9e58b04afe4d2b410382011bd78a3d2bfb08a3fe,5662
|
|
||||||
snapshots:
|
|
||||||
- completed:
|
|
||||||
size: 587819
|
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/23.yaml
|
|
||||||
sha256: 7f69bb29a57495586e7e3ed31ecc59c0d2c959cb23bd52b71ca676f254c9beb1
|
|
||||||
original: lts-18.23
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
resolver: nightly-2022-02-25
|
|
||||||
extra-deps:
|
|
||||||
- hoauth2-2.2.0@sha256:83a96156717d9e2c93394b35bef4151f82b90dc88b83d0e35c0bf1158bd41c6c,2801
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# This file was autogenerated by Stack.
|
|
||||||
# You should not edit this file by hand.
|
|
||||||
# For more information, please see the documentation at:
|
|
||||||
# https://docs.haskellstack.org/en/stable/lock_files
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- completed:
|
|
||||||
hackage: hoauth2-2.2.0@sha256:83a96156717d9e2c93394b35bef4151f82b90dc88b83d0e35c0bf1158bd41c6c,2801
|
|
||||||
pantry-tree:
|
|
||||||
size: 593
|
|
||||||
sha256: d6e2d12e0e66eb9392301ec97d50677afb71608568f3664eb466a4451c66ba59
|
|
||||||
original:
|
|
||||||
hackage: hoauth2-2.2.0@sha256:83a96156717d9e2c93394b35bef4151f82b90dc88b83d0e35c0bf1158bd41c6c,2801
|
|
||||||
snapshots:
|
|
||||||
- completed:
|
|
||||||
size: 611886
|
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/2/25.yaml
|
|
||||||
sha256: b18614ab8986a4ba6d469921a2c18decab244af78309effa3d2dab85dbdfef80
|
|
||||||
original: nightly-2022-02-25
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
resolver: nightly-2022-02-25
|
|
||||||
extra-deps:
|
|
||||||
- hoauth2-2.3.0@sha256:213744356007a4686ff3bb72105843d478bc0ba6229659429cbe241a99f55095,2816
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# This file was autogenerated by Stack.
|
|
||||||
# You should not edit this file by hand.
|
|
||||||
# For more information, please see the documentation at:
|
|
||||||
# https://docs.haskellstack.org/en/stable/lock_files
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- completed:
|
|
||||||
hackage: hoauth2-2.3.0@sha256:213744356007a4686ff3bb72105843d478bc0ba6229659429cbe241a99f55095,2816
|
|
||||||
pantry-tree:
|
|
||||||
size: 594
|
|
||||||
sha256: e559c811165a2e75cfe649b68396466b3bd0b6a5353a9d6476605e6a40e0eb37
|
|
||||||
original:
|
|
||||||
hackage: hoauth2-2.3.0@sha256:213744356007a4686ff3bb72105843d478bc0ba6229659429cbe241a99f55095,2816
|
|
||||||
snapshots:
|
|
||||||
- completed:
|
|
||||||
size: 611886
|
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/2/25.yaml
|
|
||||||
sha256: b18614ab8986a4ba6d469921a2c18decab244af78309effa3d2dab85dbdfef80
|
|
||||||
original: nightly-2022-02-25
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
resolver: lts-16.10
|
|
||||||
|
|
||||||
# Fix for weeder with stack-2
|
|
||||||
ghc-options:
|
|
||||||
"$locals":
|
|
||||||
-ddump-to-file
|
|
||||||
-ddump-hi
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
resolver: lts-17.4
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
# 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
|
|
||||||
1
stack-lts21.yaml
Normal file
1
stack-lts21.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
resolver: lts-21.25
|
||||||
1
stack-lts22.yaml
Normal file
1
stack-lts22.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
resolver: lts-22.44
|
||||||
1
stack-lts23.yaml
Normal file
1
stack-lts23.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
resolver: lts-23.28
|
||||||
1
stack-lts24.yaml
Normal file
1
stack-lts24.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
resolver: lts-24.26
|
||||||
@ -1,8 +1,4 @@
|
|||||||
resolver: nightly-2022-03-25
|
resolver: nightly-2026-01-05
|
||||||
|
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- hoauth2-2.4.0
|
- cryptonite-0.30
|
||||||
- yesod-auth-1.6.11
|
- yesod-auth-1.6.11.3
|
||||||
- yesod-core-1.6.22.0
|
|
||||||
- yesod-form-1.7.0
|
|
||||||
- yesod-persistent-1.6.0.7
|
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
# This file was autogenerated by Stack.
|
|
||||||
# You should not edit this file by hand.
|
|
||||||
# For more information, please see the documentation at:
|
|
||||||
# https://docs.haskellstack.org/en/stable/lock_files
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- completed:
|
|
||||||
hackage: hoauth2-2.4.0@sha256:c2609ee4744dee10640b9d7d85d271bd8ef643d38b0bddd006cdbe2997582481,2814
|
|
||||||
pantry-tree:
|
|
||||||
size: 594
|
|
||||||
sha256: 5e035c54476fbc89f11b8cd13dcd91f2cf2501d3b4276a1b39d1531d1487ade1
|
|
||||||
original:
|
|
||||||
hackage: hoauth2-2.4.0
|
|
||||||
- completed:
|
|
||||||
hackage: yesod-auth-1.6.11@sha256:602c0db6cc85cb7b4ad82970379e77e3dee650ab6f05d8d955c6fec7934c5f31,3054
|
|
||||||
pantry-tree:
|
|
||||||
size: 1013
|
|
||||||
sha256: 3c6076aa68d31b1f2c40f327bb11c7c7adf7c3ec0f8288ade092185b241c2451
|
|
||||||
original:
|
|
||||||
hackage: yesod-auth-1.6.11
|
|
||||||
- completed:
|
|
||||||
hackage: yesod-core-1.6.22.0@sha256:116c0c2013e81a5cd426ddeb94e07242f4b849a6e9d8a4aa02a2dae826c58086,8124
|
|
||||||
pantry-tree:
|
|
||||||
size: 5367
|
|
||||||
sha256: 61bbff374230df6c88f873bf24072760e9fbc2c5c1000870d20120d8d1028bba
|
|
||||||
original:
|
|
||||||
hackage: yesod-core-1.6.22.0
|
|
||||||
- completed:
|
|
||||||
hackage: yesod-form-1.7.0@sha256:fd857fb9ea4f5af8500ec8613aa026e3a478c874b93da9d8ab8f17f329ec8c9e,3387
|
|
||||||
pantry-tree:
|
|
||||||
size: 1729
|
|
||||||
sha256: 228cbf75994b694a05ecd413935fd3bd8c32d4a7f1b0b486777fd913a440cdbe
|
|
||||||
original:
|
|
||||||
hackage: yesod-form-1.7.0
|
|
||||||
- completed:
|
|
||||||
hackage: yesod-persistent-1.6.0.7@sha256:7ece60b1a1e0c9f56ec2f1cf67dd9d0c3962ccabc878b975bef7f743709d267d,1732
|
|
||||||
pantry-tree:
|
|
||||||
size: 497
|
|
||||||
sha256: 3778ef2964e1a3890afc22cc9124eacb40e64b62bed4983a85d3b99897f54c5c
|
|
||||||
original:
|
|
||||||
hackage: yesod-persistent-1.6.0.7
|
|
||||||
snapshots:
|
|
||||||
- completed:
|
|
||||||
size: 520054
|
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/3/25.yaml
|
|
||||||
sha256: 2c84a2124de70525171f9f6dc92544658c5353e42084e08635b8513d1fa4e6fc
|
|
||||||
original: nightly-2022-03-25
|
|
||||||
@ -1 +0,0 @@
|
|||||||
resolver: lts-18.26
|
|
||||||
1
stack.yaml
Symbolic link
1
stack.yaml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
stack-lts24.yaml
|
||||||
@ -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/lock_files
|
# https://docs.haskellstack.org/en/stable/topics/lock_files
|
||||||
|
|
||||||
packages: []
|
packages: []
|
||||||
snapshots:
|
snapshots:
|
||||||
- completed:
|
- completed:
|
||||||
size: 590102
|
sha256: d90eb1418667a225998b173817300e5ae2e1500ed03c0a9457cc2a0e78a0122a
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/26.yaml
|
size: 726337
|
||||||
sha256: e76d109964d9335abb412e22139c5bce3078be290ac6d90b8ecea6cc009bb198
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/26.yaml
|
||||||
original: lts-18.26
|
original: lts-24.26
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
{-# 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
|
||||||
|
|
||||||
@ -14,65 +15,66 @@ 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"
|
"example.com/foo?bar=baz" `shouldBe` [relativeRef|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 = [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]
|
let uriWithQuery =
|
||||||
|
[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 = [uri|http://example.com|] `withQuery` [("foo", "bar baz")]
|
let uriWithQuery =
|
||||||
|
[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
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
cabal-version: 1.18
|
cabal-version: 1.18
|
||||||
|
|
||||||
-- This file has been generated from package.yaml by hpack version 0.34.4.
|
-- This file has been generated from package.yaml by hpack version 0.38.1.
|
||||||
--
|
--
|
||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
--
|
--
|
||||||
-- hash: d8816664cb0b39ecb9a3775f44bcd9b4787d0af5d0d3f8565786479461e5ae99
|
-- hash: d595b9569ed34feddc8c41cf6f1f8cabbd8a37fa14b6afeeb24ad651ca689011
|
||||||
|
|
||||||
name: yesod-auth-oauth2
|
name: yesod-auth-oauth2
|
||||||
version: 0.7.0.2
|
version: 0.8.0.0
|
||||||
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
|
||||||
@ -41,6 +41,7 @@ library
|
|||||||
Yesod.Auth.OAuth2
|
Yesod.Auth.OAuth2
|
||||||
Yesod.Auth.OAuth2.Auth0
|
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
|
||||||
@ -53,6 +54,7 @@ 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
|
||||||
@ -70,9 +72,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
|
||||||
, cryptonite >=0.25
|
, crypton
|
||||||
, errors
|
, errors
|
||||||
, hoauth2 >=1.11.0
|
, hoauth2 >=2.8.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
|
||||||
@ -108,9 +110,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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user