Compare commits
1 commit
master
...
maksbotan/
Author | SHA1 | Date | |
---|---|---|---|
|
63e9099f87 |
195 changed files with 713 additions and 7649 deletions
99
.github/workflows/master.yml
vendored
99
.github/workflows/master.yml
vendored
|
@ -13,14 +13,12 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
cabal: ["3.6"]
|
cabal: ["3.4"]
|
||||||
ghc:
|
ghc:
|
||||||
- "8.6.5"
|
- "8.6.5"
|
||||||
- "8.8.4"
|
- "8.8.4"
|
||||||
- "8.10.7"
|
- "8.10.7"
|
||||||
- "9.0.2"
|
- "9.0.1"
|
||||||
- "9.2.2"
|
|
||||||
- "9.4.2"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -49,7 +47,10 @@ jobs:
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: |
|
run: |
|
||||||
cabal install --ignore-project -j2 doctest --constraint='doctest ^>=0.20'
|
# Using separate store-dir because default one already has 'ghc-paths' package installed
|
||||||
|
# with hardcoded path to ghcup's GHC path (which it was built with). This leads to failure in
|
||||||
|
# doctest, as it tries to invoke that GHC, and it doesn't exist here.
|
||||||
|
cabal --store-dir /tmp/cabal-store install --ignore-project -j2 doctest --constraint='doctest ^>=0.18'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
@ -60,56 +61,66 @@ jobs:
|
||||||
cabal test all
|
cabal test all
|
||||||
|
|
||||||
- name: Run doctests
|
- name: Run doctests
|
||||||
|
# doctests are broken on GHC 9 due to compiler bug:
|
||||||
|
# https://gitlab.haskell.org/ghc/ghc/-/issues/19460
|
||||||
|
continue-on-error: ${{ matrix.ghc == '9.0.1' }}
|
||||||
run: |
|
run: |
|
||||||
# Necessary for doctest to be found in $PATH
|
# Necessary for doctest to be found in $PATH
|
||||||
export PATH="$HOME/.cabal/bin:$PATH"
|
export PATH="$HOME/.cabal/bin:$PATH"
|
||||||
|
|
||||||
DOCTEST="cabal repl --with-ghc=doctest --ghc-options=-w"
|
# Filter out base-compat-batteries from .ghc.environment.*, as its modules
|
||||||
(cd servant && eval $DOCTEST)
|
# conflict with those of base-compat.
|
||||||
(cd servant-client && eval $DOCTEST)
|
#
|
||||||
(cd servant-client-core && eval $DOCTEST)
|
# FIXME: This is an ugly hack. Ultimately, we'll want to use cabal-doctest
|
||||||
(cd servant-http-streams && eval $DOCTEST)
|
# (or cabal v2-doctest, if it ever lands) to provide a clean GHC environment.
|
||||||
(cd servant-docs && eval $DOCTEST)
|
# This might allow running doctests in GHCJS build as well.
|
||||||
(cd servant-foreign && eval $DOCTEST)
|
perl -i -e 'while (<ARGV>) { print unless /package-id\s+(base-compat-batteries)-\d+(\.\d+)*/; }' .ghc.environment.*
|
||||||
(cd servant-server && eval $DOCTEST)
|
|
||||||
(cd servant-machines && eval $DOCTEST)
|
|
||||||
(cd servant-conduit && eval $DOCTEST)
|
|
||||||
(cd servant-pipes && eval $DOCTEST)
|
|
||||||
|
|
||||||
# stack:
|
(cd servant && doctest src)
|
||||||
# name: stack / ghc ${{ matrix.ghc }}
|
(cd servant-client && doctest src)
|
||||||
# runs-on: ubuntu-latest
|
(cd servant-client-core && doctest src)
|
||||||
# strategy:
|
(cd servant-http-streams && doctest src)
|
||||||
# matrix:
|
(cd servant-docs && doctest src)
|
||||||
# stack: ["2.7.5"]
|
(cd servant-foreign && doctest src)
|
||||||
# ghc: ["8.10.7"]
|
(cd servant-server && doctest src)
|
||||||
|
(cd servant-machines && doctest src)
|
||||||
|
(cd servant-conduit && doctest src)
|
||||||
|
(cd servant-pipes && doctest src)
|
||||||
|
|
||||||
# steps:
|
stack:
|
||||||
# - uses: actions/checkout@v2
|
name: stack / ghc ${{ matrix.ghc }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
stack: ["2.7.3"]
|
||||||
|
ghc: ["8.10.4"]
|
||||||
|
|
||||||
# - uses: haskell/actions/setup@v1
|
steps:
|
||||||
# name: Setup Haskell Stack
|
- uses: actions/checkout@v2
|
||||||
# with:
|
|
||||||
# ghc-version: ${{ matrix.ghc }}
|
|
||||||
# stack-version: ${{ matrix.stack }}
|
|
||||||
|
|
||||||
# - uses: actions/cache@v2.1.3
|
- uses: haskell/actions/setup@v1
|
||||||
# name: Cache ~/.stack
|
name: Setup Haskell Stack
|
||||||
# with:
|
with:
|
||||||
# path: ~/.stack
|
ghc-version: ${{ matrix.ghc }}
|
||||||
# key: ${{ runner.os }}-${{ matrix.ghc }}-stack
|
stack-version: ${{ matrix.stack }}
|
||||||
|
|
||||||
# - name: Install dependencies
|
- uses: actions/cache@v2.1.3
|
||||||
# run: |
|
name: Cache ~/.stack
|
||||||
# stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
|
with:
|
||||||
|
path: ~/.stack
|
||||||
|
key: ${{ runner.os }}-${{ matrix.ghc }}-stack
|
||||||
|
|
||||||
# - name: Build
|
- name: Install dependencies
|
||||||
# run: |
|
run: |
|
||||||
# stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
|
stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
|
||||||
|
|
||||||
# - name: Test
|
- name: Build
|
||||||
# run: |
|
run: |
|
||||||
# stack test --system-ghc
|
stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
stack test --system-ghc
|
||||||
|
|
||||||
ghcjs:
|
ghcjs:
|
||||||
name: ubuntu-latest / ghcjs 8.6
|
name: ubuntu-latest / ghcjs 8.6
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,7 +31,6 @@ doc/venv
|
||||||
doc/tutorial/static/api.js
|
doc/tutorial/static/api.js
|
||||||
doc/tutorial/static/jq.js
|
doc/tutorial/static/jq.js
|
||||||
shell.nix
|
shell.nix
|
||||||
.hspec-failures
|
|
||||||
|
|
||||||
# nix
|
# nix
|
||||||
result*
|
result*
|
||||||
|
|
|
@ -79,10 +79,8 @@ not been a timely response to a PR, you can ping the Maintainers group (with
|
||||||
We encourage people to experiment with new combinators and instances - it is
|
We encourage people to experiment with new combinators and instances - it is
|
||||||
one of the most powerful ways of using `servant`, and a wonderful way of
|
one of the most powerful ways of using `servant`, and a wonderful way of
|
||||||
getting to know it better. If you do write a new combinator, we would love to
|
getting to know it better. If you do write a new combinator, we would love to
|
||||||
know about it! Either hop on
|
know about it! Either hop on #servant on freenode and let us know, or open an
|
||||||
[#haskell-servant on libera.chat](https://web.libera.chat/#haskell-servant) and
|
issue with the `news` tag (which we will close when we read it).
|
||||||
let us know, or open an issue with the `news` tag (which we will close when we
|
|
||||||
read it).
|
|
||||||
|
|
||||||
As for adding them to the main repo: maintaining combinators can be expensive,
|
As for adding them to the main repo: maintaining combinators can be expensive,
|
||||||
since official combinators must have instances for all classes (and new classes
|
since official combinators must have instances for all classes (and new classes
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
packages:
|
packages:
|
||||||
servant/
|
servant/
|
||||||
servant-auth/servant-auth
|
|
||||||
servant-auth/servant-auth-client
|
|
||||||
servant-auth/servant-auth-docs
|
|
||||||
servant-auth/servant-auth-server
|
|
||||||
servant-auth/servant-auth-swagger
|
|
||||||
|
|
||||||
servant-client/
|
servant-client/
|
||||||
servant-client-core/
|
servant-client-core/
|
||||||
servant-http-streams/
|
servant-http-streams/
|
||||||
servant-docs/
|
servant-docs/
|
||||||
servant-foreign/
|
servant-foreign/
|
||||||
servant-server/
|
servant-server/
|
||||||
servant-swagger/
|
|
||||||
doc/tutorial/
|
doc/tutorial/
|
||||||
|
|
||||||
-- servant streaming
|
-- servant streaming
|
||||||
|
@ -37,7 +30,7 @@ packages:
|
||||||
doc/cookbook/generic
|
doc/cookbook/generic
|
||||||
doc/cookbook/hoist-server-with-context
|
doc/cookbook/hoist-server-with-context
|
||||||
doc/cookbook/https
|
doc/cookbook/https
|
||||||
doc/cookbook/jwt-and-basic-auth
|
-- doc/cookbook/jwt-and-basic-auth/
|
||||||
doc/cookbook/pagination
|
doc/cookbook/pagination
|
||||||
-- doc/cookbook/sentry
|
-- doc/cookbook/sentry
|
||||||
-- Commented out because servant-quickcheck currently doesn't build.
|
-- Commented out because servant-quickcheck currently doesn't build.
|
||||||
|
@ -47,8 +40,31 @@ packages:
|
||||||
doc/cookbook/using-custom-monad
|
doc/cookbook/using-custom-monad
|
||||||
doc/cookbook/using-free-client
|
doc/cookbook/using-free-client
|
||||||
-- doc/cookbook/open-id-connect
|
-- doc/cookbook/open-id-connect
|
||||||
doc/cookbook/managed-resource
|
doc/cookbook/openapi3
|
||||||
|
|
||||||
tests: True
|
tests: True
|
||||||
optimization: False
|
optimization: False
|
||||||
-- reorder-goals: True
|
-- reorder-goals: True
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
-- see https://github.com/haskell-infra/hackage-trustees/issues/119
|
||||||
|
foundation >=0.0.14,
|
||||||
|
memory <0.14.12 || >0.14.12
|
||||||
|
|
||||||
|
constraints: base-compat ^>=0.11
|
||||||
|
constraints: semigroups ^>=0.19
|
||||||
|
|
||||||
|
-- allow-newer: sqlite-simple-0.4.16.0:semigroups
|
||||||
|
-- allow-newer: direct-sqlite-2.3.24:semigroups
|
||||||
|
|
||||||
|
-- needed for doctests
|
||||||
|
write-ghc-environment-files: always
|
||||||
|
|
||||||
|
-- https://github.com/chordify/haskell-servant-pagination/pull/12
|
||||||
|
allow-newer: servant-pagination-2.2.2:servant
|
||||||
|
allow-newer: servant-pagination-2.2.2:servant-server
|
||||||
|
|
||||||
|
allow-newer: servant-js:servant
|
||||||
|
|
||||||
|
-- ghc 9
|
||||||
|
allow-newer: tdigest:base
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
synopsis: Fixes encoding of URL parameters in servant-client
|
|
||||||
prs: #1432
|
|
||||||
issues: #1418
|
|
||||||
description: {
|
|
||||||
Some applications use query parameters to pass arbitrary (non-unicode) binary
|
|
||||||
data. This change modifies how servant-client handles query parameters, so
|
|
||||||
that application developers can use `ToHttpApiData` to marshal binary data into
|
|
||||||
query parameters.
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
synopsis: Derive HasClient good response status from Verb status
|
|
||||||
prs: #1469
|
|
||||||
description: {
|
|
||||||
`HasClient` instances for the `Verb` datatype use `runRequest` in
|
|
||||||
`clientWithRoute` definitions.
|
|
||||||
This means that a request performed with `runClientM` will be successful if and
|
|
||||||
only if the endpoint specify a response status code >=200 and <300.
|
|
||||||
This change replaces `runRequest` with `runRequestAcceptStatus` in `Verb`
|
|
||||||
instances for the `HasClient` class, deriving the good response status from
|
|
||||||
the `Verb` status.
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
synopsis: Enable FlexibleContexts in Servant.API.ContentTypes
|
|
||||||
prs: #1477
|
|
||||||
|
|
||||||
description: {
|
|
||||||
|
|
||||||
Starting with GHC 9.2, UndecidableInstances no longer implies FlexibleContexts.
|
|
||||||
Add this extension where it's needed to make compilation succeed.
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
synopsis: Fix performRequest in servant-client-ghcjs
|
|
||||||
prs: #1529
|
|
||||||
|
|
||||||
description: {
|
|
||||||
|
|
||||||
performRequest function in servant-client-ghcjs was not compatible with the
|
|
||||||
latest RunClient typeclass. Added the acceptStatus parameter and fixed the
|
|
||||||
functionality to match what servant-client provides.
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
synopsis: Display capture hints in router layout
|
|
||||||
prs: #1556
|
|
||||||
|
|
||||||
description: {
|
|
||||||
|
|
||||||
This PR enhances the `Servant.Server.layout` function, which produces a textual description of the routing layout of an API. More precisely, it changes `<capture>` blocks, so that they display the name and type of the variable being captured instead.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
For the following API
|
|
||||||
```haskell
|
|
||||||
type API =
|
|
||||||
"a" :> "d" :> Get '[JSON] NoContent
|
|
||||||
:<|> "b" :> Capture "x" Int :> Get '[JSON] Bool
|
|
||||||
:<|> "a" :> "e" :> Get '[JSON] Int
|
|
||||||
```
|
|
||||||
|
|
||||||
we previously got the following output:
|
|
||||||
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├─ a/
|
|
||||||
│ ├─ d/
|
|
||||||
│ │ └─•
|
|
||||||
│ └─ e/
|
|
||||||
│ └─•
|
|
||||||
└─ b/
|
|
||||||
└─ <capture>/
|
|
||||||
├─•
|
|
||||||
┆
|
|
||||||
└─•
|
|
||||||
```
|
|
||||||
|
|
||||||
now we get:
|
|
||||||
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├─ a/
|
|
||||||
│ ├─ d/
|
|
||||||
│ │ └─•
|
|
||||||
│ └─ e/
|
|
||||||
│ └─•
|
|
||||||
└─ b/
|
|
||||||
└─ <x::Int>/
|
|
||||||
├─•
|
|
||||||
┆
|
|
||||||
└─•
|
|
||||||
```
|
|
||||||
|
|
||||||
This change is achieved by the introduction of a CaptureHint type, which is passed as an extra argument to the CaptureRouter and CaptureAllRouter constructors for the Router' type.
|
|
||||||
CaptureHint values are then used in routerLayout, to display the name and type of captured values, instead of just `<capture>` previously.
|
|
||||||
|
|
||||||
N.B.:
|
|
||||||
Because the choice smart constructor for routers can aggregate Capture combinators with different capture hints, the Capture*Router constructors actually take a list of CaptureHint, instead of a single one.
|
|
||||||
|
|
||||||
This PR also introduces Spec tests for the routerLayout function.
|
|
||||||
|
|
||||||
Warning:
|
|
||||||
This change is potentially breaking, because it adds the constraint `Typeable a` to all types that are to be captured. Because all types are typeable since GHC 7.10, this is not as bad as it sounds ; it only break expressions where `a` is quantified in an expression with `Capture a`.
|
|
||||||
In those cases, the fix is easy: it suffices to add `Typeable a` to the left-hand side of the quantification constraint.
|
|
||||||
|
|
||||||
For instance, the following code will no longer compile:
|
|
||||||
```haskell
|
|
||||||
type MyAPI a = Capture "foo" a :> Get '[JSON] ()
|
|
||||||
|
|
||||||
myServer :: forall a. Server (MyAPI a)
|
|
||||||
myServer = const $ return ()
|
|
||||||
|
|
||||||
myApi :: forall a. Proxy (MyAPI a)
|
|
||||||
myApi = Proxy
|
|
||||||
|
|
||||||
app :: forall a. (FromHttpApiData a) => Application
|
|
||||||
app = serve (myApi @a) (myServer @a)
|
|
||||||
```
|
|
||||||
|
|
||||||
Indeed, `app` should be replaced with:
|
|
||||||
```haskell
|
|
||||||
app :: forall a. (FromHttpApiData a, Typeable a) => Application
|
|
||||||
app = serve (myApi @a) (myServer @a)
|
|
||||||
```
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
synopsis: Encode captures using toEncodedUrlPiece
|
|
||||||
prs: #1569
|
|
||||||
issues: #1511
|
|
||||||
|
|
||||||
description: {
|
|
||||||
The `servant-client` library now makes direct use of `toEncodedUrlPiece` from `ToHttpApiData`
|
|
||||||
to encode captured values when building the request path. It gives user freedom to implement
|
|
||||||
URL-encoding however they need.
|
|
||||||
|
|
||||||
Previous behavior was to use `toUrlPiece` and URL-encode its output using `toEncodedUrlPiece`
|
|
||||||
from the `Text` instance of `ToHttpApiData`. The issue with this approach is that
|
|
||||||
`ToHttpApiData Text` is overly zealous and also encodes characters, such as `*`, which are perfectly valid in a URL.
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
synopsis: Add API docs for ServerT
|
|
||||||
prs: #1573
|
|
|
@ -1,12 +0,0 @@
|
||||||
synopsis: Allow IO in validationKeys
|
|
||||||
prs: #1580
|
|
||||||
issues: #1579
|
|
||||||
|
|
||||||
description: {
|
|
||||||
|
|
||||||
Currently validationKeys are a fixed JWKSet. This does not work with OIDC
|
|
||||||
providers such as AWS Cognito or Okta, which regularly fetching jwks_uri to
|
|
||||||
discover new and expired keys.
|
|
||||||
|
|
||||||
This change alters the type of validationKeys from JWKSet to IO JWKSet.
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
synopsis: Only include question mark for nonempty query strings
|
|
||||||
prs: 1589
|
|
|
@ -1,2 +0,0 @@
|
||||||
synopsis: Run ClientEnv's makeClientRequest in IO.
|
|
||||||
prs: #1595
|
|
|
@ -1,10 +0,0 @@
|
||||||
synopsis: Handle Cookies correctly for RunStreamingClient
|
|
||||||
prs: #1606
|
|
||||||
issues: #1605
|
|
||||||
|
|
||||||
description: {
|
|
||||||
|
|
||||||
Makes performWithStreamingRequest take into consideration the
|
|
||||||
CookieJar, which it previously didn't.
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
synopsis: Add Functor instance to AuthHandler.
|
|
||||||
prs: #1638
|
|
|
@ -1,8 +0,0 @@
|
||||||
synopsis: Add HasStatus instance for Headers (that defers StatusOf to underlying value)
|
|
||||||
prs: #1649
|
|
||||||
|
|
||||||
description: {
|
|
||||||
|
|
||||||
Adds a new HasStatus (Headers hs a) instance (StatusOf (Headers hs a) = StatusOf a)
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ BUILDDIR = _build
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
# Put it first so that "make" without argument is like "make help".
|
||||||
help:
|
help:
|
||||||
@if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check README.md."; fi
|
@if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check building-the-docs file."; fi
|
||||||
@if [ ! "z$$(which $(SPHINXBUILD))" = "z$$(pwd)/venv/bin/sphinx-build" ]; then echo "WARNING: Did you forgot to 'source venv/bin/activate'"; fi
|
@if [ ! "z$$(which $(SPHINXBUILD))" = "z$$(pwd)/venv/bin/sphinx-build" ]; then echo "WARNING: Did you forgot to 'source venv/bin/activate'"; fi
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Servant'
|
project = u'Servant'
|
||||||
copyright = u'2022, Servant Contributors'
|
copyright = u'2018, Servant Contributors'
|
||||||
author = u'Servant Contributors'
|
author = u'Servant Contributors'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
@ -169,3 +169,4 @@ texinfo_documents = [
|
||||||
source_parsers = {
|
source_parsers = {
|
||||||
'.lhs': CommonMarkParser,
|
'.lhs': CommonMarkParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-basic-auth
|
name: cookbook-basic-auth
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Basic Authentication cookbook example
|
synopsis: Basic Authentication cookbook example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-basic-auth
|
executable cookbook-basic-auth
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-basic-streaming
|
name: cookbook-basic-streaming
|
||||||
version: 2.1
|
version: 2.1
|
||||||
synopsis: Streaming in servant without streaming libs
|
synopsis: Streaming in servant without streaming libs
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-basic-streaming
|
executable cookbook-basic-streaming
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-curl-mock
|
name: cookbook-curl-mock
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Generate curl mock requests cookbook example
|
synopsis: Generate curl mock requests cookbook example
|
||||||
homepage: http://docs.servant.dev
|
homepage: http://docs.servant.dev
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbock-curl-mock
|
executable cookbock-curl-mock
|
||||||
if impl(ghc >= 9.2)
|
|
||||||
-- generic-arbitrary is incompatible
|
|
||||||
buildable: False
|
|
||||||
main-is: CurlMock.lhs
|
main-is: CurlMock.lhs
|
||||||
build-depends: base == 4.*
|
build-depends: base == 4.*
|
||||||
, aeson
|
, aeson
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-custom-errors
|
name: cookbook-custom-errors
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Return custom error messages from combinators
|
synopsis: Return custom error messages from combinators
|
||||||
homepage: http://docs.servant.dev
|
homepage: http://docs.servant.dev
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-custom-errors
|
executable cookbook-custom-errors
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: mysql-basics
|
name: mysql-basics
|
||||||
version: 0.1.0.0
|
version: 0.1.0.0
|
||||||
synopsis: Simple MySQL API cookbook example
|
synopsis: Simple MySQL API cookbook example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
executable run
|
executable run
|
||||||
hs-source-dirs: .
|
hs-source-dirs: .
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-db-postgres-pool
|
name: cookbook-db-postgres-pool
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Simple PostgreSQL connection pool cookbook example
|
synopsis: Simple PostgreSQL connection pool cookbook example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-db-postgres-pool
|
executable cookbook-db-postgres-pool
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-db-sqlite-simple
|
name: cookbook-db-sqlite-simple
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Simple SQLite DB cookbook example
|
synopsis: Simple SQLite DB cookbook example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-db-sqlite-simple
|
executable cookbook-db-sqlite-simple
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-file-upload
|
name: cookbook-file-upload
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: File upload cookbook example
|
synopsis: File upload cookbook example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-file-upload
|
executable cookbook-file-upload
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-generic
|
name: cookbook-generic
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Using custom monad to pass a state between handlers
|
synopsis: Using custom monad to pass a state between handlers
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-using-custom-monad
|
executable cookbook-using-custom-monad
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-hoist-server-with-context
|
name: cookbook-hoist-server-with-context
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
synopsis: JWT and basic access authentication with a Custom Monad cookbook example
|
synopsis: JWT and basic access authentication with a Custom Monad cookbook example
|
||||||
description: Using servant-auth to support both JWT-based and basic
|
description: Using servant-auth to support both JWT-based and basic
|
||||||
authentication.
|
authentication.
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
category: Servant
|
category: Servant
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-hoist-server-with-context
|
executable cookbook-hoist-server-with-context
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-https
|
name: cookbook-https
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: HTTPS cookbook example
|
synopsis: HTTPS cookbook example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-https
|
executable cookbook-https
|
||||||
|
|
|
@ -6,8 +6,8 @@ how to solve many common problems with servant. If you're
|
||||||
interested in contributing examples of your own, feel free
|
interested in contributing examples of your own, feel free
|
||||||
to open an issue or a pull request on
|
to open an issue or a pull request on
|
||||||
`our github repository <https://github.com/haskell-servant/servant>`_
|
`our github repository <https://github.com/haskell-servant/servant>`_
|
||||||
or even to just get in touch with us on the `**#haskell-servant** IRC channel
|
or even to just get in touch with us on the **#servant** IRC channel
|
||||||
on libera.chat <https://web.libera.chat/#haskell-servant>_ or on
|
on freenode or on
|
||||||
`the mailing list <https://groups.google.com/forum/#!forum/haskell-servant>`_.
|
`the mailing list <https://groups.google.com/forum/#!forum/haskell-servant>`_.
|
||||||
|
|
||||||
The scope is very wide. Simple and fancy authentication schemes,
|
The scope is very wide. Simple and fancy authentication schemes,
|
||||||
|
@ -19,6 +19,7 @@ you name it!
|
||||||
|
|
||||||
structuring-apis/StructuringApis.lhs
|
structuring-apis/StructuringApis.lhs
|
||||||
generic/Generic.lhs
|
generic/Generic.lhs
|
||||||
|
openapi3/OpenAPI.lhs
|
||||||
https/Https.lhs
|
https/Https.lhs
|
||||||
db-mysql-basics/MysqlBasics.lhs
|
db-mysql-basics/MysqlBasics.lhs
|
||||||
db-sqlite-simple/DBConnection.lhs
|
db-sqlite-simple/DBConnection.lhs
|
||||||
|
@ -37,4 +38,3 @@ you name it!
|
||||||
sentry/Sentry.lhs
|
sentry/Sentry.lhs
|
||||||
testing/Testing.lhs
|
testing/Testing.lhs
|
||||||
open-id-connect/OpenIdConnect.lhs
|
open-id-connect/OpenIdConnect.lhs
|
||||||
managed-resource/ManagedResource.lhs
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-jwt-and-basic-auth
|
name: cookbook-jwt-and-basic-auth
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
synopsis: JWT and basic access authentication cookbook example
|
synopsis: JWT and basic access authentication cookbook example
|
||||||
description: Using servant-auth to support both JWT-based and basic
|
description: Using servant-auth to support both JWT-based and basic
|
||||||
authentication.
|
authentication.
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
category: Servant
|
category: Servant
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-jwt-and-basic-auth
|
executable cookbook-jwt-and-basic-auth
|
||||||
|
@ -22,7 +22,7 @@ executable cookbook-jwt-and-basic-auth
|
||||||
, servant
|
, servant
|
||||||
, servant-client
|
, servant-client
|
||||||
, servant-server
|
, servant-server
|
||||||
, servant-auth == 0.4.*
|
, servant-auth ==0.3.*
|
||||||
, servant-auth-server >= 0.3.1.0
|
, servant-auth-server >= 0.3.1.0
|
||||||
, warp >= 3.2
|
, warp >= 3.2
|
||||||
, wai >= 3.2
|
, wai >= 3.2
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
# Request-lifetime Managed Resources
|
|
||||||
|
|
||||||
Let's see how we can write a handle that uses a resource managed by Servant. The resource is created automatically by Servant when the server recieves a request, and the resource is automatically destroyed when the server is finished handling a request.
|
|
||||||
|
|
||||||
As usual, we start with a little bit of throat clearing.
|
|
||||||
|
|
||||||
|
|
||||||
``` haskell
|
|
||||||
{-# LANGUAGE DataKinds #-}
|
|
||||||
{-# LANGUAGE TypeOperators #-}
|
|
||||||
import Control.Concurrent
|
|
||||||
import Control.Exception (bracket, throwIO)
|
|
||||||
import Control.Monad.IO.Class
|
|
||||||
import Control.Monad.Trans.Resource
|
|
||||||
import Data.Acquire
|
|
||||||
import Network.HTTP.Client (newManager, defaultManagerSettings)
|
|
||||||
import Network.Wai.Handler.Warp
|
|
||||||
import Servant
|
|
||||||
import Servant.Client
|
|
||||||
import System.IO
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we define an API type that uses the `WithResource` combinator. The server handler for an endpoint with a `WithResource res` component will receive a value of that type as an argument.
|
|
||||||
|
|
||||||
``` haskell
|
|
||||||
type API = WithResource Handle :> ReqBody '[PlainText] String :> Post '[JSON] NoContent
|
|
||||||
|
|
||||||
api :: Proxy API
|
|
||||||
api = Proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
But this resource value has to come from somewhere. Servant obtains the value using an Acquire provided in the context. The Acquire knows how to both create and destroy resources of a particular type.
|
|
||||||
|
|
||||||
``` haskell
|
|
||||||
appContext :: Context '[Acquire Handle]
|
|
||||||
appContext = acquireHandle :. EmptyContext
|
|
||||||
|
|
||||||
acquireHandle :: Acquire Handle
|
|
||||||
acquireHandle = mkAcquire newHandle closeHandle
|
|
||||||
|
|
||||||
newHandle :: IO Handle
|
|
||||||
newHandle = do
|
|
||||||
putStrLn "opening file"
|
|
||||||
h <- openFile "test.txt" AppendMode
|
|
||||||
putStrLn "opened file"
|
|
||||||
return h
|
|
||||||
|
|
||||||
closeHandle :: Handle -> IO ()
|
|
||||||
closeHandle h = do
|
|
||||||
putStrLn "closing file"
|
|
||||||
hClose h
|
|
||||||
putStrLn "closed file"
|
|
||||||
```
|
|
||||||
|
|
||||||
Now we create the handler which will use this resource. This handler will write the request message to the System.IO.Handle which was provided to us. In some situations the handler will succeed, but in some in will fail. In either case, Servant will clean up the resource for us.
|
|
||||||
|
|
||||||
``` haskell
|
|
||||||
server :: Server API
|
|
||||||
server = writeToFile
|
|
||||||
|
|
||||||
where writeToFile :: (ReleaseKey, Handle) -> String -> Handler NoContent
|
|
||||||
writeToFile (_, h) msg = case msg of
|
|
||||||
"illegal" -> error "wait, that's illegal!"
|
|
||||||
legalMsg -> liftIO $ do
|
|
||||||
putStrLn "writing file"
|
|
||||||
hPutStrLn h legalMsg
|
|
||||||
putStrLn "wrote file"
|
|
||||||
return NoContent
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally we run the server in the background while we post messages to it.
|
|
||||||
|
|
||||||
``` haskell
|
|
||||||
runApp :: IO ()
|
|
||||||
runApp = run 8080 (serveWithContext api appContext $ server)
|
|
||||||
|
|
||||||
postMsg :: String -> ClientM NoContent
|
|
||||||
postMsg = client api
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
mgr <- newManager defaultManagerSettings
|
|
||||||
bracket (forkIO $ runApp) killThread $ \_ -> do
|
|
||||||
ms <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")) $ do
|
|
||||||
liftIO $ putStrLn "sending hello message"
|
|
||||||
_ <- postMsg "hello"
|
|
||||||
liftIO $ putStrLn "sending illegal message"
|
|
||||||
_ <- postMsg "illegal"
|
|
||||||
liftIO $ putStrLn "done"
|
|
||||||
print ms
|
|
||||||
```
|
|
||||||
|
|
||||||
This program prints
|
|
||||||
|
|
||||||
```
|
|
||||||
sending hello message
|
|
||||||
opening file
|
|
||||||
opened file
|
|
||||||
writing file
|
|
||||||
wrote file
|
|
||||||
closing file
|
|
||||||
closed file
|
|
||||||
sending illegal message
|
|
||||||
opening file
|
|
||||||
opened file
|
|
||||||
closing file
|
|
||||||
closed file
|
|
||||||
wait, that's illegal!
|
|
||||||
CallStack (from HasCallStack):
|
|
||||||
error, called at ManagedResource.lhs:63:24 in main:Main
|
|
||||||
Left (FailureResponse (Request {requestPath = (BaseUrl {baseUrlScheme = Http, baseUrlHost = "localhost", baseUrlPort = 8080, baseUrlPath = ""},""), requestQueryString = fromList [], requestBody = Just ((),text/plain;charset=utf-8), requestAccept = fromList [], requestHeaders = fromList [], requestHttpVersion = HTTP/1.1, requestMethod = "POST"}) (Response {responseStatusCode = Status {statusCode = 500, statusMessage = "Internal Server Error"}, responseHeaders = fromList [("Transfer-Encoding","chunked"),("Date","Thu, 24 Nov 2022 21:04:47 GMT"),("Server","Warp/3.3.23"),("Content-Type","text/plain; charset=utf-8")], responseHttpVersion = HTTP/1.1, responseBody = "Something went wrong"}))
|
|
||||||
```
|
|
||||||
|
|
||||||
and appends to a file called `test.txt`. We can see from the output that when a legal message is sent, the file is opened, written to, and closed. We can also see that when an illegal message is sent, the file is opened but not written to. Crucially, it is still closed even though the handler threw an exception.
|
|
|
@ -1,30 +0,0 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-managed-resource
|
|
||||||
version: 0.1
|
|
||||||
synopsis: Simple managed resource cookbook example
|
|
||||||
homepage: http://docs.servant.dev/
|
|
||||||
license: BSD-3-Clause
|
|
||||||
license-file: ../../../servant/LICENSE
|
|
||||||
author: Servant Contributors
|
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
|
||||||
build-type: Simple
|
|
||||||
tested-with: GHC==9.4.2
|
|
||||||
|
|
||||||
executable cookbook-managed-resource
|
|
||||||
main-is: ManagedResource.lhs
|
|
||||||
build-depends: base == 4.*
|
|
||||||
, text >= 1.2
|
|
||||||
, aeson >= 1.2
|
|
||||||
, servant
|
|
||||||
, servant-client
|
|
||||||
, servant-server
|
|
||||||
, warp >= 3.2
|
|
||||||
, wai >= 3.2
|
|
||||||
, http-types >= 0.12
|
|
||||||
, markdown-unlit >= 0.4
|
|
||||||
, http-client >= 0.5
|
|
||||||
, transformers
|
|
||||||
, resourcet
|
|
||||||
default-language: Haskell2010
|
|
||||||
ghc-options: -Wall -pgmL markdown-unlit
|
|
||||||
build-tool-depends: markdown-unlit:markdown-unlit
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: open-id-connect
|
name: open-id-connect
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: OpenId Connect with Servant example
|
synopsis: OpenId Connect with Servant example
|
||||||
homepage: http://haskell-servant.readthedocs.org/
|
homepage: http://haskell-servant.readthedocs.org/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >= 1.10
|
||||||
tested-with: GHC==8.6.5
|
tested-with: GHC==8.6.5
|
||||||
|
|
||||||
executable cookbook-openidconnect
|
executable cookbook-openidconnect
|
||||||
|
|
|
@ -330,7 +330,7 @@ data Customer = Customer {
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is the code that displays the homepage.
|
Here is the code that displays the homepage.
|
||||||
It should contain a link to the `/login` URL.
|
It should contain a link to the the `/login` URL.
|
||||||
When the user clicks on this link it will be redirected to Google login page
|
When the user clicks on this link it will be redirected to Google login page
|
||||||
with some generated information.
|
with some generated information.
|
||||||
|
|
||||||
|
|
200
doc/cookbook/openapi3/OpenAPI.lhs
Normal file
200
doc/cookbook/openapi3/OpenAPI.lhs
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
# OpenAPI
|
||||||
|
|
||||||
|
OpenAPI is language-agnostic format for API specifications. It is structured as JSON or YAML
|
||||||
|
document and can be used to communicate API documentation between the backend and its clients, like
|
||||||
|
the frontend.
|
||||||
|
|
||||||
|
The OpenAPI specification itself is available at https://swagger.io/specification/. It is supported
|
||||||
|
by various tools, like [swagger-ui](https://swagger.io/tools/swagger-ui/) — a tool that
|
||||||
|
visualizes OpenAPI document and allows to send requests to the backend it describes, or
|
||||||
|
[swagger-codegen](https://swagger.io/tools/swagger-codegen/), which can generate client code in a
|
||||||
|
variety of languages given the specification.
|
||||||
|
|
||||||
|
Since Servant backends already contain a comprehensive description of the API they provide, it is
|
||||||
|
fairly easy to generate OpenAPI specification based on that description. This is achieved with
|
||||||
|
[servant-openapi3](https://hackage.haskell.org/package/servant-openapi3) package, which is based on
|
||||||
|
older `servant-swagger`, targeted at second version of OpenAPI specification (then called Swagger).
|
||||||
|
|
||||||
|
This cookbook demonstrates how to use `servant-openapi3` and how to integrate interactive schema
|
||||||
|
browser with your backend.
|
||||||
|
|
||||||
|
## The sample API
|
||||||
|
|
||||||
|
Let's start with an API of an example TODO service:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DerivingStrategies #-}
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
|
import GHC.Generics
|
||||||
|
import Data.Text
|
||||||
|
import Data.Aeson
|
||||||
|
|
||||||
|
import Servant
|
||||||
|
|
||||||
|
import Data.OpenApi
|
||||||
|
import Servant.OpenApi
|
||||||
|
import Servant.Swagger.UI
|
||||||
|
|
||||||
|
import Network.Wai.Handler.Warp as Warp
|
||||||
|
|
||||||
|
-- | A single Todo entry.
|
||||||
|
data Todo = Todo
|
||||||
|
{ created :: Int -- ^ Creation datetime.
|
||||||
|
, summary :: Text -- ^ Task summary.
|
||||||
|
}
|
||||||
|
deriving stock (Show, Generic)
|
||||||
|
deriving anyclass (ToSchema, ToJSON, FromJSON)
|
||||||
|
|
||||||
|
-- | A unique Todo entry ID.
|
||||||
|
newtype TodoId = TodoId Int
|
||||||
|
deriving stock (Show, Generic)
|
||||||
|
deriving newtype (ToJSON, FromHttpApiData)
|
||||||
|
deriving anyclass (ToParamSchema, ToSchema)
|
||||||
|
|
||||||
|
-- | The API of a Todo service.
|
||||||
|
type TodoAPI
|
||||||
|
= "todo" :> Description "Get all TODO items"
|
||||||
|
:> Get '[JSON] [Todo]
|
||||||
|
:<|> "todo" :> Description "Add a new TODO item"
|
||||||
|
:> ReqBody '[JSON] Todo :> Post '[JSON] TodoId
|
||||||
|
:<|> "todo" :> Description "Get a TODO item by its id"
|
||||||
|
:> Capture "id" TodoId :> Get '[JSON] Todo
|
||||||
|
:<|> "todo" :> Description "Update an existing TODO item by its id"
|
||||||
|
:> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] TodoId
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that all API endpoints are decorated with `Description` (coming from `servant` itself): these
|
||||||
|
descriptions will propagate to the OpenAPI document automatically.
|
||||||
|
|
||||||
|
## Adding OpenAPI
|
||||||
|
|
||||||
|
We are ready to define OpenAPI document for our `TodoAPI`. Everything you need to do for that is to
|
||||||
|
use `toOpenApi` function from `servant-openapi3` package:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
-- | OpenAPI spec for Todo API.
|
||||||
|
todoOpenApi :: OpenApi
|
||||||
|
todoOpenApi = toOpenApi (Proxy :: Proxy TodoAPI)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is possible since we've derived `ToSchema` for `Todo` and `ToParamSchema` for `TodoId` (needed
|
||||||
|
since the type is used in URLs) instances — and this is everything that is needed to generate
|
||||||
|
the OpenAPI 3.0 specification for our API. All of this is thanks to `Generic`-based schema generator
|
||||||
|
found in `openapi3` and `servant-openapi3` packages.
|
||||||
|
|
||||||
|
Of course, you can customize the schema in many ways, see the documentation for
|
||||||
|
[`openapi3`](https://hackage.haskell.org/package/openapi3) package.
|
||||||
|
|
||||||
|
The generated schema looks something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/todo": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get all TODO items",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json;charset=utf-8": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Todo"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
........
|
||||||
|
}
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"Todo": {
|
||||||
|
"required": [
|
||||||
|
"created",
|
||||||
|
"summary"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"minimum": -9223372036854775808,
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 9223372036854775807
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"TodoId": {
|
||||||
|
"minimum": -9223372036854775808,
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 9223372036854775807
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The schema can be pasted into the [Swagger editor](https://editor.swagger.io/), which will nicely
|
||||||
|
display the generated schema.
|
||||||
|
|
||||||
|
## Integrating schema browser into the backend
|
||||||
|
|
||||||
|
Or, the schema browser can be integrated into the backend itself. This is done via
|
||||||
|
`servant-swagger-ui` package, which embeds `swagger-ui` into the Servant backend.
|
||||||
|
|
||||||
|
First, define a sub-api that will serve the documentation:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
type DocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
And a full API for your backend, which combines your endpoints and `DocsAPI`:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
type API = DocsAPI :<|> TodoAPI
|
||||||
|
```
|
||||||
|
|
||||||
|
`SwaggerSchemaUI` describes an API that will serve the interactive schema browser at `/swagger-ui`
|
||||||
|
of your server and the specification in JSON format at `/swagger.json`. Of course, both paths are
|
||||||
|
customizable.
|
||||||
|
|
||||||
|
A handler for `SwaggerSchemaUI`, called `swaggerSchemaUIServer`, expectes one argument: the
|
||||||
|
specification itself. In our case, it's `todoOpenApi`.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
todoServer :: Servant.Server API
|
||||||
|
todoServer = swaggerSchemaUIServer todoOpenApi
|
||||||
|
:<|> error "The actual TODO API is not implemented"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the server can be run as usual:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
Warp.run 5000 $ serve (Proxy :: Proxy API) todoServer
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this example, navigate to http://localhost:5000/swagger-ui and you will see the interactive
|
||||||
|
schema browser:
|
||||||
|
|
||||||
|
![](./swagger-ui-example.png)
|
||||||
|
|
||||||
|
You can make requests in this UI and they will be sent to your backend as expected.
|
26
doc/cookbook/openapi3/openapi3.cabal
Normal file
26
doc/cookbook/openapi3/openapi3.cabal
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: cookbook-openapi3
|
||||||
|
version: 2.1
|
||||||
|
synopsis: OpenAPI 3.0 schema generation example
|
||||||
|
homepage: http://docs.servant.dev/
|
||||||
|
license: BSD3
|
||||||
|
license-file: ../../../servant/LICENSE
|
||||||
|
author: Servant Contributors
|
||||||
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
|
executable cookbook-openapi3
|
||||||
|
main-is: OpenAPI.lhs
|
||||||
|
build-tool-depends: markdown-unlit:markdown-unlit
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -pgmL markdown-unlit
|
||||||
|
build-depends: base >= 4.9 && <5
|
||||||
|
, aeson
|
||||||
|
, openapi3
|
||||||
|
, servant
|
||||||
|
, servant-server
|
||||||
|
, servant-openapi3
|
||||||
|
, servant-swagger-ui
|
||||||
|
, text
|
||||||
|
, warp
|
BIN
doc/cookbook/openapi3/swagger-ui-example.png
Normal file
BIN
doc/cookbook/openapi3/swagger-ui-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-pagination
|
name: cookbook-pagination
|
||||||
version: 2.1
|
version: 2.1
|
||||||
synopsis: Pagination with Servant example
|
synopsis: Pagination with Servant example
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-pagination
|
executable cookbook-pagination
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-sentry
|
name: cookbook-sentry
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Collecting runtime exceptions using Sentry
|
synopsis: Collecting runtime exceptions using Sentry
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-sentry
|
executable cookbook-sentry
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-structuring-apis
|
name: cookbook-structuring-apis
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Example that shows how APIs can be structured
|
synopsis: Example that shows how APIs can be structured
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-structuring-apis
|
executable cookbook-structuring-apis
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-testing
|
name: cookbook-testing
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
synopsis: Common testing patterns in Servant apps
|
synopsis: Common testing patterns in Servant apps
|
||||||
description: This recipe includes various strategies for writing tests for Servant.
|
description: This recipe includes various strategies for writing tests for Servant.
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
category: Servant
|
category: Servant
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-testing
|
executable cookbook-testing
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-using-custom-monad
|
name: cookbook-using-custom-monad
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Using custom monad to pass a state between handlers
|
synopsis: Using custom monad to pass a state between handlers
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-using-custom-monad
|
executable cookbook-using-custom-monad
|
||||||
|
|
|
@ -119,7 +119,7 @@ Now we can use `servant-client`'s internals to convert servant's `Request`
|
||||||
to http-client's `Request`, and we can inspect it:
|
to http-client's `Request`, and we can inspect it:
|
||||||
|
|
||||||
```haskell
|
```haskell
|
||||||
req' <- I.defaultMakeClientRequest burl req
|
let req' = I.defaultMakeClientRequest burl req
|
||||||
putStrLn $ "Making request: " ++ show req'
|
putStrLn $ "Making request: " ++ show req'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-using-free-client
|
name: cookbook-using-free-client
|
||||||
version: 0.1
|
version: 0.1
|
||||||
synopsis: Using Free client
|
synopsis: Using Free client
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
|
||||||
|
|
||||||
executable cookbook-using-free-client
|
executable cookbook-using-free-client
|
||||||
|
|
|
@ -199,7 +199,7 @@ parsers in the hope that the ones that should will always error out so
|
||||||
you can try until the right one returns a value.)
|
you can try until the right one returns a value.)
|
||||||
|
|
||||||
[servant-exceptions](https://github.com/ch1bo/servant-exceptions) is
|
[servant-exceptions](https://github.com/ch1bo/servant-exceptions) is
|
||||||
another shot at the problem. It is inspired by
|
another shot at at the problem. It is inspired by
|
||||||
servant-checked-exceptions, so it may be worth taking a closer look.
|
servant-checked-exceptions, so it may be worth taking a closer look.
|
||||||
The README claims that
|
The README claims that
|
||||||
[cardano-sl](https://github.com/input-output-hk/cardano-sl) also has
|
[cardano-sl](https://github.com/input-output-hk/cardano-sl) also has
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: cookbook-uverb
|
name: cookbook-uverb
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
synopsis: How to use the 'UVerb' type.
|
synopsis: How to use the 'UVerb' type.
|
||||||
description: Listing alternative responses and exceptions in your API types.
|
description: Listing alternative responses and exceptions in your API types.
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: ../../../servant/LICENSE
|
license-file: ../../../servant/LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
category: Servant
|
category: Servant
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with: GHC==8.6.5, GHC==8.8.4, GHC==8.10.7
|
tested-with: GHC==8.6.5, GHC==8.8.4, GHC==8.10.7
|
||||||
|
|
||||||
executable cookbook-uverb
|
executable cookbook-uverb
|
||||||
|
|
|
@ -12,7 +12,7 @@ Helpful Links
|
||||||
`https://github.com/haskell-servant/servant/issues <https://github.com/haskell-servant/servant/issues>`_
|
`https://github.com/haskell-servant/servant/issues <https://github.com/haskell-servant/servant/issues>`_
|
||||||
|
|
||||||
- the irc channel:
|
- the irc channel:
|
||||||
`#haskell-servant on libera.chat <https://web.libera.chat/#haskell-servant>`_
|
``#servant`` on freenode
|
||||||
|
|
||||||
- the mailing list:
|
- the mailing list:
|
||||||
`groups.google.com/forum/#!forum/haskell-servant <https://groups.google.com/forum/#!forum/haskell-servant>`_
|
`groups.google.com/forum/#!forum/haskell-servant <https://groups.google.com/forum/#!forum/haskell-servant>`_
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
recommonmark==0.5.0
|
recommonmark==0.5.0
|
||||||
Sphinx==1.8.4
|
Sphinx==1.8.4
|
||||||
sphinx_rtd_theme>=0.4.2
|
sphinx_rtd_theme>=0.4.2
|
||||||
jinja2<3.1.0
|
|
||||||
|
|
|
@ -40,29 +40,3 @@ nix
|
||||||
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
|
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
|
||||||
the `nix/shell.nix` file in the repository and use it to provision a suitable
|
the `nix/shell.nix` file in the repository and use it to provision a suitable
|
||||||
environment to build and run the examples.
|
environment to build and run the examples.
|
||||||
|
|
||||||
Note for Ubuntu users
|
|
||||||
--------
|
|
||||||
|
|
||||||
Ubuntu's packages for `ghc`, `cabal`, and `stack` are years out of date.
|
|
||||||
If the instructions above fail for you,
|
|
||||||
try replacing the Ubuntu packages with up-to-date versions.
|
|
||||||
First remove the installed versions:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# remove the obsolete versions
|
|
||||||
$ sudo apt remove ghc haskell-stack cabal-install
|
|
||||||
|
|
||||||
Then install fresh versions of the Haskell toolchain
|
|
||||||
using the `ghcup <https://www.haskell.org/ghcup/install/>`_ installer.
|
|
||||||
|
|
||||||
As of February 2022, one easy way to do this is by running a bootstrap script:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
|
||||||
|
|
||||||
The script is interactive and will prompt you for details about what
|
|
||||||
you want installed and where. To install manually,
|
|
||||||
see `the detailed instructions <https://www.haskell.org/ghcup/install/#manual-install>`_.
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: tutorial
|
name: tutorial
|
||||||
version: 0.10
|
version: 0.10
|
||||||
synopsis: The servant tutorial
|
synopsis: The servant tutorial
|
||||||
|
@ -7,11 +6,12 @@ description:
|
||||||
<http://docs.servant.dev/>
|
<http://docs.servant.dev/>
|
||||||
homepage: http://docs.servant.dev/
|
homepage: http://docs.servant.dev/
|
||||||
category: Servant, Documentation
|
category: Servant, Documentation
|
||||||
license: BSD-3-Clause
|
license: BSD3
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Servant Contributors
|
author: Servant Contributors
|
||||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
tested-with:
|
tested-with:
|
||||||
GHC==8.6.5
|
GHC==8.6.5
|
||||||
GHC==8.8.3, GHC ==8.10.7
|
GHC==8.8.3, GHC ==8.10.7
|
||||||
|
@ -64,7 +64,7 @@ library
|
||||||
, blaze-markup >= 0.8.0.0 && < 0.9
|
, blaze-markup >= 0.8.0.0 && < 0.9
|
||||||
, cookie >= 0.4.3 && < 0.5
|
, cookie >= 0.4.3 && < 0.5
|
||||||
, js-jquery >= 3.3.1 && < 3.4
|
, js-jquery >= 3.3.1 && < 3.4
|
||||||
, lucid >= 2.9.11 && < 2.12
|
, lucid >= 2.9.11 && < 2.10
|
||||||
, random >= 1.1 && < 1.3
|
, random >= 1.1 && < 1.3
|
||||||
, servant-js >= 0.9 && < 0.10
|
, servant-js >= 0.9 && < 0.10
|
||||||
, time >= 1.6.0.1 && < 1.13
|
, time >= 1.6.0.1 && < 1.13
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
servant-auth-server/README.lhs
|
|
|
@ -1 +0,0 @@
|
||||||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
||||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [0.4.1.0] - 2020-10-06
|
|
||||||
|
|
||||||
- Support generic Bearer token auth
|
|
||||||
|
|
||||||
## [0.4.0.0] - 2019-03-08
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- #145 Support servant-0.16 in tests @domenkozar
|
|
||||||
- #145 Drop GHC 7.10 support @domenkozar
|
|
||||||
|
|
||||||
## [0.3.3.0] - 2018-06-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Support for GHC 8.4 by @phadej
|
|
||||||
- Support for servant-0.14 by @phadej
|
|
||||||
- Changelog by @domenkozar
|
|
|
@ -1,31 +0,0 @@
|
||||||
Copyright Julian K. Arni (c) 2015
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of Julian K. Arni nor the names of other
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Distribution.Simple
|
|
||||||
main = defaultMain
|
|
|
@ -1,80 +0,0 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: servant-auth-client
|
|
||||||
version: 0.4.1.0
|
|
||||||
synopsis: servant-client/servant-auth compatibility
|
|
||||||
description: This package provides instances that allow generating clients from
|
|
||||||
<https://hackage.haskell.org/package/servant servant>
|
|
||||||
APIs that use
|
|
||||||
<https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
|
|
||||||
.
|
|
||||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
|
||||||
category: Web, Servant, Authentication
|
|
||||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
|
||||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
|
||||||
author: Julian K. Arni
|
|
||||||
maintainer: jkarni@gmail.com
|
|
||||||
copyright: (c) Julian K. Arni
|
|
||||||
license: BSD-3-Clause
|
|
||||||
license-file: LICENSE
|
|
||||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
|
||||||
build-type: Simple
|
|
||||||
extra-source-files:
|
|
||||||
CHANGELOG.md
|
|
||||||
|
|
||||||
source-repository head
|
|
||||||
type: git
|
|
||||||
location: https://github.com/haskell-servant/servant
|
|
||||||
|
|
||||||
library
|
|
||||||
hs-source-dirs:
|
|
||||||
src
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-depends:
|
|
||||||
base >= 4.10 && < 4.18
|
|
||||||
, bytestring >= 0.10.6.0 && < 0.12
|
|
||||||
, containers >= 0.5.6.2 && < 0.7
|
|
||||||
, servant-auth == 0.4.*
|
|
||||||
, servant >= 0.13 && < 0.20
|
|
||||||
, servant-client-core >= 0.13 && < 0.20
|
|
||||||
|
|
||||||
exposed-modules:
|
|
||||||
Servant.Auth.Client
|
|
||||||
Servant.Auth.Client.Internal
|
|
||||||
default-language: Haskell2010
|
|
||||||
|
|
||||||
test-suite spec
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
main-is: Spec.hs
|
|
||||||
hs-source-dirs:
|
|
||||||
test
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
|
||||||
|
|
||||||
-- dependencies with bounds inherited from the library stanza
|
|
||||||
build-depends:
|
|
||||||
base
|
|
||||||
, servant-client
|
|
||||||
, servant-auth
|
|
||||||
, servant
|
|
||||||
, servant-auth-client
|
|
||||||
|
|
||||||
-- test dependencies
|
|
||||||
build-depends:
|
|
||||||
hspec >= 2.5.5 && < 2.10
|
|
||||||
, QuickCheck >= 2.11.3 && < 2.15
|
|
||||||
, aeson >= 1.3.1.1 && < 3
|
|
||||||
, bytestring >= 0.10.6.0 && < 0.12
|
|
||||||
, http-client >= 0.5.13.1 && < 0.8
|
|
||||||
, http-types >= 0.12.2 && < 0.13
|
|
||||||
, servant-auth-server >= 0.4.2.0 && < 0.5
|
|
||||||
, servant-server >= 0.13 && < 0.20
|
|
||||||
, time >= 1.5.0.1 && < 1.13
|
|
||||||
, transformers >= 0.4.2.0 && < 0.6
|
|
||||||
, wai >= 3.2.1.2 && < 3.3
|
|
||||||
, warp >= 3.2.25 && < 3.4
|
|
||||||
, jose >= 0.10 && < 0.11
|
|
||||||
other-modules:
|
|
||||||
Servant.Auth.ClientSpec
|
|
||||||
default-language: Haskell2010
|
|
|
@ -1,3 +0,0 @@
|
||||||
module Servant.Auth.Client (Token(..), Bearer) where
|
|
||||||
|
|
||||||
import Servant.Auth.Client.Internal (Bearer, Token(..))
|
|
|
@ -1,64 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
#if __GLASGOW_HASKELL__ == 800
|
|
||||||
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
|
||||||
#endif
|
|
||||||
module Servant.Auth.Client.Internal where
|
|
||||||
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import Data.Monoid
|
|
||||||
import Data.Proxy (Proxy (..))
|
|
||||||
import Data.String (IsString)
|
|
||||||
import GHC.Exts (Constraint)
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Servant.API ((:>))
|
|
||||||
import Servant.Auth
|
|
||||||
|
|
||||||
import Servant.Client.Core
|
|
||||||
import Data.Sequence ((<|))
|
|
||||||
|
|
||||||
-- | A simple bearer token.
|
|
||||||
newtype Token = Token { getToken :: BS.ByteString }
|
|
||||||
deriving (Eq, Show, Read, Generic, IsString)
|
|
||||||
|
|
||||||
type family HasBearer xs :: Constraint where
|
|
||||||
HasBearer (Bearer ': xs) = ()
|
|
||||||
HasBearer (JWT ': xs) = ()
|
|
||||||
HasBearer (x ': xs) = HasBearer xs
|
|
||||||
HasBearer '[] = BearerAuthNotEnabled
|
|
||||||
|
|
||||||
class BearerAuthNotEnabled
|
|
||||||
|
|
||||||
-- | @'HasBearer' auths@ is nominally a redundant constraint, but ensures we're not
|
|
||||||
-- trying to send a token to an API that doesn't accept them.
|
|
||||||
instance (HasBearer auths, HasClient m api) => HasClient m (Auth auths a :> api) where
|
|
||||||
type Client m (Auth auths a :> api) = Token -> Client m api
|
|
||||||
|
|
||||||
clientWithRoute m _ req (Token token)
|
|
||||||
= clientWithRoute m (Proxy :: Proxy api)
|
|
||||||
$ req { requestHeaders = ("Authorization", headerVal) <| requestHeaders req }
|
|
||||||
where
|
|
||||||
headerVal = "Bearer " <> token
|
|
||||||
|
|
||||||
#if MIN_VERSION_servant_client_core(0,14,0)
|
|
||||||
hoistClientMonad pm _ nt cl = hoistClientMonad pm (Proxy :: Proxy api) nt . cl
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
-- * Authentication combinators
|
|
||||||
|
|
||||||
-- | A Bearer token in the Authorization header:
|
|
||||||
--
|
|
||||||
-- @Authorization: Bearer <token>@
|
|
||||||
--
|
|
||||||
-- This can be any token recognized by the server, for example,
|
|
||||||
-- a JSON Web Token (JWT).
|
|
||||||
--
|
|
||||||
-- Note that, since the exact way the token is validated is not specified,
|
|
||||||
-- this combinator can only be used in the client. The server would not know
|
|
||||||
-- how to validate it, while the client does not care.
|
|
||||||
-- If you want to implement Bearer authentication in your server, you have to
|
|
||||||
-- choose a specific combinator, such as 'JWT'.
|
|
||||||
data Bearer
|
|
|
@ -1,161 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# LANGUAGE DeriveAnyClass #-}
|
|
||||||
module Servant.Auth.ClientSpec (spec) where
|
|
||||||
|
|
||||||
import Crypto.JOSE (JWK,
|
|
||||||
KeyMaterialGenParam (OctGenParam),
|
|
||||||
genJWK)
|
|
||||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
|
||||||
import qualified Data.ByteString.Lazy as BSL
|
|
||||||
import Data.Time (UTCTime, defaultTimeLocale,
|
|
||||||
parseTimeOrError)
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Network.HTTP.Client (Manager, defaultManagerSettings,
|
|
||||||
newManager)
|
|
||||||
import Network.HTTP.Types (status401)
|
|
||||||
import Network.Wai.Handler.Warp (testWithApplication)
|
|
||||||
import Servant
|
|
||||||
import Servant.Client (BaseUrl (..), Scheme (Http),
|
|
||||||
ClientError (FailureResponse),
|
|
||||||
#if MIN_VERSION_servant_client(0,16,0)
|
|
||||||
ResponseF(..),
|
|
||||||
#elif MIN_VERSION_servant_client(0,13,0)
|
|
||||||
GenResponse(..),
|
|
||||||
#elif MIN_VERSION_servant_client(0,12,0)
|
|
||||||
Response(..),
|
|
||||||
#endif
|
|
||||||
client)
|
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
|
||||||
import Test.Hspec
|
|
||||||
import Test.QuickCheck
|
|
||||||
|
|
||||||
#if MIN_VERSION_servant_client(0,13,0)
|
|
||||||
import Servant.Client (mkClientEnv, runClientM)
|
|
||||||
#elif MIN_VERSION_servant_client(0,9,0)
|
|
||||||
import Servant.Client (ClientEnv (..), runClientM)
|
|
||||||
#else
|
|
||||||
import Control.Monad.Trans.Except (runExceptT)
|
|
||||||
#endif
|
|
||||||
#if !MIN_VERSION_servant_server(0,16,0)
|
|
||||||
#define ClientError ServantError
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Servant.Auth.Client
|
|
||||||
import Servant.Auth.Server
|
|
||||||
import Servant.Auth.Server.SetCookieOrphan ()
|
|
||||||
|
|
||||||
spec :: Spec
|
|
||||||
spec = describe "The JWT combinator" $ do
|
|
||||||
hasClientSpec
|
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * HasClient {{{
|
|
||||||
|
|
||||||
hasClientSpec :: Spec
|
|
||||||
hasClientSpec = describe "HasClient" $ around (testWithApplication $ return app) $ do
|
|
||||||
|
|
||||||
let mkTok :: User -> Maybe UTCTime -> IO Token
|
|
||||||
mkTok user mexp = do
|
|
||||||
Right tok <- makeJWT user jwtCfg mexp
|
|
||||||
return $ Token $ BSL.toStrict tok
|
|
||||||
|
|
||||||
it "succeeds when the token does not have expiry" $ \port -> property $ \user -> do
|
|
||||||
tok <- mkTok user Nothing
|
|
||||||
v <- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
|
||||||
v `shouldBe` Right (length $ name user)
|
|
||||||
|
|
||||||
it "succeeds when the token is not expired" $ \port -> property $ \user -> do
|
|
||||||
tok <- mkTok user (Just future)
|
|
||||||
v <- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
|
||||||
v `shouldBe` Right (length $ name user)
|
|
||||||
|
|
||||||
it "fails when token is expired" $ \port -> property $ \user -> do
|
|
||||||
tok <- mkTok user (Just past)
|
|
||||||
#if MIN_VERSION_servant_client(0,16,0)
|
|
||||||
Left (FailureResponse _ (Response stat _ _ _))
|
|
||||||
#elif MIN_VERSION_servant_client(0,12,0)
|
|
||||||
Left (FailureResponse (Response stat _ _ _))
|
|
||||||
#elif MIN_VERSION_servant_client(0,11,0)
|
|
||||||
Left (FailureResponse _ stat _ _)
|
|
||||||
#else
|
|
||||||
Left (FailureResponse stat _ _)
|
|
||||||
#endif
|
|
||||||
<- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
|
||||||
stat `shouldBe` status401
|
|
||||||
|
|
||||||
|
|
||||||
getIntClient :: Token -> Manager -> BaseUrl -> IO (Either ClientError Int)
|
|
||||||
#if MIN_VERSION_servant(0,13,0)
|
|
||||||
getIntClient tok m burl = runClientM (client api tok) (mkClientEnv m burl)
|
|
||||||
#elif MIN_VERSION_servant(0,9,0)
|
|
||||||
getIntClient tok m burl = runClientM (client api tok) (ClientEnv m burl)
|
|
||||||
#else
|
|
||||||
getIntClient tok m burl = runExceptT $ client api tok m burl
|
|
||||||
#endif
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * API and Server {{{
|
|
||||||
|
|
||||||
type API = Auth '[JWT] User :> Get '[JSON] Int
|
|
||||||
|
|
||||||
api :: Proxy API
|
|
||||||
api = Proxy
|
|
||||||
|
|
||||||
theKey :: JWK
|
|
||||||
theKey = unsafePerformIO . genJWK $ OctGenParam 256
|
|
||||||
{-# NOINLINE theKey #-}
|
|
||||||
|
|
||||||
mgr :: Manager
|
|
||||||
mgr = unsafePerformIO $ newManager defaultManagerSettings
|
|
||||||
{-# NOINLINE mgr #-}
|
|
||||||
|
|
||||||
app :: Application
|
|
||||||
app = serveWithContext api ctx server
|
|
||||||
where
|
|
||||||
ctx = cookieCfg :. jwtCfg :. EmptyContext
|
|
||||||
|
|
||||||
jwtCfg :: JWTSettings
|
|
||||||
jwtCfg = defaultJWTSettings theKey
|
|
||||||
|
|
||||||
cookieCfg :: CookieSettings
|
|
||||||
cookieCfg = defaultCookieSettings
|
|
||||||
|
|
||||||
|
|
||||||
server :: Server API
|
|
||||||
server = getInt
|
|
||||||
where
|
|
||||||
getInt :: AuthResult User -> Handler Int
|
|
||||||
getInt (Authenticated u) = return . length $ name u
|
|
||||||
getInt _ = throwAll err401
|
|
||||||
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Utils {{{
|
|
||||||
|
|
||||||
past :: UTCTime
|
|
||||||
past = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "1970-01-01"
|
|
||||||
|
|
||||||
future :: UTCTime
|
|
||||||
future = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "2070-01-01"
|
|
||||||
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Types {{{
|
|
||||||
|
|
||||||
data User = User
|
|
||||||
{ name :: String
|
|
||||||
, _id :: String
|
|
||||||
} deriving (Eq, Show, Read, Generic)
|
|
||||||
|
|
||||||
instance FromJWT User
|
|
||||||
instance ToJWT User
|
|
||||||
instance FromJSON User
|
|
||||||
instance ToJSON User
|
|
||||||
|
|
||||||
instance Arbitrary User where
|
|
||||||
arbitrary = User <$> arbitrary <*> arbitrary
|
|
||||||
|
|
||||||
-- }}}
|
|
|
@ -1 +0,0 @@
|
||||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
|
|
@ -1 +0,0 @@
|
||||||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
|
|
@ -1,14 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
||||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [0.2.10.0] - 2018-06-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Support for GHC 8.4 by @phadej
|
|
||||||
- Changelog by @domenkozar
|
|
|
@ -1,31 +0,0 @@
|
||||||
Copyright Julian K. Arni (c) 2015
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of Julian K. Arni nor the names of other
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# OPTIONS_GHC -Wall #-}
|
|
||||||
module Main (main) where
|
|
||||||
|
|
||||||
#ifndef MIN_VERSION_cabal_doctest
|
|
||||||
#define MIN_VERSION_cabal_doctest(x,y,z) 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if MIN_VERSION_cabal_doctest(1,0,0)
|
|
||||||
|
|
||||||
import Distribution.Extra.Doctest ( defaultMainWithDoctests )
|
|
||||||
main :: IO ()
|
|
||||||
main = defaultMainWithDoctests "doctests"
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
#ifdef MIN_VERSION_Cabal
|
|
||||||
-- If the macro is defined, we have new cabal-install,
|
|
||||||
-- but for some reason we don't have cabal-doctest in package-db
|
|
||||||
--
|
|
||||||
-- Probably we are running cabal sdist, when otherwise using new-build
|
|
||||||
-- workflow
|
|
||||||
#warning You are configuring this package without cabal-doctest installed. \
|
|
||||||
The doctests test-suite will not work as a result. \
|
|
||||||
To fix this, install cabal-doctest before configuring.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Distribution.Simple
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = defaultMain
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,84 +0,0 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: servant-auth-docs
|
|
||||||
version: 0.2.10.0
|
|
||||||
synopsis: servant-docs/servant-auth compatibility
|
|
||||||
description: This package provides instances that allow generating docs from
|
|
||||||
<https://hackage.haskell.org/package/servant servant>
|
|
||||||
APIs that use
|
|
||||||
<https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
|
|
||||||
.
|
|
||||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
|
||||||
category: Web, Servant, Authentication
|
|
||||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
|
||||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
|
||||||
author: Julian K. Arni
|
|
||||||
maintainer: jkarni@gmail.com
|
|
||||||
copyright: (c) Julian K. Arni
|
|
||||||
license: BSD-3-Clause
|
|
||||||
license-file: LICENSE
|
|
||||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
|
||||||
build-type: Custom
|
|
||||||
extra-source-files:
|
|
||||||
CHANGELOG.md
|
|
||||||
|
|
||||||
custom-setup
|
|
||||||
setup-depends:
|
|
||||||
base, Cabal, cabal-doctest >=1.0.6 && <1.1
|
|
||||||
|
|
||||||
source-repository head
|
|
||||||
type: git
|
|
||||||
location: https://github.com/haskell-servant/servant
|
|
||||||
|
|
||||||
library
|
|
||||||
hs-source-dirs:
|
|
||||||
src
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-depends:
|
|
||||||
base >= 4.10 && < 4.18
|
|
||||||
, servant-docs >= 0.11.2 && < 0.13
|
|
||||||
, servant >= 0.13 && < 0.20
|
|
||||||
, servant-auth == 0.4.*
|
|
||||||
, lens >= 4.16.1 && <5.3
|
|
||||||
exposed-modules:
|
|
||||||
Servant.Auth.Docs
|
|
||||||
default-language: Haskell2010
|
|
||||||
|
|
||||||
test-suite doctests
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
main-is: doctests.hs
|
|
||||||
build-depends:
|
|
||||||
base,
|
|
||||||
servant-auth-docs,
|
|
||||||
doctest >= 0.16 && < 0.21,
|
|
||||||
QuickCheck >= 2.11.3 && < 2.15,
|
|
||||||
template-haskell
|
|
||||||
ghc-options: -Wall -threaded
|
|
||||||
hs-source-dirs: test
|
|
||||||
default-language: Haskell2010
|
|
||||||
|
|
||||||
test-suite spec
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
main-is: Spec.hs
|
|
||||||
hs-source-dirs:
|
|
||||||
test
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
|
||||||
|
|
||||||
-- dependencies with bounds inherited from the library stanza
|
|
||||||
build-depends:
|
|
||||||
base
|
|
||||||
, text
|
|
||||||
, servant-docs
|
|
||||||
, servant
|
|
||||||
, servant-auth
|
|
||||||
, lens
|
|
||||||
|
|
||||||
-- test dependencies
|
|
||||||
build-depends:
|
|
||||||
servant-auth-docs
|
|
||||||
, hspec >= 2.5.5 && < 2.10
|
|
||||||
, QuickCheck >= 2.11.3 && < 2.15
|
|
||||||
|
|
||||||
default-language: Haskell2010
|
|
|
@ -1,96 +0,0 @@
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
module Servant.Auth.Docs
|
|
||||||
(
|
|
||||||
-- | The purpose of this package is provide the instance for 'servant-auth'
|
|
||||||
-- combinators needed for 'servant-docs' documentation generation.
|
|
||||||
--
|
|
||||||
-- >>> type API = Auth '[JWT, Cookie, BasicAuth] Int :> Get '[JSON] Int
|
|
||||||
-- >>> putStr $ markdown $ docs (Proxy :: Proxy API)
|
|
||||||
-- ## GET /
|
|
||||||
-- ...
|
|
||||||
-- ... Authentication
|
|
||||||
-- ...
|
|
||||||
-- This part of the API is protected by the following authentication mechanisms:
|
|
||||||
-- ...
|
|
||||||
-- * JSON Web Tokens ([JWTs](https://en.wikipedia.org/wiki/JSON_Web_Token))
|
|
||||||
-- * [Cookies](https://en.wikipedia.org/wiki/HTTP_cookie)
|
|
||||||
-- * [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
|
||||||
-- ...
|
|
||||||
-- Clients must supply the following data
|
|
||||||
-- ...
|
|
||||||
-- One of the following:
|
|
||||||
-- ...
|
|
||||||
-- * A JWT Token signed with this server's key
|
|
||||||
-- * Cookies automatically set by browsers, plus a header
|
|
||||||
-- * Cookies automatically set by browsers, plus a header
|
|
||||||
-- ...
|
|
||||||
|
|
||||||
-- * Re-export
|
|
||||||
JWT
|
|
||||||
, BasicAuth
|
|
||||||
, Cookie
|
|
||||||
, Auth
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Control.Lens ((%~), (&), (|>))
|
|
||||||
import Data.List (intercalate)
|
|
||||||
import Data.Monoid
|
|
||||||
import Data.Proxy (Proxy (Proxy))
|
|
||||||
import Servant.API hiding (BasicAuth)
|
|
||||||
import Servant.Auth
|
|
||||||
import Servant.Docs hiding (pretty)
|
|
||||||
import Servant.Docs.Internal (DocAuthentication (..), authInfo)
|
|
||||||
|
|
||||||
instance (AllDocs auths, HasDocs api) => HasDocs (Auth auths r :> api) where
|
|
||||||
docsFor _ (endpoint, action) =
|
|
||||||
docsFor (Proxy :: Proxy api) (endpoint, action & authInfo %~ (|> info))
|
|
||||||
where
|
|
||||||
(intro, reqData) = pretty $ allDocs (Proxy :: Proxy auths)
|
|
||||||
info = DocAuthentication intro reqData
|
|
||||||
|
|
||||||
|
|
||||||
pretty :: [(String, String)] -> (String, String)
|
|
||||||
pretty [] = error "shouldn't happen"
|
|
||||||
pretty [(i, d)] =
|
|
||||||
( "This part of the API is protected by " <> i
|
|
||||||
, d
|
|
||||||
)
|
|
||||||
pretty rs =
|
|
||||||
( "This part of the API is protected by the following authentication mechanisms:\n\n"
|
|
||||||
++ " * " <> intercalate "\n * " (fst <$> rs)
|
|
||||||
, "\nOne of the following:\n\n"
|
|
||||||
++ " * " <> intercalate "\n * " (snd <$> rs)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AllDocs (x :: [*]) where
|
|
||||||
allDocs :: proxy x
|
|
||||||
-- intro, req
|
|
||||||
-> [(String, String)]
|
|
||||||
|
|
||||||
instance (OneDoc a, AllDocs as) => AllDocs (a ': as) where
|
|
||||||
allDocs _ = oneDoc (Proxy :: Proxy a) : allDocs (Proxy :: Proxy as)
|
|
||||||
|
|
||||||
instance AllDocs '[] where
|
|
||||||
allDocs _ = []
|
|
||||||
|
|
||||||
class OneDoc a where
|
|
||||||
oneDoc :: proxy a -> (String, String)
|
|
||||||
|
|
||||||
instance OneDoc JWT where
|
|
||||||
oneDoc _ =
|
|
||||||
("JSON Web Tokens ([JWTs](https://en.wikipedia.org/wiki/JSON_Web_Token))"
|
|
||||||
, "A JWT Token signed with this server's key")
|
|
||||||
|
|
||||||
instance OneDoc Cookie where
|
|
||||||
oneDoc _ =
|
|
||||||
("[Cookies](https://en.wikipedia.org/wiki/HTTP_cookie)"
|
|
||||||
, "Cookies automatically set by browsers, plus a header")
|
|
||||||
|
|
||||||
instance OneDoc BasicAuth where
|
|
||||||
oneDoc _ =
|
|
||||||
( "[Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)"
|
|
||||||
, "Cookies automatically set by browsers, plus a header")
|
|
||||||
|
|
||||||
-- $setup
|
|
||||||
-- >>> instance ToSample Int where toSamples _ = singleSample 1729
|
|
|
@ -1 +0,0 @@
|
||||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
|
|
@ -1,12 +0,0 @@
|
||||||
module Main where
|
|
||||||
|
|
||||||
import Build_doctests (flags, pkgs, module_sources)
|
|
||||||
import Data.Foldable (traverse_)
|
|
||||||
import Test.DocTest
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
traverse_ putStrLn args
|
|
||||||
doctest args
|
|
||||||
where
|
|
||||||
args = flags ++ pkgs ++ module_sources
|
|
|
@ -1 +0,0 @@
|
||||||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
|
|
@ -1,130 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
||||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [0.4.6.0] - 2020-10-06
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- expose verifyJWT and use it in two places [@domenkozar]
|
|
||||||
- support GHC 8.10 [@domenkozar]
|
|
||||||
- move ToJWT/FromJWT to servant-auth [@erewok]
|
|
||||||
- #165 fix AnySite with Cookie 3.5.0 [@odr]
|
|
||||||
|
|
||||||
## [0.4.5.1] - 2020-02-06
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- #158 servant 0.17 support [@phadej]
|
|
||||||
|
|
||||||
## [0.4.5.0] - 2019-12-28
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- #144 servant 0.16 support and drop GHC 7.10 support [@domenkozar]
|
|
||||||
- #148 removed unused constaint in HasServer instance for Auth
|
|
||||||
- #154 GHC 8.8 support [@phadej]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- #141 Support Stream combinator [@domenkozar]
|
|
||||||
- #143 Allow servant-0.16 [@phadej]
|
|
||||||
|
|
||||||
## [0.4.4.0] - 2019-03-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- #141 Support Stream combinator [@domenkozar]
|
|
||||||
- #143 Allow servant-0.16 [@phadej]
|
|
||||||
|
|
||||||
## [0.4.3.0] - 2019-01-17
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- #117 Avoid running auth checks unnecessarily [@sopvop]
|
|
||||||
- #110 Get rid of crypto-api dependency [@domenkozar]
|
|
||||||
- #130 clearSession: improve cross-browser compatibility [@domenkozar]
|
|
||||||
- #136 weed out bytestring-conversion [@stephenirl]
|
|
||||||
|
|
||||||
## [0.4.2.0] - 2018-11-05
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- `Headers hs a` instance for AddSetCookieApi [@domenkozar]
|
|
||||||
- GHC 8.6.x support [@domenkozar]
|
|
||||||
|
|
||||||
## [0.4.1.0] - 2018-10-05
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- #125 Allow setting domain name for a cookie [@domenkozar]
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- bump http-api-data to 0.3.10 that includes Cookie orphan instances previously located in servant-auth-server [@phadej]
|
|
||||||
- #114 Export `HasSecurity` typeclass [@rockbmb]
|
|
||||||
|
|
||||||
## [0.4.0.1] - 2018-09-23
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- #123 Session cookie did not apply SameSite attribute [@domenkozar]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- #112 HasLink instance for Auth combinator [@adetokunbo]
|
|
||||||
- #111 Documentation for using hoistServer [@mschristiansen]
|
|
||||||
- #107 Add utility functions for reading and writing a key to a file [@mschristiansen]
|
|
||||||
|
|
||||||
## [0.4.0.0] - 2018-06-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Support GHC 8.4 by @phadej and @domenkozar
|
|
||||||
- Support for servant-0.14 by @phadej
|
|
||||||
- #96 Support for jose-0.7 by @xaviershay
|
|
||||||
- #92 add `clearSession` for logout by @plredmond and @3noch
|
|
||||||
- #95 makeJWT: allow setting Alg via defaultJWTSettings by @domenkozar
|
|
||||||
- #89 Validate JWT against a JWKSet instead of JWK by @sopvop
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- #92 Rename CSRF to XSRF by @plredmond and @3noch
|
|
||||||
- #92 extract 'XsrfCookieSettings' from 'CookieSettings' and make XSRF checking optional
|
|
||||||
by @plredmond and @3noch
|
|
||||||
- #69 export SameSite by @domenkozar
|
|
||||||
- #102 Reuse Servant.Api.IsSecure instead of duplicating ADT by @domenkozar
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
- #92 Renamed 'makeCsrfCookie' to 'makeXsrfCookie' and marked the former as deprecated
|
|
||||||
by @plredmond and @3noc
|
|
||||||
- #92 Made several changes to the structure of 'CookieSettings' which will require
|
|
||||||
attention by users who have modified the XSRF settings by @plredmond and @3noch
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- #94 Force cookie expiration on serverside by @karshan
|
|
||||||
|
|
||||||
## [0.3.2.0] - 2018-02-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- #76 Export wwwAuthenticatedErr and elaborate its annotation by @defanor
|
|
||||||
- Support for servant-0.14 by @phadej
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Disable the readme executable for ghcjs builds by @hamishmack
|
|
||||||
- #84 Make AddSetCookieApi type family open by @qnikst
|
|
||||||
- #79 Make CSRF checks optional for GET requests by @harendra-kumar
|
|
||||||
|
|
||||||
## [0.3.1.0] - 2017-11-08
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Support for servant-0.12 by @phadej
|
|
||||||
|
|
||||||
## [0.3.0.0] - 2017-11-07
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- #47 'cookiePath' and 'xsrfCookiePath' added to 'CookieSettings' by @mchaver
|
|
||||||
|
|
||||||
## [0.2.8.0] - 2017-05-26
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- #45 Support for servant-0.11 by @phadej
|
|
||||||
|
|
||||||
## [0.2.7.0] - 2017-02-11
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- #27 #41 'acceptLogin' and 'makeCsrfCookie' functions by @bts
|
|
|
@ -1,31 +0,0 @@
|
||||||
Copyright Julian K. Arni (c) 2015
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of Julian K. Arni nor the names of other
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
|
@ -1,291 +0,0 @@
|
||||||
# servant-auth
|
|
||||||
|
|
||||||
These packages provides safe and easy-to-use authentication options for
|
|
||||||
`servant`. The same API can be protected via:
|
|
||||||
- basicauth
|
|
||||||
- cookies
|
|
||||||
- JWT tokens
|
|
||||||
|
|
||||||
|
|
||||||
| Package | Hackage |
|
|
||||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| servant-auth | [![servant-auth](https://img.shields.io/hackage/v/servant-auth?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth) |
|
|
||||||
| servant-auth-server | [![servant-auth-server](https://img.shields.io/hackage/v/servant-auth-server.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-server) |
|
|
||||||
| servant-auth-client | [![servant-auth-client](https://img.shields.io/hackage/v/servant-auth-client.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-client) |
|
|
||||||
| servant-auth-swagger | [![servant-auth-swagger](https://img.shields.io/hackage/v/servant-auth-swagger.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-swagger) |
|
|
||||||
| servant-auth-docs | [![servant-auth-docs](https://img.shields.io/hackage/v/servant-auth-docs.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-docs) |
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
First some imports:
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
{-# OPTIONS_GHC -fno-warn-unused-binds #-}
|
|
||||||
{-# OPTIONS_GHC -fno-warn-deprecations #-}
|
|
||||||
import Control.Concurrent (forkIO)
|
|
||||||
import Control.Monad (forever)
|
|
||||||
import Control.Monad.Trans (liftIO)
|
|
||||||
import Data.Aeson (FromJSON, ToJSON)
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Network.Wai.Handler.Warp (run)
|
|
||||||
import System.Environment (getArgs)
|
|
||||||
import Servant
|
|
||||||
import Servant.Auth.Server
|
|
||||||
import Servant.Auth.Server.SetCookieOrphan ()
|
|
||||||
~~~
|
|
||||||
|
|
||||||
`servant-auth` library introduces a combinator `Auth`:
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
data Auth (auths :: [*]) val
|
|
||||||
~~~
|
|
||||||
|
|
||||||
What `Auth [Auth1, Auth2] Something :> API` means is that `API` is protected by
|
|
||||||
*either* `Auth1` *or* `Auth2`, and the result of authentication will be of type
|
|
||||||
`AuthResult Something`, where :
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
data AuthResult val
|
|
||||||
= BadPassword
|
|
||||||
| NoSuchUser
|
|
||||||
| Authenticated val
|
|
||||||
| Indefinite
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Your handlers will get a value of type `AuthResult Something`, and can decide
|
|
||||||
what to do with it.
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
|
|
||||||
data User = User { name :: String, email :: String }
|
|
||||||
deriving (Eq, Show, Read, Generic)
|
|
||||||
|
|
||||||
instance ToJSON User
|
|
||||||
instance ToJWT User
|
|
||||||
instance FromJSON User
|
|
||||||
instance FromJWT User
|
|
||||||
|
|
||||||
data Login = Login { username :: String, password :: String }
|
|
||||||
deriving (Eq, Show, Read, Generic)
|
|
||||||
|
|
||||||
instance ToJSON Login
|
|
||||||
instance FromJSON Login
|
|
||||||
|
|
||||||
type Protected
|
|
||||||
= "name" :> Get '[JSON] String
|
|
||||||
:<|> "email" :> Get '[JSON] String
|
|
||||||
|
|
||||||
|
|
||||||
-- | 'Protected' will be protected by 'auths', which we still have to specify.
|
|
||||||
protected :: Servant.Auth.Server.AuthResult User -> Server Protected
|
|
||||||
-- If we get an "Authenticated v", we can trust the information in v, since
|
|
||||||
-- it was signed by a key we trust.
|
|
||||||
protected (Servant.Auth.Server.Authenticated user) = return (name user) :<|> return (email user)
|
|
||||||
-- Otherwise, we return a 401.
|
|
||||||
protected _ = throwAll err401
|
|
||||||
|
|
||||||
type Unprotected =
|
|
||||||
"login"
|
|
||||||
:> ReqBody '[JSON] Login
|
|
||||||
:> Verb 'POST 204 '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
|
|
||||||
, Header "Set-Cookie" SetCookie]
|
|
||||||
NoContent)
|
|
||||||
:<|> Raw
|
|
||||||
|
|
||||||
unprotected :: CookieSettings -> JWTSettings -> Server Unprotected
|
|
||||||
unprotected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"
|
|
||||||
|
|
||||||
type API auths = (Servant.Auth.Server.Auth auths User :> Protected) :<|> Unprotected
|
|
||||||
|
|
||||||
server :: CookieSettings -> JWTSettings -> Server (API auths)
|
|
||||||
server cs jwts = protected :<|> unprotected cs jwts
|
|
||||||
|
|
||||||
~~~
|
|
||||||
|
|
||||||
The code is common to all authentications. In order to pick one or more specific
|
|
||||||
authentication methods, all we need to do is provide the expect configuration
|
|
||||||
parameters.
|
|
||||||
|
|
||||||
## API tokens
|
|
||||||
|
|
||||||
The following example illustrates how to protect an API with tokens.
|
|
||||||
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
-- In main, we fork the server, and allow new tokens to be created in the
|
|
||||||
-- command line for the specified user name and email.
|
|
||||||
mainWithJWT :: IO ()
|
|
||||||
mainWithJWT = do
|
|
||||||
-- We generate the key for signing tokens. This would generally be persisted,
|
|
||||||
-- and kept safely
|
|
||||||
myKey <- generateKey
|
|
||||||
-- Adding some configurations. All authentications require CookieSettings to
|
|
||||||
-- be in the context.
|
|
||||||
let jwtCfg = defaultJWTSettings myKey
|
|
||||||
cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
|
|
||||||
--- Here we actually make concrete
|
|
||||||
api = Proxy :: Proxy (API '[JWT])
|
|
||||||
_ <- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
|
|
||||||
|
|
||||||
putStrLn "Started server on localhost:7249"
|
|
||||||
putStrLn "Enter name and email separated by a space for a new token"
|
|
||||||
|
|
||||||
forever $ do
|
|
||||||
xs <- words <$> getLine
|
|
||||||
case xs of
|
|
||||||
[name', email'] -> do
|
|
||||||
etoken <- makeJWT (User name' email') jwtCfg Nothing
|
|
||||||
case etoken of
|
|
||||||
Left e -> putStrLn $ "Error generating token:t" ++ show e
|
|
||||||
Right v -> putStrLn $ "New token:\t" ++ show v
|
|
||||||
_ -> putStrLn "Expecting a name and email separated by spaces"
|
|
||||||
|
|
||||||
~~~
|
|
||||||
|
|
||||||
And indeed:
|
|
||||||
|
|
||||||
~~~ bash
|
|
||||||
|
|
||||||
./readme JWT
|
|
||||||
|
|
||||||
Started server on localhost:7249
|
|
||||||
Enter name and email separated by a space for a new token
|
|
||||||
alice alice@gmail.com
|
|
||||||
New token: "eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE"
|
|
||||||
|
|
||||||
curl localhost:7249/name -v
|
|
||||||
|
|
||||||
* Hostname was NOT found in DNS cache
|
|
||||||
* Trying 127.0.0.1...
|
|
||||||
* Connected to localhost (127.0.0.1) port 7249 (#0)
|
|
||||||
> GET /name HTTP/1.1
|
|
||||||
> User-Agent: curl/7.35.0
|
|
||||||
> Host: localhost:7249
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 401 Unauthorized
|
|
||||||
< Transfer-Encoding: chunked
|
|
||||||
< Date: Wed, 07 Sep 2016 20:17:17 GMT
|
|
||||||
* Server Warp/3.2.7 is not blacklisted
|
|
||||||
< Server: Warp/3.2.7
|
|
||||||
<
|
|
||||||
* Connection #0 to host localhost left intact
|
|
||||||
|
|
||||||
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE" \
|
|
||||||
localhost:7249/name -v
|
|
||||||
|
|
||||||
* Hostname was NOT found in DNS cache
|
|
||||||
* Trying 127.0.0.1...
|
|
||||||
* Connected to localhost (127.0.0.1) port 7249 (#0)
|
|
||||||
> GET /name HTTP/1.1
|
|
||||||
> User-Agent: curl/7.35.0
|
|
||||||
> Host: localhost:7249
|
|
||||||
> Accept: */*
|
|
||||||
> Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Transfer-Encoding: chunked
|
|
||||||
< Date: Wed, 07 Sep 2016 20:16:11 GMT
|
|
||||||
* Server Warp/3.2.7 is not blacklisted
|
|
||||||
< Server: Warp/3.2.7
|
|
||||||
< Content-Type: application/json
|
|
||||||
< Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE; HttpOnly; Secure
|
|
||||||
< Set-Cookie: XSRF-TOKEN=TWcdPnHr2QHcVyTw/TTBLQ==; Secure
|
|
||||||
<
|
|
||||||
* Connection #0 to host localhost left intact
|
|
||||||
"alice"%
|
|
||||||
|
|
||||||
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## Cookies
|
|
||||||
|
|
||||||
What if, in addition to API tokens, we want to expose our API to browsers? All
|
|
||||||
we need to do is say so!
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
mainWithCookies :: IO ()
|
|
||||||
mainWithCookies = do
|
|
||||||
-- We *also* need a key to sign the cookies
|
|
||||||
myKey <- generateKey
|
|
||||||
-- Adding some configurations. 'Cookie' requires, in addition to
|
|
||||||
-- CookieSettings, JWTSettings (for signing), so everything is just as before
|
|
||||||
let jwtCfg = defaultJWTSettings myKey
|
|
||||||
cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
|
|
||||||
--- Here is the actual change
|
|
||||||
api = Proxy :: Proxy (API '[Cookie])
|
|
||||||
run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
|
|
||||||
|
|
||||||
-- Here is the login handler
|
|
||||||
checkCreds :: CookieSettings
|
|
||||||
-> JWTSettings
|
|
||||||
-> Login
|
|
||||||
-> Handler (Headers '[ Header "Set-Cookie" SetCookie
|
|
||||||
, Header "Set-Cookie" SetCookie]
|
|
||||||
NoContent)
|
|
||||||
checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do
|
|
||||||
-- Usually you would ask a database for the user info. This is just a
|
|
||||||
-- regular servant handler, so you can follow your normal database access
|
|
||||||
-- patterns (including using 'enter').
|
|
||||||
let usr = User "Ali Baba" "ali@email.com"
|
|
||||||
mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings usr
|
|
||||||
case mApplyCookies of
|
|
||||||
Nothing -> throwError err401
|
|
||||||
Just applyCookies -> return $ applyCookies NoContent
|
|
||||||
checkCreds _ _ _ = throwError err401
|
|
||||||
~~~
|
|
||||||
|
|
||||||
### XSRF and the frontend
|
|
||||||
|
|
||||||
XSRF protection works by requiring that there be a header of the same value as
|
|
||||||
a distinguished cookie that is set by the server on each request. What the
|
|
||||||
cookie and header name are can be configured (see `xsrfCookieName` and
|
|
||||||
`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and
|
|
||||||
"X-XSRF-TOKEN". This means that, if your client is a browser and you're using
|
|
||||||
cookies, Javascript on the client must set the header of each request by
|
|
||||||
reading the cookie. For jQuery, and with the default values, that might be:
|
|
||||||
|
|
||||||
~~~ javascript
|
|
||||||
|
|
||||||
var token = (function() {
|
|
||||||
r = document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))
|
|
||||||
if (r) return r[1];
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
$.ajaxPrefilter(function(opts, origOpts, xhr) {
|
|
||||||
xhr.setRequestHeader('X-XSRF-TOKEN', token);
|
|
||||||
}
|
|
||||||
|
|
||||||
~~~
|
|
||||||
|
|
||||||
I *believe* nothing at all needs to be done if you're using Angular's `$http`
|
|
||||||
directive, but I haven't tested this.
|
|
||||||
|
|
||||||
XSRF protection can be disabled just for `GET` requests by setting
|
|
||||||
`xsrfExcludeGet = False`. You might want this if you're relying on the browser
|
|
||||||
to navigate between pages that require cookie authentication.
|
|
||||||
|
|
||||||
XSRF protection can be completely disabled by setting `cookieXsrfSetting =
|
|
||||||
Nothing` in `CookieSettings`. This is not recommended! If your cookie
|
|
||||||
authenticated web application runs any javascript, it's recommended to send the
|
|
||||||
XSRF header. However, if your web application runs no javascript, disabling
|
|
||||||
XSRF entirely may be required.
|
|
||||||
|
|
||||||
# Note on this README
|
|
||||||
|
|
||||||
This README is a literate haskell file. Here is 'main', allowing you to pick
|
|
||||||
between the examples above.
|
|
||||||
|
|
||||||
~~~ haskell
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
args <- getArgs
|
|
||||||
let usage = "Usage: readme (JWT|Cookie)"
|
|
||||||
case args of
|
|
||||||
["JWT"] -> mainWithJWT
|
|
||||||
["Cookie"] -> mainWithCookies
|
|
||||||
e -> putStrLn $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage
|
|
||||||
|
|
||||||
~~~
|
|
|
@ -1 +0,0 @@
|
||||||
README.lhs
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Distribution.Simple
|
|
||||||
main = defaultMain
|
|
|
@ -1,134 +0,0 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: servant-auth-server
|
|
||||||
version: 0.4.7.0
|
|
||||||
synopsis: servant-server/servant-auth compatibility
|
|
||||||
description: This package provides the required instances for using the @Auth@ combinator
|
|
||||||
in your 'servant' server.
|
|
||||||
.
|
|
||||||
Both cookie- and token- (REST API) based authentication is provided.
|
|
||||||
.
|
|
||||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
|
||||||
category: Web, Servant, Authentication
|
|
||||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
|
||||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
|
||||||
author: Julian K. Arni
|
|
||||||
maintainer: jkarni@gmail.com
|
|
||||||
copyright: (c) Julian K. Arni
|
|
||||||
license: BSD-3-Clause
|
|
||||||
license-file: LICENSE
|
|
||||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
|
||||||
build-type: Simple
|
|
||||||
extra-source-files:
|
|
||||||
CHANGELOG.md
|
|
||||||
|
|
||||||
source-repository head
|
|
||||||
type: git
|
|
||||||
location: https://github.com/haskell-servant/servant
|
|
||||||
|
|
||||||
library
|
|
||||||
hs-source-dirs:
|
|
||||||
src
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-depends:
|
|
||||||
base >= 4.10 && < 4.18
|
|
||||||
, aeson >= 1.0.0.1 && < 3
|
|
||||||
, base64-bytestring >= 1.0.0.1 && < 2
|
|
||||||
, blaze-builder >= 0.4.1.0 && < 0.5
|
|
||||||
, bytestring >= 0.10.6.0 && < 0.12
|
|
||||||
, case-insensitive >= 1.2.0.11 && < 1.3
|
|
||||||
, cookie >= 0.4.4 && < 0.5
|
|
||||||
, data-default-class >= 0.1.2.0 && < 0.2
|
|
||||||
, entropy >= 0.4.1.3 && < 0.5
|
|
||||||
, http-types >= 0.12.2 && < 0.13
|
|
||||||
, jose >= 0.10 && < 0.11
|
|
||||||
, lens >= 4.16.1 && < 5.3
|
|
||||||
, memory >= 0.14.16 && < 0.19
|
|
||||||
, monad-time >= 0.3.1.0 && < 0.4
|
|
||||||
, mtl ^>= 2.2.2 || ^>= 2.3.1
|
|
||||||
, servant >= 0.13 && < 0.20
|
|
||||||
, servant-auth == 0.4.*
|
|
||||||
, servant-server >= 0.13 && < 0.20
|
|
||||||
, tagged >= 0.8.4 && < 0.9
|
|
||||||
, text >= 1.2.3.0 && < 2.1
|
|
||||||
, time >= 1.5.0.1 && < 1.13
|
|
||||||
, unordered-containers >= 0.2.9.0 && < 0.3
|
|
||||||
, wai >= 3.2.1.2 && < 3.3
|
|
||||||
|
|
||||||
if impl(ghc >= 9)
|
|
||||||
build-depends:
|
|
||||||
-- base64-bytestring 1.2.1.0 contains important fix for GHC-9, lower versions
|
|
||||||
-- produce wrong results, thus corrupring JWT via jose package.
|
|
||||||
-- See: https://github.com/haskell/base64-bytestring/pull/46
|
|
||||||
base64-bytestring >= 1.2.1.0
|
|
||||||
|
|
||||||
exposed-modules:
|
|
||||||
Servant.Auth.Server
|
|
||||||
Servant.Auth.Server.Internal
|
|
||||||
Servant.Auth.Server.Internal.AddSetCookie
|
|
||||||
Servant.Auth.Server.Internal.BasicAuth
|
|
||||||
Servant.Auth.Server.Internal.Class
|
|
||||||
Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
Servant.Auth.Server.Internal.Cookie
|
|
||||||
Servant.Auth.Server.Internal.FormLogin
|
|
||||||
Servant.Auth.Server.Internal.JWT
|
|
||||||
Servant.Auth.Server.Internal.ThrowAll
|
|
||||||
Servant.Auth.Server.Internal.Types
|
|
||||||
Servant.Auth.Server.SetCookieOrphan
|
|
||||||
default-language: Haskell2010
|
|
||||||
|
|
||||||
test-suite readme
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
main-is: README.lhs
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall -pgmL markdown-unlit
|
|
||||||
build-tool-depends: markdown-unlit:markdown-unlit
|
|
||||||
build-depends:
|
|
||||||
base
|
|
||||||
, servant-auth
|
|
||||||
, servant-auth-server
|
|
||||||
, servant-server
|
|
||||||
, aeson
|
|
||||||
, mtl
|
|
||||||
, warp
|
|
||||||
default-language: Haskell2010
|
|
||||||
if impl(ghcjs)
|
|
||||||
buildable: False
|
|
||||||
|
|
||||||
test-suite spec
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
main-is: Spec.hs
|
|
||||||
hs-source-dirs:
|
|
||||||
test
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
|
||||||
|
|
||||||
-- dependencies with bounds inherited from the library stanza
|
|
||||||
build-depends:
|
|
||||||
base
|
|
||||||
, aeson
|
|
||||||
, bytestring
|
|
||||||
, case-insensitive
|
|
||||||
, jose
|
|
||||||
, lens
|
|
||||||
, mtl
|
|
||||||
, time
|
|
||||||
, http-types
|
|
||||||
, wai
|
|
||||||
, servant
|
|
||||||
, servant-server
|
|
||||||
, transformers
|
|
||||||
|
|
||||||
-- test dependencies
|
|
||||||
build-depends:
|
|
||||||
servant-auth-server
|
|
||||||
, hspec >= 2.5.5 && < 2.10
|
|
||||||
, QuickCheck >= 2.11.3 && < 2.15
|
|
||||||
, http-client >= 0.5.13.1 && < 0.8
|
|
||||||
, lens-aeson >= 1.0.2 && < 1.3
|
|
||||||
, warp >= 3.2.25 && < 3.4
|
|
||||||
, wreq >= 0.5.2.1 && < 0.6
|
|
||||||
other-modules:
|
|
||||||
Servant.Auth.ServerSpec
|
|
||||||
default-language: Haskell2010
|
|
|
@ -1,180 +0,0 @@
|
||||||
module Servant.Auth.Server
|
|
||||||
(
|
|
||||||
-- | This package provides implementations for some common authentication
|
|
||||||
-- methods. Authentication yields a trustworthy (because generated by the
|
|
||||||
-- server) value of an some arbitrary type:
|
|
||||||
--
|
|
||||||
-- > type MyApi = Protected
|
|
||||||
-- >
|
|
||||||
-- > type Protected = Auth '[JWT, Cookie] User :> Get '[JSON] UserAccountDetails
|
|
||||||
-- >
|
|
||||||
-- > server :: Server Protected
|
|
||||||
-- > server (Authenticated usr) = ... -- here we know the client really is
|
|
||||||
-- > -- who she claims to be
|
|
||||||
-- > server _ = throwAll err401
|
|
||||||
--
|
|
||||||
-- Additional configuration happens via 'Context'.
|
|
||||||
--
|
|
||||||
-- == Example for Custom Handler
|
|
||||||
-- To use a custom 'Servant.Server.Handler' it is necessary to use
|
|
||||||
-- 'Servant.Server.hoistServerWithContext' instead of
|
|
||||||
-- 'Servant.Server.hoistServer' and specify the 'Context'.
|
|
||||||
--
|
|
||||||
-- Below is an example of passing 'CookieSettings' and 'JWTSettings' in the
|
|
||||||
-- 'Context' to create a specialized function equivalent to
|
|
||||||
-- 'Servant.Server.hoistServer' for an API that includes cookie
|
|
||||||
-- authentication.
|
|
||||||
--
|
|
||||||
-- > hoistServerWithAuth
|
|
||||||
-- > :: HasServer api '[CookieSettings, JWTSettings]
|
|
||||||
-- > => Proxy api
|
|
||||||
-- > -> (forall x. m x -> n x)
|
|
||||||
-- > -> ServerT api m
|
|
||||||
-- > -> ServerT api n
|
|
||||||
-- > hoistServerWithAuth api =
|
|
||||||
-- > hoistServerWithContext api (Proxy :: Proxy '[CookieSettings, JWTSettings])
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
-- * Auth
|
|
||||||
-- | Basic types
|
|
||||||
Auth
|
|
||||||
, AuthResult(..)
|
|
||||||
, AuthCheck(..)
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
-- * JWT
|
|
||||||
-- | JSON Web Tokens (JWT) are a compact and secure way of transferring
|
|
||||||
-- information between parties. In this library, they are signed by the
|
|
||||||
-- server (or by some other party posessing the relevant key), and used to
|
|
||||||
-- indicate the bearer's identity or authorization.
|
|
||||||
--
|
|
||||||
-- Arbitrary information can be encoded - just declare instances for the
|
|
||||||
-- 'FromJWT' and 'ToJWT' classes. Don't go overboard though - be aware that
|
|
||||||
-- usually you'll be trasmitting this information on each request (and
|
|
||||||
-- response!).
|
|
||||||
--
|
|
||||||
-- Note that, while the tokens are signed, they are not encrypted. Do not put
|
|
||||||
-- any information you do not wish the client to know in them!
|
|
||||||
|
|
||||||
-- ** Combinator
|
|
||||||
-- | Re-exported from 'servant-auth'
|
|
||||||
, JWT
|
|
||||||
|
|
||||||
-- ** Classes
|
|
||||||
, FromJWT(..)
|
|
||||||
, ToJWT(..)
|
|
||||||
|
|
||||||
-- ** Related types
|
|
||||||
, IsMatch(..)
|
|
||||||
|
|
||||||
-- ** Settings
|
|
||||||
, JWTSettings(..)
|
|
||||||
, defaultJWTSettings
|
|
||||||
|
|
||||||
-- ** Create check
|
|
||||||
, jwtAuthCheck
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
-- * Cookie
|
|
||||||
-- | Cookies are also a method of identifying and authenticating a user. They
|
|
||||||
-- are particular common when the client is a browser
|
|
||||||
|
|
||||||
-- ** Combinator
|
|
||||||
-- | Re-exported from 'servant-auth'
|
|
||||||
, Cookie
|
|
||||||
|
|
||||||
-- ** Settings
|
|
||||||
, CookieSettings(..)
|
|
||||||
, XsrfCookieSettings(..)
|
|
||||||
, defaultCookieSettings
|
|
||||||
, defaultXsrfCookieSettings
|
|
||||||
, makeSessionCookie
|
|
||||||
, makeSessionCookieBS
|
|
||||||
, makeXsrfCookie
|
|
||||||
, makeCsrfCookie
|
|
||||||
, makeCookie
|
|
||||||
, makeCookieBS
|
|
||||||
, acceptLogin
|
|
||||||
, clearSession
|
|
||||||
|
|
||||||
|
|
||||||
-- ** Related types
|
|
||||||
, IsSecure(..)
|
|
||||||
, SameSite(..)
|
|
||||||
, AreAuths
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
-- * BasicAuth
|
|
||||||
-- ** Combinator
|
|
||||||
-- | Re-exported from 'servant-auth'
|
|
||||||
, BasicAuth
|
|
||||||
|
|
||||||
-- ** Classes
|
|
||||||
, FromBasicAuthData(..)
|
|
||||||
|
|
||||||
-- ** Settings
|
|
||||||
, BasicAuthCfg
|
|
||||||
|
|
||||||
-- ** Related types
|
|
||||||
, BasicAuthData(..)
|
|
||||||
, IsPasswordCorrect(..)
|
|
||||||
|
|
||||||
-- ** Authentication request
|
|
||||||
, wwwAuthenticatedErr
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
-- * Utilies
|
|
||||||
, ThrowAll(throwAll)
|
|
||||||
, generateKey
|
|
||||||
, generateSecret
|
|
||||||
, fromSecret
|
|
||||||
, writeKey
|
|
||||||
, readKey
|
|
||||||
, makeJWT
|
|
||||||
, verifyJWT
|
|
||||||
|
|
||||||
-- ** Re-exports
|
|
||||||
, Default(def)
|
|
||||||
, SetCookie
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Prelude hiding (readFile, writeFile)
|
|
||||||
import Data.ByteString (ByteString, writeFile, readFile)
|
|
||||||
import Data.Default.Class (Default (def))
|
|
||||||
import Servant.Auth
|
|
||||||
import Servant.Auth.JWT
|
|
||||||
import Servant.Auth.Server.Internal ()
|
|
||||||
import Servant.Auth.Server.Internal.BasicAuth
|
|
||||||
import Servant.Auth.Server.Internal.Class
|
|
||||||
import Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
import Servant.Auth.Server.Internal.Cookie
|
|
||||||
import Servant.Auth.Server.Internal.JWT
|
|
||||||
import Servant.Auth.Server.Internal.ThrowAll
|
|
||||||
import Servant.Auth.Server.Internal.Types
|
|
||||||
|
|
||||||
import Crypto.JOSE as Jose
|
|
||||||
import Servant (BasicAuthData (..))
|
|
||||||
import Web.Cookie (SetCookie)
|
|
||||||
|
|
||||||
-- | Generate a key suitable for use with 'defaultConfig'.
|
|
||||||
generateKey :: IO Jose.JWK
|
|
||||||
generateKey = Jose.genJWK $ Jose.OctGenParam 256
|
|
||||||
|
|
||||||
-- | Generate a bytestring suitable for use with 'fromSecret'.
|
|
||||||
generateSecret :: MonadRandom m => m ByteString
|
|
||||||
generateSecret = Jose.getRandomBytes 256
|
|
||||||
|
|
||||||
-- | Restores a key from a bytestring.
|
|
||||||
fromSecret :: ByteString -> Jose.JWK
|
|
||||||
fromSecret = Jose.fromOctets
|
|
||||||
|
|
||||||
-- | Writes a secret to a file. Can for instance be used from the REPL
|
|
||||||
-- to persist a key to a file, which can then be included with the
|
|
||||||
-- application. Restore the key using 'readKey'.
|
|
||||||
writeKey :: FilePath -> IO ()
|
|
||||||
writeKey fp = writeFile fp =<< generateSecret
|
|
||||||
|
|
||||||
-- | Reads a key from a file.
|
|
||||||
readKey :: FilePath -> IO Jose.JWK
|
|
||||||
readKey fp = fromSecret <$> readFile fp
|
|
|
@ -1,70 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
|
|
||||||
module Servant.Auth.Server.Internal where
|
|
||||||
|
|
||||||
import Control.Monad.Trans (liftIO)
|
|
||||||
import Servant ((:>), Handler, HasServer (..),
|
|
||||||
Proxy (..),
|
|
||||||
HasContextEntry(getContextEntry))
|
|
||||||
import Servant.Auth
|
|
||||||
import Servant.Auth.JWT (ToJWT)
|
|
||||||
|
|
||||||
import Servant.Auth.Server.Internal.AddSetCookie
|
|
||||||
import Servant.Auth.Server.Internal.Class
|
|
||||||
import Servant.Auth.Server.Internal.Cookie
|
|
||||||
import Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
import Servant.Auth.Server.Internal.JWT
|
|
||||||
import Servant.Auth.Server.Internal.Types
|
|
||||||
|
|
||||||
import Servant.Server.Internal (DelayedIO, addAuthCheck, withRequest)
|
|
||||||
|
|
||||||
instance ( n ~ 'S ('S 'Z)
|
|
||||||
, HasServer (AddSetCookiesApi n api) ctxs, AreAuths auths ctxs v
|
|
||||||
, HasServer api ctxs -- this constraint is needed to implement hoistServer
|
|
||||||
, AddSetCookies n (ServerT api Handler) (ServerT (AddSetCookiesApi n api) Handler)
|
|
||||||
, ToJWT v
|
|
||||||
, HasContextEntry ctxs CookieSettings
|
|
||||||
, HasContextEntry ctxs JWTSettings
|
|
||||||
) => HasServer (Auth auths v :> api) ctxs where
|
|
||||||
type ServerT (Auth auths v :> api) m = AuthResult v -> ServerT api m
|
|
||||||
|
|
||||||
#if MIN_VERSION_servant_server(0,12,0)
|
|
||||||
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
|
|
||||||
#endif
|
|
||||||
|
|
||||||
route _ context subserver =
|
|
||||||
route (Proxy :: Proxy (AddSetCookiesApi n api))
|
|
||||||
context
|
|
||||||
(fmap go subserver `addAuthCheck` authCheck)
|
|
||||||
|
|
||||||
where
|
|
||||||
authCheck :: DelayedIO (AuthResult v, SetCookieList ('S ('S 'Z)))
|
|
||||||
authCheck = withRequest $ \req -> liftIO $ do
|
|
||||||
authResult <- runAuthCheck (runAuths (Proxy :: Proxy auths) context) req
|
|
||||||
cookies <- makeCookies authResult
|
|
||||||
return (authResult, cookies)
|
|
||||||
|
|
||||||
jwtSettings :: JWTSettings
|
|
||||||
jwtSettings = getContextEntry context
|
|
||||||
|
|
||||||
cookieSettings :: CookieSettings
|
|
||||||
cookieSettings = getContextEntry context
|
|
||||||
|
|
||||||
makeCookies :: AuthResult v -> IO (SetCookieList ('S ('S 'Z)))
|
|
||||||
makeCookies authResult = do
|
|
||||||
xsrf <- makeXsrfCookie cookieSettings
|
|
||||||
fmap (Just xsrf `SetCookieCons`) $
|
|
||||||
case authResult of
|
|
||||||
(Authenticated v) -> do
|
|
||||||
ejwt <- makeSessionCookie cookieSettings jwtSettings v
|
|
||||||
case ejwt of
|
|
||||||
Nothing -> return $ Nothing `SetCookieCons` SetCookieNil
|
|
||||||
Just jwt -> return $ Just jwt `SetCookieCons` SetCookieNil
|
|
||||||
_ -> return $ Nothing `SetCookieCons` SetCookieNil
|
|
||||||
|
|
||||||
go :: (AuthResult v -> ServerT api Handler)
|
|
||||||
-> (AuthResult v, SetCookieList n)
|
|
||||||
-> ServerT (AddSetCookiesApi n api) Handler
|
|
||||||
go fn (authResult, cookies) = addSetCookies cookies $ fn authResult
|
|
|
@ -1,106 +0,0 @@
|
||||||
{-# LANGUAGE PolyKinds #-}
|
|
||||||
{-# LANGUAGE TupleSections #-}
|
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
|
|
||||||
module Servant.Auth.Server.Internal.AddSetCookie where
|
|
||||||
|
|
||||||
import Blaze.ByteString.Builder (toByteString)
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import Data.Tagged (Tagged (..))
|
|
||||||
import qualified Network.HTTP.Types as HTTP
|
|
||||||
import Network.Wai (mapResponseHeaders)
|
|
||||||
import Servant
|
|
||||||
import Servant.API.Generic
|
|
||||||
import Servant.Server.Generic
|
|
||||||
import Web.Cookie
|
|
||||||
|
|
||||||
-- What are we doing here? Well, the idea is to add headers to the response,
|
|
||||||
-- but the headers come from the authentication check. In order to do that, we
|
|
||||||
-- tweak a little the general theme of recursing down the API tree; this time,
|
|
||||||
-- we recurse down a variation of it that adds headers to all the endpoints.
|
|
||||||
-- This involves the usual type-level checks.
|
|
||||||
--
|
|
||||||
-- TODO: If the endpoints already have headers, this will not work as is.
|
|
||||||
|
|
||||||
data Nat = Z | S Nat
|
|
||||||
|
|
||||||
type family AddSetCookiesApi (n :: Nat) a where
|
|
||||||
AddSetCookiesApi ('S 'Z) a = AddSetCookieApi a
|
|
||||||
AddSetCookiesApi ('S n) a = AddSetCookiesApi n (AddSetCookieApi a)
|
|
||||||
|
|
||||||
type family AddSetCookieApiVerb a where
|
|
||||||
AddSetCookieApiVerb (Headers ls a) = Headers (Header "Set-Cookie" SetCookie ': ls) a
|
|
||||||
AddSetCookieApiVerb a = Headers '[Header "Set-Cookie" SetCookie] a
|
|
||||||
|
|
||||||
type family AddSetCookieApi a :: *
|
|
||||||
type instance AddSetCookieApi (a :> b) = a :> AddSetCookieApi b
|
|
||||||
type instance AddSetCookieApi (a :<|> b) = AddSetCookieApi a :<|> AddSetCookieApi b
|
|
||||||
type instance AddSetCookieApi (NamedRoutes api) = AddSetCookieApi (ToServantApi api)
|
|
||||||
type instance AddSetCookieApi (Verb method stat ctyps a)
|
|
||||||
= Verb method stat ctyps (AddSetCookieApiVerb a)
|
|
||||||
type instance AddSetCookieApi Raw = Raw
|
|
||||||
#if MIN_VERSION_servant_server(0,15,0)
|
|
||||||
type instance AddSetCookieApi (Stream method stat framing ctyps a)
|
|
||||||
= Stream method stat framing ctyps (AddSetCookieApiVerb a)
|
|
||||||
#endif
|
|
||||||
type instance AddSetCookieApi (Headers hs a) = AddSetCookieApiVerb (Headers hs a)
|
|
||||||
|
|
||||||
data SetCookieList (n :: Nat) :: * where
|
|
||||||
SetCookieNil :: SetCookieList 'Z
|
|
||||||
SetCookieCons :: Maybe SetCookie -> SetCookieList n -> SetCookieList ('S n)
|
|
||||||
|
|
||||||
class AddSetCookies (n :: Nat) orig new where
|
|
||||||
addSetCookies :: SetCookieList n -> orig -> new
|
|
||||||
|
|
||||||
instance {-# OVERLAPS #-} AddSetCookies ('S n) oldb newb
|
|
||||||
=> AddSetCookies ('S n) (a -> oldb) (a -> newb) where
|
|
||||||
addSetCookies cookies oldfn = addSetCookies cookies . oldfn
|
|
||||||
|
|
||||||
instance AddSetCookies 'Z orig orig where
|
|
||||||
addSetCookies _ = id
|
|
||||||
|
|
||||||
instance {-# OVERLAPPABLE #-}
|
|
||||||
( Functor m
|
|
||||||
, AddSetCookies n (m old) (m cookied)
|
|
||||||
, AddHeader "Set-Cookie" SetCookie cookied new
|
|
||||||
) => AddSetCookies ('S n) (m old) (m new) where
|
|
||||||
addSetCookies (mCookie `SetCookieCons` rest) oldVal =
|
|
||||||
case mCookie of
|
|
||||||
Nothing -> noHeader <$> addSetCookies rest oldVal
|
|
||||||
Just cookie -> addHeader cookie <$> addSetCookies rest oldVal
|
|
||||||
|
|
||||||
instance {-# OVERLAPS #-}
|
|
||||||
(AddSetCookies ('S n) a a', AddSetCookies ('S n) b b')
|
|
||||||
=> AddSetCookies ('S n) (a :<|> b) (a' :<|> b') where
|
|
||||||
addSetCookies cookies (a :<|> b) = addSetCookies cookies a :<|> addSetCookies cookies b
|
|
||||||
|
|
||||||
instance {-# OVERLAPS #-}
|
|
||||||
( AddSetCookies ('S n) (ServerT (ToServantApi api) m) cookiedApi
|
|
||||||
, Generic (api (AsServerT m))
|
|
||||||
, GServantProduct (Rep (api (AsServerT m)))
|
|
||||||
, ToServant api (AsServerT m) ~ ServerT (ToServantApi api) m
|
|
||||||
)
|
|
||||||
=> AddSetCookies ('S n) (api (AsServerT m)) cookiedApi where
|
|
||||||
addSetCookies cookies = addSetCookies cookies . toServant
|
|
||||||
|
|
||||||
-- | for @servant <0.11@
|
|
||||||
instance
|
|
||||||
AddSetCookies ('S n) Application Application where
|
|
||||||
addSetCookies cookies r request respond
|
|
||||||
= r request $ respond . mapResponseHeaders (++ mkHeaders cookies)
|
|
||||||
|
|
||||||
-- | for @servant >=0.11@
|
|
||||||
instance
|
|
||||||
AddSetCookies ('S n) (Tagged m Application) (Tagged m Application) where
|
|
||||||
addSetCookies cookies r = Tagged $ \request respond ->
|
|
||||||
unTagged r request $ respond . mapResponseHeaders (++ mkHeaders cookies)
|
|
||||||
|
|
||||||
mkHeaders :: SetCookieList x -> [HTTP.Header]
|
|
||||||
mkHeaders x = ("Set-Cookie",) <$> mkCookies x
|
|
||||||
where
|
|
||||||
mkCookies :: forall y. SetCookieList y -> [BS.ByteString]
|
|
||||||
mkCookies SetCookieNil = []
|
|
||||||
mkCookies (SetCookieCons Nothing rest) = mkCookies rest
|
|
||||||
mkCookies (SetCookieCons (Just y) rest)
|
|
||||||
= toByteString (renderSetCookie y) : mkCookies rest
|
|
|
@ -1,59 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
module Servant.Auth.Server.Internal.BasicAuth where
|
|
||||||
|
|
||||||
#if !MIN_VERSION_servant_server(0,16,0)
|
|
||||||
#define ServerError ServantErr
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import Servant (BasicAuthData (..),
|
|
||||||
ServerError (..), err401)
|
|
||||||
import Servant.Server.Internal.BasicAuth (decodeBAHdr,
|
|
||||||
mkBAChallengerHdr)
|
|
||||||
|
|
||||||
import Servant.Auth.Server.Internal.Types
|
|
||||||
|
|
||||||
-- | A 'ServerError' that asks the client to authenticate via Basic
|
|
||||||
-- Authentication, should be invoked by an application whenever
|
|
||||||
-- appropriate. The argument is the realm.
|
|
||||||
wwwAuthenticatedErr :: BS.ByteString -> ServerError
|
|
||||||
wwwAuthenticatedErr realm = err401 { errHeaders = [mkBAChallengerHdr realm] }
|
|
||||||
|
|
||||||
-- | A type holding the configuration for Basic Authentication.
|
|
||||||
-- It is defined as a type family with no arguments, so that
|
|
||||||
-- it can be instantiated to whatever type you need to
|
|
||||||
-- authenticate your users (use @type instance BasicAuthCfg = ...@).
|
|
||||||
--
|
|
||||||
-- Note that the instantiation is application-wide,
|
|
||||||
-- i.e. there can be only one instance.
|
|
||||||
-- As a consequence, it should not be instantiated in a library.
|
|
||||||
--
|
|
||||||
-- Basic Authentication expects an element of type 'BasicAuthCfg'
|
|
||||||
-- to be in the 'Context'; that element is then passed automatically
|
|
||||||
-- to the instance of 'FromBasicAuthData' together with the
|
|
||||||
-- authentication data obtained from the client.
|
|
||||||
--
|
|
||||||
-- If you do not need a configuration for Basic Authentication,
|
|
||||||
-- you can use just @BasicAuthCfg = ()@, and recall to also
|
|
||||||
-- add @()@ to the 'Context'.
|
|
||||||
-- A basic but more interesting example is to take as 'BasicAuthCfg'
|
|
||||||
-- a list of authorised username/password pairs:
|
|
||||||
--
|
|
||||||
-- > deriving instance Eq BasicAuthData
|
|
||||||
-- > type instance BasicAuthCfg = [BasicAuthData]
|
|
||||||
-- > instance FromBasicAuthData User where
|
|
||||||
-- > fromBasicAuthData authData authCfg =
|
|
||||||
-- > if elem authData authCfg then ...
|
|
||||||
type family BasicAuthCfg
|
|
||||||
|
|
||||||
class FromBasicAuthData a where
|
|
||||||
-- | Whether the username exists and the password is correct.
|
|
||||||
-- Note that, rather than passing a 'Pass' to the function, we pass a
|
|
||||||
-- function that checks an 'EncryptedPass'. This is to make sure you don't
|
|
||||||
-- accidentally do something untoward with the password, like store it.
|
|
||||||
fromBasicAuthData :: BasicAuthData -> BasicAuthCfg -> IO (AuthResult a)
|
|
||||||
|
|
||||||
basicAuthCheck :: FromBasicAuthData usr => BasicAuthCfg -> AuthCheck usr
|
|
||||||
basicAuthCheck cfg = AuthCheck $ \req -> case decodeBAHdr req of
|
|
||||||
Nothing -> return Indefinite
|
|
||||||
Just baData -> fromBasicAuthData baData cfg
|
|
|
@ -1,72 +0,0 @@
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
|
||||||
module Servant.Auth.Server.Internal.Class where
|
|
||||||
|
|
||||||
import Servant.Auth
|
|
||||||
import Data.Monoid
|
|
||||||
import Servant hiding (BasicAuth)
|
|
||||||
|
|
||||||
import Servant.Auth.JWT
|
|
||||||
import Servant.Auth.Server.Internal.Types
|
|
||||||
import Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
import Servant.Auth.Server.Internal.BasicAuth
|
|
||||||
import Servant.Auth.Server.Internal.Cookie
|
|
||||||
import Servant.Auth.Server.Internal.JWT (jwtAuthCheck)
|
|
||||||
|
|
||||||
-- | @IsAuth a ctx v@ indicates that @a@ is an auth type that expects all
|
|
||||||
-- elements of @ctx@ to be the in the Context and whose authentication check
|
|
||||||
-- returns an @AuthCheck v@.
|
|
||||||
class IsAuth a v where
|
|
||||||
type family AuthArgs a :: [*]
|
|
||||||
runAuth :: proxy a -> proxy v -> Unapp (AuthArgs a) (AuthCheck v)
|
|
||||||
|
|
||||||
instance FromJWT usr => IsAuth Cookie usr where
|
|
||||||
type AuthArgs Cookie = '[CookieSettings, JWTSettings]
|
|
||||||
runAuth _ _ = cookieAuthCheck
|
|
||||||
|
|
||||||
instance FromJWT usr => IsAuth JWT usr where
|
|
||||||
type AuthArgs JWT = '[JWTSettings]
|
|
||||||
runAuth _ _ = jwtAuthCheck
|
|
||||||
|
|
||||||
instance FromBasicAuthData usr => IsAuth BasicAuth usr where
|
|
||||||
type AuthArgs BasicAuth = '[BasicAuthCfg]
|
|
||||||
runAuth _ _ = basicAuthCheck
|
|
||||||
|
|
||||||
-- * Helper
|
|
||||||
|
|
||||||
class AreAuths (as :: [*]) (ctxs :: [*]) v where
|
|
||||||
runAuths :: proxy as -> Context ctxs -> AuthCheck v
|
|
||||||
|
|
||||||
instance AreAuths '[] ctxs v where
|
|
||||||
runAuths _ _ = mempty
|
|
||||||
|
|
||||||
instance ( AuthCheck v ~ App (AuthArgs a) (Unapp (AuthArgs a) (AuthCheck v))
|
|
||||||
, IsAuth a v
|
|
||||||
, AreAuths as ctxs v
|
|
||||||
, AppCtx ctxs (AuthArgs a) (Unapp (AuthArgs a) (AuthCheck v))
|
|
||||||
) => AreAuths (a ': as) ctxs v where
|
|
||||||
runAuths _ ctxs = go <> runAuths (Proxy :: Proxy as) ctxs
|
|
||||||
where
|
|
||||||
go = appCtx (Proxy :: Proxy (AuthArgs a))
|
|
||||||
ctxs
|
|
||||||
(runAuth (Proxy :: Proxy a) (Proxy :: Proxy v))
|
|
||||||
|
|
||||||
type family Unapp ls res where
|
|
||||||
Unapp '[] res = res
|
|
||||||
Unapp (arg1 ': rest) res = arg1 -> Unapp rest res
|
|
||||||
|
|
||||||
type family App ls res where
|
|
||||||
App '[] res = res
|
|
||||||
App (arg1 ': rest) (arg1 -> res) = App rest res
|
|
||||||
|
|
||||||
-- | @AppCtx@ applies the function @res@ to the arguments in @ls@ by taking the
|
|
||||||
-- values from the Context provided.
|
|
||||||
class AppCtx ctx ls res where
|
|
||||||
appCtx :: proxy ls -> Context ctx -> res -> App ls res
|
|
||||||
|
|
||||||
instance ( HasContextEntry ctxs ctx
|
|
||||||
, AppCtx ctxs rest res
|
|
||||||
) => AppCtx ctxs (ctx ': rest) (ctx -> res) where
|
|
||||||
appCtx _ ctx fn = appCtx (Proxy :: Proxy rest) ctx $ fn $ getContextEntry ctx
|
|
||||||
|
|
||||||
instance AppCtx ctx '[] res where
|
|
||||||
appCtx _ _ r = r
|
|
|
@ -1,127 +0,0 @@
|
||||||
module Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
( module Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
, Servant.API.IsSecure(..)
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Crypto.JOSE as Jose
|
|
||||||
import Crypto.JWT as Jose
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import Data.Default.Class
|
|
||||||
import Data.Time
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Servant.API (IsSecure(..))
|
|
||||||
|
|
||||||
data IsMatch = Matches | DoesNotMatch
|
|
||||||
deriving (Eq, Show, Read, Generic, Ord)
|
|
||||||
|
|
||||||
data IsPasswordCorrect = PasswordCorrect | PasswordIncorrect
|
|
||||||
deriving (Eq, Show, Read, Generic, Ord)
|
|
||||||
|
|
||||||
-- The @SameSite@ attribute of cookies determines whether cookies will be sent
|
|
||||||
-- on cross-origin requests.
|
|
||||||
--
|
|
||||||
-- See <https://tools.ietf.org/html/draft-west-first-party-cookies-07 this document>
|
|
||||||
-- for more information.
|
|
||||||
data SameSite = AnySite | SameSiteStrict | SameSiteLax
|
|
||||||
deriving (Eq, Show, Read, Generic, Ord)
|
|
||||||
|
|
||||||
-- | @JWTSettings@ are used to generate cookies, and to verify JWTs.
|
|
||||||
data JWTSettings = JWTSettings
|
|
||||||
{
|
|
||||||
-- | Key used to sign JWT.
|
|
||||||
signingKey :: Jose.JWK
|
|
||||||
-- | Algorithm used to sign JWT.
|
|
||||||
, jwtAlg :: Maybe Jose.Alg
|
|
||||||
-- | Keys used to validate JWT.
|
|
||||||
, validationKeys :: IO Jose.JWKSet
|
|
||||||
-- | An @aud@ predicate. The @aud@ is a string or URI that identifies the
|
|
||||||
-- intended recipient of the JWT.
|
|
||||||
, audienceMatches :: Jose.StringOrURI -> IsMatch
|
|
||||||
} deriving (Generic)
|
|
||||||
|
|
||||||
-- | A @JWTSettings@ where the audience always matches.
|
|
||||||
defaultJWTSettings :: Jose.JWK -> JWTSettings
|
|
||||||
defaultJWTSettings k = JWTSettings
|
|
||||||
{ signingKey = k
|
|
||||||
, jwtAlg = Nothing
|
|
||||||
, validationKeys = pure $ Jose.JWKSet [k]
|
|
||||||
, audienceMatches = const Matches }
|
|
||||||
|
|
||||||
-- | The policies to use when generating cookies.
|
|
||||||
--
|
|
||||||
-- If *both* 'cookieMaxAge' and 'cookieExpires' are @Nothing@, browsers will
|
|
||||||
-- treat the cookie as a *session cookie*. These will be deleted when the
|
|
||||||
-- browser is closed.
|
|
||||||
--
|
|
||||||
-- Note that having the setting @Secure@ may cause testing failures if you are
|
|
||||||
-- not testing over HTTPS.
|
|
||||||
data CookieSettings = CookieSettings
|
|
||||||
{
|
|
||||||
-- | 'Secure' means browsers will only send cookies over HTTPS. Default:
|
|
||||||
-- @Secure@.
|
|
||||||
cookieIsSecure :: !IsSecure
|
|
||||||
-- | How long from now until the cookie expires. Default: @Nothing@.
|
|
||||||
, cookieMaxAge :: !(Maybe DiffTime)
|
|
||||||
-- | At what time the cookie expires. Default: @Nothing@.
|
|
||||||
, cookieExpires :: !(Maybe UTCTime)
|
|
||||||
-- | The URL path and sub-paths for which this cookie is used. Default: @Just "/"@.
|
|
||||||
, cookiePath :: !(Maybe BS.ByteString)
|
|
||||||
-- | Domain name, if set cookie also allows subdomains. Default: @Nothing@.
|
|
||||||
, cookieDomain :: !(Maybe BS.ByteString)
|
|
||||||
-- | 'SameSite' settings. Default: @SameSiteLax@.
|
|
||||||
, cookieSameSite :: !SameSite
|
|
||||||
-- | What name to use for the cookie used for the session.
|
|
||||||
, sessionCookieName :: !BS.ByteString
|
|
||||||
-- | The optional settings to use for XSRF protection. Default: @Just def@.
|
|
||||||
, cookieXsrfSetting :: !(Maybe XsrfCookieSettings)
|
|
||||||
} deriving (Eq, Show, Generic)
|
|
||||||
|
|
||||||
instance Default CookieSettings where
|
|
||||||
def = defaultCookieSettings
|
|
||||||
|
|
||||||
defaultCookieSettings :: CookieSettings
|
|
||||||
defaultCookieSettings = CookieSettings
|
|
||||||
{ cookieIsSecure = Secure
|
|
||||||
, cookieMaxAge = Nothing
|
|
||||||
, cookieExpires = Nothing
|
|
||||||
, cookiePath = Just "/"
|
|
||||||
, cookieDomain = Nothing
|
|
||||||
, cookieSameSite = SameSiteLax
|
|
||||||
, sessionCookieName = "JWT-Cookie"
|
|
||||||
, cookieXsrfSetting = Just def
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | The policies to use when generating and verifying XSRF cookies
|
|
||||||
data XsrfCookieSettings = XsrfCookieSettings
|
|
||||||
{
|
|
||||||
-- | What name to use for the cookie used for XSRF protection.
|
|
||||||
xsrfCookieName :: !BS.ByteString
|
|
||||||
-- | What path to use for the cookie used for XSRF protection. Default @Just "/"@.
|
|
||||||
, xsrfCookiePath :: !(Maybe BS.ByteString)
|
|
||||||
-- | What name to use for the header used for XSRF protection.
|
|
||||||
, xsrfHeaderName :: !BS.ByteString
|
|
||||||
-- | Exclude GET request method from XSRF protection.
|
|
||||||
, xsrfExcludeGet :: !Bool
|
|
||||||
} deriving (Eq, Show, Generic)
|
|
||||||
|
|
||||||
instance Default XsrfCookieSettings where
|
|
||||||
def = defaultXsrfCookieSettings
|
|
||||||
|
|
||||||
defaultXsrfCookieSettings :: XsrfCookieSettings
|
|
||||||
defaultXsrfCookieSettings = XsrfCookieSettings
|
|
||||||
{ xsrfCookieName = "XSRF-TOKEN"
|
|
||||||
, xsrfCookiePath = Just "/"
|
|
||||||
, xsrfHeaderName = "X-XSRF-TOKEN"
|
|
||||||
, xsrfExcludeGet = False
|
|
||||||
}
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Internal {{{
|
|
||||||
|
|
||||||
jwtSettingsToJwtValidationSettings :: JWTSettings -> Jose.JWTValidationSettings
|
|
||||||
jwtSettingsToJwtValidationSettings s
|
|
||||||
= defaultJWTValidationSettings (toBool <$> audienceMatches s)
|
|
||||||
where
|
|
||||||
toBool Matches = True
|
|
||||||
toBool DoesNotMatch = False
|
|
||||||
-- }}}
|
|
|
@ -1,183 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
module Servant.Auth.Server.Internal.Cookie where
|
|
||||||
|
|
||||||
import Blaze.ByteString.Builder (toByteString)
|
|
||||||
import Control.Monad (MonadPlus(..), guard)
|
|
||||||
import Control.Monad.Except
|
|
||||||
import Control.Monad.Reader
|
|
||||||
import qualified Crypto.JOSE as Jose
|
|
||||||
import qualified Crypto.JWT as Jose
|
|
||||||
import Data.ByteArray (constEq)
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Base64 as BS64
|
|
||||||
import qualified Data.ByteString.Lazy as BSL
|
|
||||||
import Data.CaseInsensitive (mk)
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
import Data.Time.Calendar (Day(..))
|
|
||||||
import Data.Time.Clock (UTCTime(..), secondsToDiffTime)
|
|
||||||
import Network.HTTP.Types (methodGet)
|
|
||||||
import Network.HTTP.Types.Header(hCookie)
|
|
||||||
import Network.Wai (Request, requestHeaders, requestMethod)
|
|
||||||
import Servant (AddHeader, addHeader)
|
|
||||||
import System.Entropy (getEntropy)
|
|
||||||
import Web.Cookie
|
|
||||||
|
|
||||||
import Servant.Auth.JWT (FromJWT (decodeJWT), ToJWT)
|
|
||||||
import Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
import Servant.Auth.Server.Internal.JWT (makeJWT, verifyJWT)
|
|
||||||
import Servant.Auth.Server.Internal.Types
|
|
||||||
|
|
||||||
|
|
||||||
cookieAuthCheck :: FromJWT usr => CookieSettings -> JWTSettings -> AuthCheck usr
|
|
||||||
cookieAuthCheck ccfg jwtSettings = do
|
|
||||||
req <- ask
|
|
||||||
jwtCookie <- maybe mempty return $ do
|
|
||||||
cookies' <- lookup hCookie $ requestHeaders req
|
|
||||||
let cookies = parseCookies cookies'
|
|
||||||
-- Apply the XSRF check if enabled.
|
|
||||||
guard $ fromMaybe True $ do
|
|
||||||
xsrfCookieCfg <- xsrfCheckRequired ccfg req
|
|
||||||
return $ xsrfCookieAuthCheck xsrfCookieCfg req cookies
|
|
||||||
-- session cookie *must* be HttpOnly and Secure
|
|
||||||
lookup (sessionCookieName ccfg) cookies
|
|
||||||
verifiedJWT <- liftIO $ verifyJWT jwtSettings jwtCookie
|
|
||||||
case verifiedJWT of
|
|
||||||
Nothing -> mzero
|
|
||||||
Just v -> return v
|
|
||||||
|
|
||||||
xsrfCheckRequired :: CookieSettings -> Request -> Maybe XsrfCookieSettings
|
|
||||||
xsrfCheckRequired cookieSettings req = do
|
|
||||||
xsrfCookieCfg <- cookieXsrfSetting cookieSettings
|
|
||||||
let disableForGetReq = xsrfExcludeGet xsrfCookieCfg && requestMethod req == methodGet
|
|
||||||
guard $ not disableForGetReq
|
|
||||||
return xsrfCookieCfg
|
|
||||||
|
|
||||||
xsrfCookieAuthCheck :: XsrfCookieSettings -> Request -> [(BS.ByteString, BS.ByteString)] -> Bool
|
|
||||||
xsrfCookieAuthCheck xsrfCookieCfg req cookies = fromMaybe False $ do
|
|
||||||
xsrfCookie <- lookup (xsrfCookieName xsrfCookieCfg) cookies
|
|
||||||
xsrfHeader <- lookup (mk $ xsrfHeaderName xsrfCookieCfg) $ requestHeaders req
|
|
||||||
return $ xsrfCookie `constEq` xsrfHeader
|
|
||||||
|
|
||||||
-- | Makes a cookie to be used for XSRF.
|
|
||||||
makeXsrfCookie :: CookieSettings -> IO SetCookie
|
|
||||||
makeXsrfCookie cookieSettings = case cookieXsrfSetting cookieSettings of
|
|
||||||
Just xsrfCookieSettings -> makeRealCookie xsrfCookieSettings
|
|
||||||
Nothing -> return $ noXsrfTokenCookie cookieSettings
|
|
||||||
where
|
|
||||||
makeRealCookie xsrfCookieSettings = do
|
|
||||||
xsrfValue <- BS64.encode <$> getEntropy 32
|
|
||||||
return
|
|
||||||
$ applyXsrfCookieSettings xsrfCookieSettings
|
|
||||||
$ applyCookieSettings cookieSettings
|
|
||||||
$ def{ setCookieValue = xsrfValue }
|
|
||||||
|
|
||||||
|
|
||||||
-- | Alias for 'makeXsrfCookie'.
|
|
||||||
makeCsrfCookie :: CookieSettings -> IO SetCookie
|
|
||||||
makeCsrfCookie = makeXsrfCookie
|
|
||||||
{-# DEPRECATED makeCsrfCookie "Use makeXsrfCookie instead" #-}
|
|
||||||
|
|
||||||
|
|
||||||
-- | Makes a cookie with session information.
|
|
||||||
makeSessionCookie :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe SetCookie)
|
|
||||||
makeSessionCookie cookieSettings jwtSettings v = do
|
|
||||||
ejwt <- makeJWT v jwtSettings (cookieExpires cookieSettings)
|
|
||||||
case ejwt of
|
|
||||||
Left _ -> return Nothing
|
|
||||||
Right jwt -> return
|
|
||||||
$ Just
|
|
||||||
$ applySessionCookieSettings cookieSettings
|
|
||||||
$ applyCookieSettings cookieSettings
|
|
||||||
$ def{ setCookieValue = BSL.toStrict jwt }
|
|
||||||
|
|
||||||
noXsrfTokenCookie :: CookieSettings -> SetCookie
|
|
||||||
noXsrfTokenCookie cookieSettings =
|
|
||||||
applyCookieSettings cookieSettings $ def{ setCookieName = "NO-XSRF-TOKEN", setCookieValue = "" }
|
|
||||||
|
|
||||||
applyCookieSettings :: CookieSettings -> SetCookie -> SetCookie
|
|
||||||
applyCookieSettings cookieSettings setCookie = setCookie
|
|
||||||
{ setCookieMaxAge = cookieMaxAge cookieSettings
|
|
||||||
, setCookieExpires = cookieExpires cookieSettings
|
|
||||||
, setCookiePath = cookiePath cookieSettings
|
|
||||||
, setCookieDomain = cookieDomain cookieSettings
|
|
||||||
, setCookieSecure = case cookieIsSecure cookieSettings of
|
|
||||||
Secure -> True
|
|
||||||
NotSecure -> False
|
|
||||||
}
|
|
||||||
|
|
||||||
applyXsrfCookieSettings :: XsrfCookieSettings -> SetCookie -> SetCookie
|
|
||||||
applyXsrfCookieSettings xsrfCookieSettings setCookie = setCookie
|
|
||||||
{ setCookieName = xsrfCookieName xsrfCookieSettings
|
|
||||||
, setCookiePath = xsrfCookiePath xsrfCookieSettings
|
|
||||||
, setCookieHttpOnly = False
|
|
||||||
}
|
|
||||||
|
|
||||||
applySessionCookieSettings :: CookieSettings -> SetCookie -> SetCookie
|
|
||||||
applySessionCookieSettings cookieSettings setCookie = setCookie
|
|
||||||
{ setCookieName = sessionCookieName cookieSettings
|
|
||||||
, setCookieSameSite = case cookieSameSite cookieSettings of
|
|
||||||
AnySite -> anySite
|
|
||||||
SameSiteStrict -> Just sameSiteStrict
|
|
||||||
SameSiteLax -> Just sameSiteLax
|
|
||||||
, setCookieHttpOnly = True
|
|
||||||
}
|
|
||||||
where
|
|
||||||
#if MIN_VERSION_cookie(0,4,5)
|
|
||||||
anySite = Just sameSiteNone
|
|
||||||
#else
|
|
||||||
anySite = Nothing
|
|
||||||
#endif
|
|
||||||
|
|
||||||
-- | For a JWT-serializable session, returns a function that decorates a
|
|
||||||
-- provided response object with XSRF and session cookies. This should be used
|
|
||||||
-- when a user successfully authenticates with credentials.
|
|
||||||
acceptLogin :: ( ToJWT session
|
|
||||||
, AddHeader "Set-Cookie" SetCookie response withOneCookie
|
|
||||||
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
|
|
||||||
=> CookieSettings
|
|
||||||
-> JWTSettings
|
|
||||||
-> session
|
|
||||||
-> IO (Maybe (response -> withTwoCookies))
|
|
||||||
acceptLogin cookieSettings jwtSettings session = do
|
|
||||||
mSessionCookie <- makeSessionCookie cookieSettings jwtSettings session
|
|
||||||
case mSessionCookie of
|
|
||||||
Nothing -> pure Nothing
|
|
||||||
Just sessionCookie -> do
|
|
||||||
xsrfCookie <- makeXsrfCookie cookieSettings
|
|
||||||
return $ Just $ addHeader sessionCookie . addHeader xsrfCookie
|
|
||||||
|
|
||||||
-- | Arbitrary cookie expiry time set back in history after unix time 0
|
|
||||||
expireTime :: UTCTime
|
|
||||||
expireTime = UTCTime (ModifiedJulianDay 50000) 0
|
|
||||||
|
|
||||||
-- | Adds headers to a response that clears all session cookies
|
|
||||||
-- | using max-age and expires cookie attributes.
|
|
||||||
clearSession :: ( AddHeader "Set-Cookie" SetCookie response withOneCookie
|
|
||||||
, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )
|
|
||||||
=> CookieSettings
|
|
||||||
-> response
|
|
||||||
-> withTwoCookies
|
|
||||||
clearSession cookieSettings = addHeader clearedSessionCookie . addHeader clearedXsrfCookie
|
|
||||||
where
|
|
||||||
-- According to RFC6265 max-age takes precedence, but IE/Edge ignore it completely so we set both
|
|
||||||
cookieSettingsExpires = cookieSettings
|
|
||||||
{ cookieExpires = Just expireTime
|
|
||||||
, cookieMaxAge = Just (secondsToDiffTime 0)
|
|
||||||
}
|
|
||||||
clearedSessionCookie = applySessionCookieSettings cookieSettingsExpires $ applyCookieSettings cookieSettingsExpires def
|
|
||||||
clearedXsrfCookie = case cookieXsrfSetting cookieSettings of
|
|
||||||
Just xsrfCookieSettings -> applyXsrfCookieSettings xsrfCookieSettings $ applyCookieSettings cookieSettingsExpires def
|
|
||||||
Nothing -> noXsrfTokenCookie cookieSettingsExpires
|
|
||||||
|
|
||||||
makeSessionCookieBS :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe BS.ByteString)
|
|
||||||
makeSessionCookieBS a b c = fmap (toByteString . renderSetCookie) <$> makeSessionCookie a b c
|
|
||||||
|
|
||||||
-- | Alias for 'makeSessionCookie'.
|
|
||||||
makeCookie :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe SetCookie)
|
|
||||||
makeCookie = makeSessionCookie
|
|
||||||
{-# DEPRECATED makeCookie "Use makeSessionCookie instead" #-}
|
|
||||||
|
|
||||||
-- | Alias for 'makeSessionCookieBS'.
|
|
||||||
makeCookieBS :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe BS.ByteString)
|
|
||||||
makeCookieBS = makeSessionCookieBS
|
|
||||||
{-# DEPRECATED makeCookieBS "Use makeSessionCookieBS instead" #-}
|
|
|
@ -1,3 +0,0 @@
|
||||||
module Servant.Auth.Server.Internal.FormLogin where
|
|
||||||
|
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
module Servant.Auth.Server.Internal.JWT where
|
|
||||||
|
|
||||||
import Control.Lens
|
|
||||||
import Control.Monad (MonadPlus(..), guard)
|
|
||||||
import Control.Monad.Reader
|
|
||||||
import qualified Crypto.JOSE as Jose
|
|
||||||
import qualified Crypto.JWT as Jose
|
|
||||||
import Data.ByteArray (constEq)
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Lazy as BSL
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
import Data.Time (UTCTime)
|
|
||||||
import Network.Wai (requestHeaders)
|
|
||||||
|
|
||||||
import Servant.Auth.JWT (FromJWT(..), ToJWT(..))
|
|
||||||
import Servant.Auth.Server.Internal.ConfigTypes
|
|
||||||
import Servant.Auth.Server.Internal.Types
|
|
||||||
|
|
||||||
|
|
||||||
-- | A JWT @AuthCheck@. You likely won't need to use this directly unless you
|
|
||||||
-- are protecting a @Raw@ endpoint.
|
|
||||||
jwtAuthCheck :: FromJWT usr => JWTSettings -> AuthCheck usr
|
|
||||||
jwtAuthCheck jwtSettings = do
|
|
||||||
req <- ask
|
|
||||||
token <- maybe mempty return $ do
|
|
||||||
authHdr <- lookup "Authorization" $ requestHeaders req
|
|
||||||
let bearer = "Bearer "
|
|
||||||
(mbearer, rest) = BS.splitAt (BS.length bearer) authHdr
|
|
||||||
guard (mbearer `constEq` bearer)
|
|
||||||
return rest
|
|
||||||
verifiedJWT <- liftIO $ verifyJWT jwtSettings token
|
|
||||||
case verifiedJWT of
|
|
||||||
Nothing -> mzero
|
|
||||||
Just v -> return v
|
|
||||||
|
|
||||||
-- | Creates a JWT containing the specified data. The data is stored in the
|
|
||||||
-- @dat@ claim. The 'Maybe UTCTime' argument indicates the time at which the
|
|
||||||
-- token expires.
|
|
||||||
makeJWT :: ToJWT a
|
|
||||||
=> a -> JWTSettings -> Maybe UTCTime -> IO (Either Jose.Error BSL.ByteString)
|
|
||||||
makeJWT v cfg expiry = Jose.runJOSE $ do
|
|
||||||
bestAlg <- Jose.bestJWSAlg $ signingKey cfg
|
|
||||||
let alg = fromMaybe bestAlg $ jwtAlg cfg
|
|
||||||
ejwt <- Jose.signClaims (signingKey cfg)
|
|
||||||
(Jose.newJWSHeader ((), alg))
|
|
||||||
(addExp $ encodeJWT v)
|
|
||||||
|
|
||||||
return $ Jose.encodeCompact ejwt
|
|
||||||
where
|
|
||||||
addExp claims = case expiry of
|
|
||||||
Nothing -> claims
|
|
||||||
Just e -> claims & Jose.claimExp ?~ Jose.NumericDate e
|
|
||||||
|
|
||||||
|
|
||||||
verifyJWT :: FromJWT a => JWTSettings -> BS.ByteString -> IO (Maybe a)
|
|
||||||
verifyJWT jwtCfg input = do
|
|
||||||
keys <- validationKeys jwtCfg
|
|
||||||
verifiedJWT <- Jose.runJOSE $ do
|
|
||||||
unverifiedJWT <- Jose.decodeCompact (BSL.fromStrict input)
|
|
||||||
Jose.verifyClaims
|
|
||||||
(jwtSettingsToJwtValidationSettings jwtCfg)
|
|
||||||
keys
|
|
||||||
unverifiedJWT
|
|
||||||
return $ case verifiedJWT of
|
|
||||||
Left (_ :: Jose.JWTError) -> Nothing
|
|
||||||
Right v -> case decodeJWT v of
|
|
||||||
Left _ -> Nothing
|
|
||||||
Right v' -> Just v'
|
|
|
@ -1,58 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
|
||||||
module Servant.Auth.Server.Internal.ThrowAll where
|
|
||||||
|
|
||||||
#if !MIN_VERSION_servant_server(0,16,0)
|
|
||||||
#define ServerError ServantErr
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Control.Monad.Error.Class
|
|
||||||
import Data.Tagged (Tagged (..))
|
|
||||||
import Servant ((:<|>) (..), ServerError(..), NamedRoutes(..))
|
|
||||||
import Servant.API.Generic
|
|
||||||
import Servant.Server.Generic
|
|
||||||
import Servant.Server
|
|
||||||
import Network.HTTP.Types
|
|
||||||
import Network.Wai
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
|
||||||
|
|
||||||
class ThrowAll a where
|
|
||||||
-- | 'throwAll' is a convenience function to throw errors across an entire
|
|
||||||
-- sub-API
|
|
||||||
--
|
|
||||||
--
|
|
||||||
-- > throwAll err400 :: Handler a :<|> Handler b :<|> Handler c
|
|
||||||
-- > == throwError err400 :<|> throwError err400 :<|> err400
|
|
||||||
throwAll :: ServerError -> a
|
|
||||||
|
|
||||||
instance (ThrowAll a, ThrowAll b) => ThrowAll (a :<|> b) where
|
|
||||||
throwAll e = throwAll e :<|> throwAll e
|
|
||||||
|
|
||||||
instance
|
|
||||||
( ThrowAll (ToServant api (AsServerT m)) , GenericServant api (AsServerT m)) =>
|
|
||||||
ThrowAll (api (AsServerT m)) where
|
|
||||||
|
|
||||||
throwAll = fromServant . throwAll
|
|
||||||
|
|
||||||
-- Really this shouldn't be necessary - ((->) a) should be an instance of
|
|
||||||
-- MonadError, no?
|
|
||||||
instance {-# OVERLAPPING #-} ThrowAll b => ThrowAll (a -> b) where
|
|
||||||
throwAll e = const $ throwAll e
|
|
||||||
|
|
||||||
instance {-# OVERLAPPABLE #-} (MonadError ServerError m) => ThrowAll (m a) where
|
|
||||||
throwAll = throwError
|
|
||||||
|
|
||||||
-- | for @servant <0.11@
|
|
||||||
instance {-# OVERLAPPING #-} ThrowAll Application where
|
|
||||||
throwAll e _req respond
|
|
||||||
= respond $ responseLBS (mkStatus (errHTTPCode e) (BS.pack $ errReasonPhrase e))
|
|
||||||
(errHeaders e)
|
|
||||||
(errBody e)
|
|
||||||
|
|
||||||
-- | for @servant >=0.11@
|
|
||||||
instance {-# OVERLAPPING #-} MonadError ServerError m => ThrowAll (Tagged m Application) where
|
|
||||||
throwAll e = Tagged $ \_req respond ->
|
|
||||||
respond $ responseLBS (mkStatus (errHTTPCode e) (BS.pack $ errReasonPhrase e))
|
|
||||||
(errHeaders e)
|
|
||||||
(errBody e)
|
|
|
@ -1,113 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
module Servant.Auth.Server.Internal.Types where
|
|
||||||
|
|
||||||
import Control.Applicative
|
|
||||||
import Control.Monad (MonadPlus(..), ap)
|
|
||||||
import Control.Monad.Reader
|
|
||||||
import Control.Monad.Time
|
|
||||||
import Data.Monoid (Monoid (..))
|
|
||||||
import Data.Semigroup (Semigroup (..))
|
|
||||||
import Data.Time (getCurrentTime)
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Network.Wai (Request)
|
|
||||||
|
|
||||||
import qualified Control.Monad.Fail as Fail
|
|
||||||
|
|
||||||
-- | The result of an authentication attempt.
|
|
||||||
data AuthResult val
|
|
||||||
= BadPassword
|
|
||||||
| NoSuchUser
|
|
||||||
-- | Authentication succeeded.
|
|
||||||
| Authenticated val
|
|
||||||
-- | If an authentication procedure cannot be carried out - if for example it
|
|
||||||
-- expects a password and username in a header that is not present -
|
|
||||||
-- @Indefinite@ is returned. This indicates that other authentication
|
|
||||||
-- methods should be tried.
|
|
||||||
| Indefinite
|
|
||||||
deriving (Eq, Show, Read, Generic, Ord, Functor, Traversable, Foldable)
|
|
||||||
|
|
||||||
instance Semigroup (AuthResult val) where
|
|
||||||
Indefinite <> y = y
|
|
||||||
x <> _ = x
|
|
||||||
|
|
||||||
instance Monoid (AuthResult val) where
|
|
||||||
mempty = Indefinite
|
|
||||||
mappend = (<>)
|
|
||||||
|
|
||||||
instance Applicative AuthResult where
|
|
||||||
pure = return
|
|
||||||
(<*>) = ap
|
|
||||||
|
|
||||||
instance Monad AuthResult where
|
|
||||||
return = Authenticated
|
|
||||||
Authenticated v >>= f = f v
|
|
||||||
BadPassword >>= _ = BadPassword
|
|
||||||
NoSuchUser >>= _ = NoSuchUser
|
|
||||||
Indefinite >>= _ = Indefinite
|
|
||||||
|
|
||||||
instance Alternative AuthResult where
|
|
||||||
empty = mzero
|
|
||||||
(<|>) = mplus
|
|
||||||
|
|
||||||
instance MonadPlus AuthResult where
|
|
||||||
mzero = mempty
|
|
||||||
mplus = (<>)
|
|
||||||
|
|
||||||
|
|
||||||
-- | An @AuthCheck@ is the function used to decide the authentication status
|
|
||||||
-- (the 'AuthResult') of a request. Different @AuthCheck@s may be combined as a
|
|
||||||
-- Monoid or Alternative; the semantics of this is that the *first*
|
|
||||||
-- non-'Indefinite' result from left to right is used and the rest are ignored.
|
|
||||||
newtype AuthCheck val = AuthCheck
|
|
||||||
{ runAuthCheck :: Request -> IO (AuthResult val) }
|
|
||||||
deriving (Generic, Functor)
|
|
||||||
|
|
||||||
instance Semigroup (AuthCheck val) where
|
|
||||||
AuthCheck f <> AuthCheck g = AuthCheck $ \x -> do
|
|
||||||
fx <- f x
|
|
||||||
case fx of
|
|
||||||
Indefinite -> g x
|
|
||||||
r -> pure r
|
|
||||||
|
|
||||||
instance Monoid (AuthCheck val) where
|
|
||||||
mempty = AuthCheck $ const $ return mempty
|
|
||||||
mappend = (<>)
|
|
||||||
|
|
||||||
instance Applicative AuthCheck where
|
|
||||||
pure = return
|
|
||||||
(<*>) = ap
|
|
||||||
|
|
||||||
instance Monad AuthCheck where
|
|
||||||
return = AuthCheck . return . return . return
|
|
||||||
AuthCheck ac >>= f = AuthCheck $ \req -> do
|
|
||||||
aresult <- ac req
|
|
||||||
case aresult of
|
|
||||||
Authenticated usr -> runAuthCheck (f usr) req
|
|
||||||
BadPassword -> return BadPassword
|
|
||||||
NoSuchUser -> return NoSuchUser
|
|
||||||
Indefinite -> return Indefinite
|
|
||||||
|
|
||||||
#if !MIN_VERSION_base(4,13,0)
|
|
||||||
fail = Fail.fail
|
|
||||||
#endif
|
|
||||||
|
|
||||||
instance Fail.MonadFail AuthCheck where
|
|
||||||
fail _ = AuthCheck . const $ return Indefinite
|
|
||||||
|
|
||||||
instance MonadReader Request AuthCheck where
|
|
||||||
ask = AuthCheck $ \x -> return (Authenticated x)
|
|
||||||
local f (AuthCheck check) = AuthCheck $ \req -> check (f req)
|
|
||||||
|
|
||||||
instance MonadIO AuthCheck where
|
|
||||||
liftIO action = AuthCheck $ const $ Authenticated <$> action
|
|
||||||
|
|
||||||
instance MonadTime AuthCheck where
|
|
||||||
currentTime = liftIO getCurrentTime
|
|
||||||
|
|
||||||
instance Alternative AuthCheck where
|
|
||||||
empty = mzero
|
|
||||||
(<|>) = mplus
|
|
||||||
|
|
||||||
instance MonadPlus AuthCheck where
|
|
||||||
mzero = mempty
|
|
||||||
mplus = (<>)
|
|
|
@ -1,3 +0,0 @@
|
||||||
module Servant.Auth.Server.SetCookieOrphan
|
|
||||||
{-# DEPRECATED "instance exists in http-api-data-0.3.9. This module will be removed in next major release." #-}
|
|
||||||
() where
|
|
|
@ -1,606 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
module Servant.Auth.ServerSpec (spec) where
|
|
||||||
|
|
||||||
#if !MIN_VERSION_servant_server(0,16,0)
|
|
||||||
#define ServerError ServantErr
|
|
||||||
#endif
|
|
||||||
|
|
||||||
import Control.Lens
|
|
||||||
import Control.Monad.IO.Class (liftIO)
|
|
||||||
import Crypto.JOSE (Alg (HS256, None), Error,
|
|
||||||
JWK, JWSHeader,
|
|
||||||
KeyMaterialGenParam (OctGenParam),
|
|
||||||
ToCompact, encodeCompact,
|
|
||||||
genJWK, newJWSHeader, runJOSE)
|
|
||||||
import Crypto.JWT (Audience (..), ClaimsSet,
|
|
||||||
NumericDate (NumericDate),
|
|
||||||
SignedJWT,
|
|
||||||
claimAud, claimNbf,
|
|
||||||
signClaims,
|
|
||||||
emptyClaimsSet,
|
|
||||||
unregisteredClaims)
|
|
||||||
import Data.Aeson (FromJSON, ToJSON, Value,
|
|
||||||
toJSON, encode)
|
|
||||||
import Data.Aeson.Lens (_JSON)
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Lazy as BSL
|
|
||||||
import Data.CaseInsensitive (mk)
|
|
||||||
import Data.Foldable (find)
|
|
||||||
import Data.Monoid
|
|
||||||
import Data.Time
|
|
||||||
import Data.Time.Clock (getCurrentTime)
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Network.HTTP.Client (cookie_http_only,
|
|
||||||
cookie_name, cookie_value,
|
|
||||||
cookie_expiry_time,
|
|
||||||
destroyCookieJar)
|
|
||||||
import Network.HTTP.Types (Status, status200,
|
|
||||||
status401)
|
|
||||||
import Network.Wai (responseLBS)
|
|
||||||
import Network.Wai.Handler.Warp (testWithApplication)
|
|
||||||
import Network.Wreq (Options, auth, basicAuth,
|
|
||||||
cookieExpiryTime, cookies,
|
|
||||||
defaults, get, getWith, postWith,
|
|
||||||
header, oauth2Bearer,
|
|
||||||
responseBody,
|
|
||||||
responseCookieJar,
|
|
||||||
responseHeader,
|
|
||||||
responseStatus)
|
|
||||||
import Network.Wreq.Types (Postable(..))
|
|
||||||
import Servant hiding (BasicAuth,
|
|
||||||
IsSecure (..), header)
|
|
||||||
import Servant.API.Generic ((:-))
|
|
||||||
import Servant.Auth.Server
|
|
||||||
import Servant.Auth.Server.Internal.Cookie (expireTime)
|
|
||||||
import Servant.Auth.Server.SetCookieOrphan ()
|
|
||||||
#if MIN_VERSION_servant_server(0,15,0)
|
|
||||||
import qualified Servant.Types.SourceT as S
|
|
||||||
#endif
|
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
|
||||||
import Test.Hspec
|
|
||||||
import Test.QuickCheck
|
|
||||||
import qualified Network.HTTP.Client as HCli
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
spec :: Spec
|
|
||||||
spec = do
|
|
||||||
authSpec
|
|
||||||
cookieAuthSpec
|
|
||||||
jwtAuthSpec
|
|
||||||
throwAllSpec
|
|
||||||
basicAuthSpec
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Auth {{{
|
|
||||||
|
|
||||||
authSpec :: Spec
|
|
||||||
authSpec
|
|
||||||
= describe "The Auth combinator"
|
|
||||||
$ around (testWithApplication . return $ app jwtAndCookieApi) $ do
|
|
||||||
|
|
||||||
it "returns a 401 if all authentications are Indefinite" $ \port -> do
|
|
||||||
get (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "succeeds if one authentication suceeds" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- makeJWT user jwtCfg Nothing
|
|
||||||
opts <- addJwtToHeader jwt
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
it "fails (403) if one authentication fails" $ const $
|
|
||||||
pendingWith "Authentications don't yet fail, only are Indefinite"
|
|
||||||
|
|
||||||
it "doesn't clobber pre-existing response headers" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- makeJWT user jwtCfg Nothing
|
|
||||||
opts <- addJwtToHeader jwt
|
|
||||||
resp <- getWith opts (url port ++ "/header")
|
|
||||||
resp ^. responseHeader "Blah" `shouldBe` "1797"
|
|
||||||
resp ^. responseHeader "Set-Cookie" `shouldSatisfy` (/= "")
|
|
||||||
|
|
||||||
context "Raw" $ do
|
|
||||||
|
|
||||||
it "gets the response body" $ \port -> property $ \(user :: User) -> do
|
|
||||||
jwt <- makeJWT user jwtCfg Nothing
|
|
||||||
opts <- addJwtToHeader jwt
|
|
||||||
resp <- getWith opts (url port ++ "/raw")
|
|
||||||
resp ^. responseBody `shouldBe` "how are you?"
|
|
||||||
|
|
||||||
it "doesn't clobber pre-existing reponse headers" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- makeJWT user jwtCfg Nothing
|
|
||||||
opts <- addJwtToHeader jwt
|
|
||||||
resp <- getWith opts (url port ++ "/raw")
|
|
||||||
resp ^. responseHeader "hi" `shouldBe` "there"
|
|
||||||
resp ^. responseHeader "Set-Cookie" `shouldSatisfy` (/= "")
|
|
||||||
|
|
||||||
|
|
||||||
context "Setting cookies" $ do
|
|
||||||
|
|
||||||
it "sets cookies that it itself accepts" $ \port -> property $ \user -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims $ toJSON user)
|
|
||||||
opts' <- addJwtToCookie cookieCfg jwt
|
|
||||||
let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])
|
|
||||||
(xsrfField xsrfCookieName cookieCfg <> "=blah")
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
let (cookieJar:_) = resp ^.. responseCookieJar
|
|
||||||
Just xxsrf = find (\x -> cookie_name x == xsrfField xsrfCookieName cookieCfg)
|
|
||||||
$ destroyCookieJar cookieJar
|
|
||||||
opts2 = defaults
|
|
||||||
& cookies .~ Just cookieJar
|
|
||||||
& header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ [cookie_value xxsrf]
|
|
||||||
resp2 <- getWith opts2 (url port)
|
|
||||||
resp2 ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
it "uses the Expiry from the configuration" $ \port -> property $ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims $ toJSON user)
|
|
||||||
opts' <- addJwtToCookie cookieCfg jwt
|
|
||||||
let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])
|
|
||||||
(xsrfField xsrfCookieName cookieCfg <> "=blah")
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
let (cookieJar:_) = resp ^.. responseCookieJar
|
|
||||||
Just xxsrf = find (\x -> cookie_name x == xsrfField xsrfCookieName cookieCfg)
|
|
||||||
$ destroyCookieJar cookieJar
|
|
||||||
xxsrf ^. cookieExpiryTime `shouldBe` future
|
|
||||||
|
|
||||||
it "sets the token cookie as HttpOnly" $ \port -> property $ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims $ toJSON user)
|
|
||||||
opts' <- addJwtToCookie cookieCfg jwt
|
|
||||||
let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])
|
|
||||||
(xsrfField xsrfCookieName cookieCfg <> "=blah")
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
let (cookieJar:_) = resp ^.. responseCookieJar
|
|
||||||
Just token = find (\x -> cookie_name x == sessionCookieName cookieCfg)
|
|
||||||
$ destroyCookieJar cookieJar
|
|
||||||
cookie_http_only token `shouldBe` True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Cookie Auth {{{
|
|
||||||
|
|
||||||
cookieAuthSpec :: Spec
|
|
||||||
cookieAuthSpec
|
|
||||||
= describe "The Auth combinator" $ do
|
|
||||||
describe "With XSRF check" $
|
|
||||||
around (testWithApplication . return $ app cookieOnlyApi) $ do
|
|
||||||
|
|
||||||
it "fails if XSRF header and cookie don't match" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts' <- addJwtToCookie cookieCfg jwt
|
|
||||||
let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])
|
|
||||||
(xsrfField xsrfCookieName cookieCfg <> "=blerg")
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "fails with no XSRF header or cookie" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts <- addJwtToCookie cookieCfg jwt
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "succeeds if XSRF header and cookie match, and JWT is valid" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts' <- addJwtToCookie cookieCfg jwt
|
|
||||||
let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])
|
|
||||||
(xsrfField xsrfCookieName cookieCfg <> "=blah")
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
it "sets and clears the right cookies" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
let optsFromResp resp =
|
|
||||||
let jar = resp ^. responseCookieJar
|
|
||||||
Just xsrfCookieValue = cookie_value <$> find (\c -> cookie_name c == xsrfField xsrfCookieName cookieCfg) (destroyCookieJar jar)
|
|
||||||
in defaults
|
|
||||||
& cookies .~ Just jar -- real cookie jars aren't updated by being replaced
|
|
||||||
& header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ [xsrfCookieValue]
|
|
||||||
|
|
||||||
resp <- postWith defaults (url port ++ "/login") user
|
|
||||||
(resp ^. responseCookieJar) `shouldMatchCookieNames`
|
|
||||||
[ sessionCookieName cookieCfg
|
|
||||||
, xsrfField xsrfCookieName cookieCfg
|
|
||||||
]
|
|
||||||
let loggedInOpts = optsFromResp resp
|
|
||||||
|
|
||||||
resp <- getWith loggedInOpts (url port)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
-- logout
|
|
||||||
resp <- getWith loggedInOpts (url port ++ "/logout")
|
|
||||||
|
|
||||||
-- assert cookies were expired
|
|
||||||
now <- getCurrentTime
|
|
||||||
let assertCookie c = now >= cookie_expiry_time c
|
|
||||||
all assertCookie (destroyCookieJar (resp ^. responseCookieJar)) `shouldBe` True
|
|
||||||
|
|
||||||
let loggedOutOpts = optsFromResp resp
|
|
||||||
getWith loggedOutOpts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
describe "With no XSRF check for GET requests" $ let
|
|
||||||
noXsrfGet xsrfCfg = xsrfCfg { xsrfExcludeGet = True }
|
|
||||||
cookieCfgNoXsrfGet = cookieCfg { cookieXsrfSetting = fmap noXsrfGet $ cookieXsrfSetting cookieCfg }
|
|
||||||
in around (testWithApplication . return $ appWithCookie cookieOnlyApi cookieCfgNoXsrfGet) $ do
|
|
||||||
|
|
||||||
it "succeeds with no XSRF header or cookie for GET" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts <- addJwtToCookie cookieCfgNoXsrfGet jwt
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
it "fails with no XSRF header or cookie for POST" $ \port -> property
|
|
||||||
$ \(user :: User) number -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts <- addJwtToCookie cookieCfgNoXsrfGet jwt
|
|
||||||
postWith opts (url port) (toJSON (number :: Int)) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
describe "With no XSRF check at all" $ let
|
|
||||||
cookieCfgNoXsrf = cookieCfg { cookieXsrfSetting = Nothing }
|
|
||||||
in around (testWithApplication . return $ appWithCookie cookieOnlyApi cookieCfgNoXsrf) $ do
|
|
||||||
|
|
||||||
it "succeeds with no XSRF header or cookie for GET" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts <- addJwtToCookie cookieCfgNoXsrf jwt
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
it "succeeds with no XSRF header or cookie for POST" $ \port -> property
|
|
||||||
$ \(user :: User) number -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
|
|
||||||
opts <- addJwtToCookie cookieCfgNoXsrf jwt
|
|
||||||
resp <- postWith opts (url port) $ toJSON (number :: Int)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just number
|
|
||||||
|
|
||||||
it "sets and clears the right cookies" $ \port -> property
|
|
||||||
$ \(user :: User) -> do
|
|
||||||
let optsFromResp resp = defaults
|
|
||||||
& cookies .~ Just (resp ^. responseCookieJar) -- real cookie jars aren't updated by being replaced
|
|
||||||
|
|
||||||
resp <- postWith defaults (url port ++ "/login") user
|
|
||||||
(resp ^. responseCookieJar) `shouldMatchCookieNames`
|
|
||||||
[ sessionCookieName cookieCfg
|
|
||||||
, "NO-XSRF-TOKEN"
|
|
||||||
]
|
|
||||||
let loggedInOpts = optsFromResp resp
|
|
||||||
|
|
||||||
resp <- getWith (loggedInOpts) (url port)
|
|
||||||
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)
|
|
||||||
|
|
||||||
resp <- getWith loggedInOpts (url port ++ "/logout")
|
|
||||||
(resp ^. responseCookieJar) `shouldMatchCookieNameValues`
|
|
||||||
[ (sessionCookieName cookieCfg, "value")
|
|
||||||
, ("NO-XSRF-TOKEN", "")
|
|
||||||
]
|
|
||||||
|
|
||||||
-- assert cookies were expired
|
|
||||||
now <- getCurrentTime
|
|
||||||
let assertCookie c = now >= cookie_expiry_time c
|
|
||||||
all assertCookie (destroyCookieJar (resp ^. responseCookieJar)) `shouldBe` True
|
|
||||||
|
|
||||||
let loggedOutOpts = optsFromResp resp
|
|
||||||
|
|
||||||
getWith loggedOutOpts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * JWT Auth {{{
|
|
||||||
|
|
||||||
jwtAuthSpec :: Spec
|
|
||||||
jwtAuthSpec
|
|
||||||
= describe "The JWT combinator"
|
|
||||||
$ around (testWithApplication . return $ app jwtOnlyApi) $ do
|
|
||||||
|
|
||||||
it "fails if 'aud' does not match predicate" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims (toJSON user) & claimAud .~ Just (Audience ["boo"]))
|
|
||||||
opts <- addJwtToHeader (jwt >>= (return . encodeCompact))
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "succeeds if 'aud' does match predicate" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims (toJSON user) & claimAud .~ Just (Audience ["anythingElse"]))
|
|
||||||
opts <- addJwtToHeader (jwt >>= (return . encodeCompact))
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
resp ^. responseStatus `shouldBe` status200
|
|
||||||
|
|
||||||
it "fails if 'nbf' is set to a future date" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims (toJSON user) & claimNbf .~ Just (NumericDate future))
|
|
||||||
opts <- addJwtToHeader (jwt >>= (return . encodeCompact))
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "fails if 'exp' is set to a past date" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- makeJWT user jwtCfg (Just past)
|
|
||||||
opts <- addJwtToHeader jwt
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "succeeds if 'exp' is set to a future date" $ \port -> property $
|
|
||||||
\(user :: User) -> do
|
|
||||||
jwt <- makeJWT user jwtCfg (Just future)
|
|
||||||
opts <- addJwtToHeader jwt
|
|
||||||
resp <- getWith opts (url port)
|
|
||||||
resp ^. responseStatus `shouldBe` status200
|
|
||||||
|
|
||||||
it "fails if JWT is not signed" $ \port -> property $ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), None))
|
|
||||||
(claims $ toJSON user)
|
|
||||||
opts <- addJwtToHeader (jwt >>= (return . encodeCompact))
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "fails if JWT does not use expected algorithm" $ const $
|
|
||||||
pendingWith "Need https://github.com/frasertweedale/hs-jose/issues/19"
|
|
||||||
|
|
||||||
it "fails if data is not valid JSON" $ \port -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims "{{")
|
|
||||||
opts <- addJwtToHeader (jwt >>= (return .encodeCompact))
|
|
||||||
getWith opts (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "suceeds as wreq's oauth2Bearer" $ \port -> property $ \(user :: User) -> do
|
|
||||||
jwt <- createJWT theKey (newJWSHeader ((), HS256))
|
|
||||||
(claims $ toJSON user)
|
|
||||||
resp <- case jwt >>= (return . encodeCompact) of
|
|
||||||
Left (e :: Error) -> fail $ show e
|
|
||||||
Right v -> getWith (defaults & auth ?~ oauth2Bearer (BSL.toStrict v)) (url port)
|
|
||||||
resp ^. responseStatus `shouldBe` status200
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Basic Auth {{{
|
|
||||||
|
|
||||||
basicAuthSpec :: Spec
|
|
||||||
basicAuthSpec = describe "The BasicAuth combinator"
|
|
||||||
$ around (testWithApplication . return $ app basicAuthApi) $ do
|
|
||||||
|
|
||||||
it "succeeds with the correct password and username" $ \port -> do
|
|
||||||
resp <- getWith (defaults & auth ?~ basicAuth "ali" "Open sesame") (url port)
|
|
||||||
resp ^. responseStatus `shouldBe` status200
|
|
||||||
|
|
||||||
it "fails with non-existent user" $ \port -> do
|
|
||||||
getWith (defaults & auth ?~ basicAuth "thief" "Open sesame") (url port)
|
|
||||||
`shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "fails with incorrect password" $ \port -> do
|
|
||||||
getWith (defaults & auth ?~ basicAuth "ali" "phatic") (url port)
|
|
||||||
`shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
it "fails with no auth header" $ \port -> do
|
|
||||||
get (url port) `shouldHTTPErrorWith` status401
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * ThrowAll {{{
|
|
||||||
|
|
||||||
throwAllSpec :: Spec
|
|
||||||
throwAllSpec = describe "throwAll" $ do
|
|
||||||
|
|
||||||
it "works for plain values" $ do
|
|
||||||
let t :: Either ServerError Int :<|> Either ServerError Bool :<|> Either ServerError String
|
|
||||||
t = throwAll err401
|
|
||||||
t `shouldBe` throwError err401 :<|> throwError err401 :<|> throwError err401
|
|
||||||
|
|
||||||
it "works for function types" $ property $ \i -> do
|
|
||||||
let t :: Int -> (Either ServerError Bool :<|> Either ServerError String)
|
|
||||||
t = throwAll err401
|
|
||||||
expected _ = throwError err401 :<|> throwError err401
|
|
||||||
t i `shouldBe` expected i
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * API and Server {{{
|
|
||||||
|
|
||||||
type API auths
|
|
||||||
= Auth auths User :>
|
|
||||||
( Get '[JSON] Int
|
|
||||||
:<|> ReqBody '[JSON] Int :> Post '[JSON] Int
|
|
||||||
:<|> NamedRoutes DummyRoutes
|
|
||||||
:<|> "header" :> Get '[JSON] (Headers '[Header "Blah" Int] Int)
|
|
||||||
#if MIN_VERSION_servant_server(0,15,0)
|
|
||||||
:<|> "stream" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
|
|
||||||
#endif
|
|
||||||
:<|> "raw" :> Raw
|
|
||||||
)
|
|
||||||
:<|> "login" :> ReqBody '[JSON] User :> Post '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
|
|
||||||
, Header "Set-Cookie" SetCookie ] NoContent)
|
|
||||||
:<|> "logout" :> Get '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
|
|
||||||
, Header "Set-Cookie" SetCookie ] NoContent)
|
|
||||||
|
|
||||||
data DummyRoutes mode = DummyRoutes
|
|
||||||
{ dummyInt :: mode :- "dummy" :> Get '[JSON] Int
|
|
||||||
} deriving Generic
|
|
||||||
|
|
||||||
jwtOnlyApi :: Proxy (API '[Servant.Auth.Server.JWT])
|
|
||||||
jwtOnlyApi = Proxy
|
|
||||||
|
|
||||||
cookieOnlyApi :: Proxy (API '[Cookie])
|
|
||||||
cookieOnlyApi = Proxy
|
|
||||||
|
|
||||||
basicAuthApi :: Proxy (API '[BasicAuth])
|
|
||||||
basicAuthApi = Proxy
|
|
||||||
|
|
||||||
jwtAndCookieApi :: Proxy (API '[Servant.Auth.Server.JWT, Cookie])
|
|
||||||
jwtAndCookieApi = Proxy
|
|
||||||
|
|
||||||
theKey :: JWK
|
|
||||||
theKey = unsafePerformIO . genJWK $ OctGenParam 256
|
|
||||||
{-# NOINLINE theKey #-}
|
|
||||||
|
|
||||||
|
|
||||||
cookieCfg :: CookieSettings
|
|
||||||
cookieCfg = def
|
|
||||||
{ cookieExpires = Just future
|
|
||||||
, cookieIsSecure = NotSecure
|
|
||||||
, sessionCookieName = "RuncibleSpoon"
|
|
||||||
, cookieXsrfSetting = pure $ def
|
|
||||||
{ xsrfCookieName = "TheyDinedOnMince"
|
|
||||||
, xsrfHeaderName = "AndSlicesOfQuince"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xsrfField :: (XsrfCookieSettings -> a) -> CookieSettings -> a
|
|
||||||
xsrfField f = maybe (error "expected XsrfCookieSettings for test") f . cookieXsrfSetting
|
|
||||||
|
|
||||||
jwtCfg :: JWTSettings
|
|
||||||
jwtCfg = (defaultJWTSettings theKey) { audienceMatches = \x ->
|
|
||||||
if x == "boo" then DoesNotMatch else Matches }
|
|
||||||
|
|
||||||
instance FromBasicAuthData User where
|
|
||||||
fromBasicAuthData (BasicAuthData usr pwd) _
|
|
||||||
= return $ if usr == "ali" && pwd == "Open sesame"
|
|
||||||
then Authenticated $ User "ali" "ali@the-thieves-den.com"
|
|
||||||
else Indefinite
|
|
||||||
|
|
||||||
-- Could be anything, really, but since this is already in the cfg we don't
|
|
||||||
-- have to add it
|
|
||||||
type instance BasicAuthCfg = JWK
|
|
||||||
|
|
||||||
appWithCookie :: AreAuths auths '[CookieSettings, JWTSettings, JWK] User
|
|
||||||
=> Proxy (API auths) -> CookieSettings -> Application
|
|
||||||
appWithCookie api ccfg = serveWithContext api ctx $ server ccfg
|
|
||||||
where
|
|
||||||
ctx = ccfg :. jwtCfg :. theKey :. EmptyContext
|
|
||||||
|
|
||||||
-- | Takes a proxy parameter indicating which authentication systems to enable.
|
|
||||||
app :: AreAuths auths '[CookieSettings, JWTSettings, JWK] User
|
|
||||||
=> Proxy (API auths) -> Application
|
|
||||||
app api = appWithCookie api cookieCfg
|
|
||||||
|
|
||||||
server :: CookieSettings -> Server (API auths)
|
|
||||||
server ccfg =
|
|
||||||
(\authResult -> case authResult of
|
|
||||||
Authenticated usr -> getInt usr
|
|
||||||
:<|> postInt usr
|
|
||||||
:<|> DummyRoutes { dummyInt = getInt usr }
|
|
||||||
:<|> getHeaderInt
|
|
||||||
#if MIN_VERSION_servant_server(0,15,0)
|
|
||||||
:<|> return (S.source ["bytestring"])
|
|
||||||
#endif
|
|
||||||
:<|> raw
|
|
||||||
Indefinite -> throwAll err401
|
|
||||||
_ -> throwAll err403
|
|
||||||
)
|
|
||||||
:<|> getLogin
|
|
||||||
:<|> getLogout
|
|
||||||
where
|
|
||||||
getInt :: User -> Handler Int
|
|
||||||
getInt usr = return . length $ name usr
|
|
||||||
|
|
||||||
postInt :: User -> Int -> Handler Int
|
|
||||||
postInt _ = return
|
|
||||||
|
|
||||||
getHeaderInt :: Handler (Headers '[Header "Blah" Int] Int)
|
|
||||||
getHeaderInt = return $ addHeader 1797 17
|
|
||||||
|
|
||||||
getLogin :: User -> Handler (Headers '[ Header "Set-Cookie" SetCookie
|
|
||||||
, Header "Set-Cookie" SetCookie ] NoContent)
|
|
||||||
getLogin user = do
|
|
||||||
maybeApplyCookies <- liftIO $ acceptLogin ccfg jwtCfg user
|
|
||||||
case maybeApplyCookies of
|
|
||||||
Just applyCookies -> return $ applyCookies NoContent
|
|
||||||
Nothing -> error "cookies failed to apply"
|
|
||||||
|
|
||||||
getLogout :: Handler (Headers '[ Header "Set-Cookie" SetCookie
|
|
||||||
, Header "Set-Cookie" SetCookie ] NoContent)
|
|
||||||
getLogout = return $ clearSession ccfg NoContent
|
|
||||||
|
|
||||||
raw :: Server Raw
|
|
||||||
raw =
|
|
||||||
#if MIN_VERSION_servant_server(0,11,0)
|
|
||||||
Tagged $
|
|
||||||
#endif
|
|
||||||
\_req respond ->
|
|
||||||
respond $ responseLBS status200 [("hi", "there")] "how are you?"
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Utils {{{
|
|
||||||
|
|
||||||
past :: UTCTime
|
|
||||||
past = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "1970-01-01"
|
|
||||||
|
|
||||||
future :: UTCTime
|
|
||||||
future = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "2070-01-01"
|
|
||||||
|
|
||||||
addJwtToHeader :: Either Error BSL.ByteString -> IO Options
|
|
||||||
addJwtToHeader jwt = case jwt of
|
|
||||||
Left e -> fail $ show e
|
|
||||||
Right v -> return
|
|
||||||
$ defaults & header "Authorization" .~ ["Bearer " <> BSL.toStrict v]
|
|
||||||
|
|
||||||
createJWT :: JWK -> JWSHeader () -> ClaimsSet -> IO (Either Error Crypto.JWT.SignedJWT)
|
|
||||||
createJWT k a b = runJOSE $ signClaims k a b
|
|
||||||
|
|
||||||
addJwtToCookie :: ToCompact a => CookieSettings -> Either Error a -> IO Options
|
|
||||||
addJwtToCookie ccfg jwt = case jwt >>= (return . encodeCompact) of
|
|
||||||
Left e -> fail $ show e
|
|
||||||
Right v -> return
|
|
||||||
$ defaults & header "Cookie" .~ [sessionCookieName ccfg <> "=" <> BSL.toStrict v]
|
|
||||||
|
|
||||||
addCookie :: Options -> BS.ByteString -> Options
|
|
||||||
addCookie opts cookie' = opts & header "Cookie" %~ \c -> case c of
|
|
||||||
[h] -> [cookie' <> "; " <> h]
|
|
||||||
[] -> [cookie']
|
|
||||||
_ -> error "expecting single cookie header"
|
|
||||||
|
|
||||||
|
|
||||||
shouldHTTPErrorWith :: IO a -> Status -> Expectation
|
|
||||||
shouldHTTPErrorWith act stat = act `shouldThrow` \e -> case e of
|
|
||||||
#if MIN_VERSION_http_client(0,5,0)
|
|
||||||
HCli.HttpExceptionRequest _ (HCli.StatusCodeException resp _)
|
|
||||||
-> HCli.responseStatus resp == stat
|
|
||||||
#else
|
|
||||||
HCli.StatusCodeException x _ _ -> x == stat
|
|
||||||
#endif
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
shouldMatchCookieNames :: HCli.CookieJar -> [BS.ByteString] -> Expectation
|
|
||||||
shouldMatchCookieNames cj patterns
|
|
||||||
= fmap cookie_name (destroyCookieJar cj)
|
|
||||||
`shouldMatchList` patterns
|
|
||||||
|
|
||||||
shouldMatchCookieNameValues :: HCli.CookieJar -> [(BS.ByteString, BS.ByteString)] -> Expectation
|
|
||||||
shouldMatchCookieNameValues cj patterns
|
|
||||||
= fmap ((,) <$> cookie_name <*> cookie_value) (destroyCookieJar cj)
|
|
||||||
`shouldMatchList` patterns
|
|
||||||
|
|
||||||
url :: Int -> String
|
|
||||||
url port = "http://localhost:" <> show port
|
|
||||||
|
|
||||||
claims :: Value -> ClaimsSet
|
|
||||||
claims val = emptyClaimsSet & unregisteredClaims . at "dat" .~ Just val
|
|
||||||
-- }}}
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- * Types {{{
|
|
||||||
|
|
||||||
data User = User
|
|
||||||
{ name :: String
|
|
||||||
, _id :: String
|
|
||||||
} deriving (Eq, Show, Read, Generic)
|
|
||||||
|
|
||||||
instance FromJWT User
|
|
||||||
instance ToJWT User
|
|
||||||
instance FromJSON User
|
|
||||||
instance ToJSON User
|
|
||||||
|
|
||||||
instance Arbitrary User where
|
|
||||||
arbitrary = User <$> arbitrary <*> arbitrary
|
|
||||||
|
|
||||||
instance Postable User where
|
|
||||||
postPayload user request = return $ request
|
|
||||||
{ HCli.requestBody = HCli.RequestBodyLBS $ encode user
|
|
||||||
, HCli.requestHeaders = (mk "Content-Type", "application/json") : HCli.requestHeaders request
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
-- }}}
|
|
|
@ -1 +0,0 @@
|
||||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
|
|
@ -1 +0,0 @@
|
||||||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
||||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [0.2.10.1] - 2020-10-06
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Support GHC 8.10 @domenkozar
|
|
||||||
- Fix build with swagger 2.5.x @domenkozar
|
|
||||||
|
|
||||||
## [0.2.10.0] - 2018-06-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for GHC 8.4 by @phadej
|
|
||||||
- Changelog by @domenkozar
|
|
||||||
- #93: Add Cookie in SwaggerSpec API by @domenkozar
|
|
||||||
- #42: Add dummy AllHasSecurity Cookie instance by @sordina
|
|
|
@ -1,31 +0,0 @@
|
||||||
Copyright Julian K. Arni (c) 2015
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of Julian K. Arni nor the names of other
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Distribution.Simple
|
|
||||||
main = defaultMain
|
|
|
@ -1,70 +0,0 @@
|
||||||
cabal-version: 2.2
|
|
||||||
name: servant-auth-swagger
|
|
||||||
version: 0.2.10.1
|
|
||||||
synopsis: servant-swagger/servant-auth compatibility
|
|
||||||
description: This package provides instances that allow generating swagger2 schemas from
|
|
||||||
<https://hackage.haskell.org/package/servant servant>
|
|
||||||
APIs that use
|
|
||||||
<https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
|
|
||||||
.
|
|
||||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
|
||||||
category: Web, Servant, Authentication
|
|
||||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
|
||||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
|
||||||
author: Julian K. Arni
|
|
||||||
maintainer: jkarni@gmail.com
|
|
||||||
copyright: (c) Julian K. Arni
|
|
||||||
license: BSD-3-Clause
|
|
||||||
license-file: LICENSE
|
|
||||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4
|
|
||||||
build-type: Simple
|
|
||||||
extra-source-files:
|
|
||||||
CHANGELOG.md
|
|
||||||
|
|
||||||
source-repository head
|
|
||||||
type: git
|
|
||||||
location: https://github.com/haskell-servant/servant
|
|
||||||
|
|
||||||
library
|
|
||||||
hs-source-dirs:
|
|
||||||
src
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-depends:
|
|
||||||
base >= 4.10 && < 4.18
|
|
||||||
, text >= 1.2.3.0 && < 2.1
|
|
||||||
, servant-swagger >= 1.1.5 && < 2
|
|
||||||
, swagger2 >= 2.2.2 && < 3
|
|
||||||
, servant >= 0.13 && < 0.20
|
|
||||||
, servant-auth == 0.4.*
|
|
||||||
, lens >= 4.16.1 && < 5.3
|
|
||||||
exposed-modules:
|
|
||||||
Servant.Auth.Swagger
|
|
||||||
default-language: Haskell2010
|
|
||||||
|
|
||||||
test-suite spec
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
main-is: Spec.hs
|
|
||||||
hs-source-dirs:
|
|
||||||
test
|
|
||||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
|
||||||
ghc-options: -Wall
|
|
||||||
build-tool-depends: hspec-discover:hspec-discover >= 2.5.5 && <2.10
|
|
||||||
-- dependencies with bounds inherited from the library stanza
|
|
||||||
build-depends:
|
|
||||||
base
|
|
||||||
, text
|
|
||||||
, servant-swagger
|
|
||||||
, swagger2
|
|
||||||
, servant
|
|
||||||
, servant-auth
|
|
||||||
, lens
|
|
||||||
|
|
||||||
-- test dependencies
|
|
||||||
build-depends:
|
|
||||||
servant-auth-swagger
|
|
||||||
, hspec >= 2.5.5 && < 2.10
|
|
||||||
, QuickCheck >= 2.11.3 && < 2.15
|
|
||||||
other-modules:
|
|
||||||
Servant.Auth.SwaggerSpec
|
|
||||||
default-language: Haskell2010
|
|
|
@ -1,87 +0,0 @@
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
module Servant.Auth.Swagger
|
|
||||||
(
|
|
||||||
-- | The purpose of this package is provide the instance for 'servant-auth'
|
|
||||||
-- combinators needed for 'servant-swagger' documentation generation.
|
|
||||||
--
|
|
||||||
-- Currently only JWT and BasicAuth are supported.
|
|
||||||
|
|
||||||
-- * Re-export
|
|
||||||
JWT
|
|
||||||
, BasicAuth
|
|
||||||
, Auth
|
|
||||||
|
|
||||||
-- * Needed to define instances of @HasSwagger@
|
|
||||||
, HasSecurity (..)
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Control.Lens ((&), (<>~))
|
|
||||||
import Data.Proxy (Proxy (Proxy))
|
|
||||||
import Data.Swagger (ApiKeyLocation (..), ApiKeyParams (..),
|
|
||||||
SecurityRequirement (..), SecurityScheme (..),
|
|
||||||
#if MIN_VERSION_swagger2(2,6,0)
|
|
||||||
SecurityDefinitions(..),
|
|
||||||
#endif
|
|
||||||
SecuritySchemeType (..), allOperations, security,
|
|
||||||
securityDefinitions)
|
|
||||||
import GHC.Exts (fromList)
|
|
||||||
import Servant.API hiding (BasicAuth)
|
|
||||||
import Servant.Auth
|
|
||||||
import Servant.Swagger
|
|
||||||
|
|
||||||
import qualified Data.Text as T
|
|
||||||
|
|
||||||
instance (AllHasSecurity xs, HasSwagger api) => HasSwagger (Auth xs r :> api) where
|
|
||||||
toSwagger _
|
|
||||||
= toSwagger (Proxy :: Proxy api)
|
|
||||||
& securityDefinitions <>~ mkSec (fromList secs)
|
|
||||||
& allOperations.security <>~ secReqs
|
|
||||||
where
|
|
||||||
secs = securities (Proxy :: Proxy xs)
|
|
||||||
secReqs = [ SecurityRequirement (fromList [(s,[])]) | (s,_) <- secs]
|
|
||||||
mkSec =
|
|
||||||
#if MIN_VERSION_swagger2(2,6,0)
|
|
||||||
SecurityDefinitions
|
|
||||||
#else
|
|
||||||
id
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
class HasSecurity x where
|
|
||||||
securityName :: Proxy x -> T.Text
|
|
||||||
securityScheme :: Proxy x -> SecurityScheme
|
|
||||||
|
|
||||||
instance HasSecurity BasicAuth where
|
|
||||||
securityName _ = "BasicAuth"
|
|
||||||
securityScheme _ = SecurityScheme type_ (Just desc)
|
|
||||||
where
|
|
||||||
type_ = SecuritySchemeBasic
|
|
||||||
desc = "Basic access authentication"
|
|
||||||
|
|
||||||
instance HasSecurity JWT where
|
|
||||||
securityName _ = "JwtSecurity"
|
|
||||||
securityScheme _ = SecurityScheme type_ (Just desc)
|
|
||||||
where
|
|
||||||
type_ = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader)
|
|
||||||
desc = "JSON Web Token-based API key"
|
|
||||||
|
|
||||||
class AllHasSecurity (x :: [*]) where
|
|
||||||
securities :: Proxy x -> [(T.Text,SecurityScheme)]
|
|
||||||
|
|
||||||
instance {-# OVERLAPPABLE #-} (HasSecurity x, AllHasSecurity xs) => AllHasSecurity (x ': xs) where
|
|
||||||
securities _ = (securityName px, securityScheme px) : securities pxs
|
|
||||||
where
|
|
||||||
px :: Proxy x
|
|
||||||
px = Proxy
|
|
||||||
pxs :: Proxy xs
|
|
||||||
pxs = Proxy
|
|
||||||
|
|
||||||
instance {-# OVERLAPPING #-} AllHasSecurity xs => AllHasSecurity (Cookie ': xs) where
|
|
||||||
securities _ = securities pxs
|
|
||||||
where
|
|
||||||
pxs :: Proxy xs
|
|
||||||
pxs = Proxy
|
|
||||||
|
|
||||||
instance AllHasSecurity '[] where
|
|
||||||
securities _ = []
|
|
|
@ -1,38 +0,0 @@
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
module Servant.Auth.SwaggerSpec (spec) where
|
|
||||||
|
|
||||||
import Control.Lens
|
|
||||||
import Data.Proxy
|
|
||||||
import Servant.API
|
|
||||||
import Servant.Auth
|
|
||||||
import Servant.Auth.Swagger
|
|
||||||
import Data.Swagger
|
|
||||||
import Servant.Swagger
|
|
||||||
import Test.Hspec
|
|
||||||
|
|
||||||
spec :: Spec
|
|
||||||
spec = describe "HasSwagger instance" $ do
|
|
||||||
|
|
||||||
let swag = toSwagger (Proxy :: Proxy API)
|
|
||||||
|
|
||||||
it "adds security definitions at the top level" $ do
|
|
||||||
#if MIN_VERSION_swagger2(2,6,0)
|
|
||||||
let (SecurityDefinitions secDefs) = swag ^. securityDefinitions
|
|
||||||
#else
|
|
||||||
let secDefs = swag ^. securityDefinitions
|
|
||||||
#endif
|
|
||||||
length secDefs `shouldSatisfy` (> 0)
|
|
||||||
|
|
||||||
it "adds security at sub-apis" $ do
|
|
||||||
swag ^. security `shouldBe` []
|
|
||||||
show (swag ^. paths . at "/secure") `shouldContain` "JwtSecurity"
|
|
||||||
show (swag ^. paths . at "/insecure") `shouldNotContain` "JwtSecurity"
|
|
||||||
|
|
||||||
-- * API
|
|
||||||
|
|
||||||
type API = "secure" :> Auth '[JWT, Cookie] Int :> SecureAPI
|
|
||||||
:<|> "insecure" :> InsecureAPI
|
|
||||||
|
|
||||||
type SecureAPI = Get '[JSON] Int :<|> ReqBody '[JSON] Int :> Post '[JSON] Int
|
|
||||||
|
|
||||||
type InsecureAPI = SecureAPI
|
|
|
@ -1 +0,0 @@
|
||||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
|
|
@ -1 +0,0 @@
|
||||||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue