Skip to content

Commit

Permalink
feat: allow configuring the cookie name (#563)
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas authored Nov 15, 2021
1 parent 6fb1621 commit 99bf84c
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 95 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/ci.yaml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ jobs:
run: ./tests/use-go-deadlock.sh

- name: Test
run: go test -race -covermode atomic -coverprofile=profile.cov ./...
run: go test -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./...

- name: Test Caddy module
run: go test -timeout 1m -race ./...
run: |
go test -timeout 1m -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./...
sed '1d' profile.cov >> ../profile.cov
working-directory: ./caddy

- name: Upload coverage results
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
GO111MODULE=off go get github.com/mattn/goveralls
$(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
9 changes: 5 additions & 4 deletions authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ type mercureClaim struct {
type role int

const (
roleSubscriber role = iota
defaultCookieName = "mercureAuthorization"
roleSubscriber role = iota
rolePublisher
)

Expand All @@ -45,9 +46,9 @@ var (
ErrPublicKey = errors.New("public key error")
)

// Authorize validates the JWT that may be provided through an "Authorization" HTTP header or a "mercureAuthorization" cookie.
// Authorize validates the JWT that may be provided through an "Authorization" HTTP header or an authorization cookie.
// It returns the claims contained in the token if it exists and is valid, nil if no token is provided (anonymous mode), and an error if the token is not valid.
func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string) (*claims, error) {
func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string, cookieName string) (*claims, error) {
authorizationHeaders, headerExists := r.Header["Authorization"]
if headerExists {
if len(authorizationHeaders) != 1 || len(authorizationHeaders[0]) < 48 || authorizationHeaders[0][:7] != "Bearer " {
Expand All @@ -57,7 +58,7 @@ func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string) (
return validateJWT(authorizationHeaders[0][7:], jwtConfig)
}

cookie, err := r.Cookie("mercureAuthorization")
cookie, err := r.Cookie(cookieName)
if err != nil {
// Anonymous
return nil, nil //nolint:nilerr,nilnil
Expand Down
115 changes: 62 additions & 53 deletions authorization_test.go

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ type Mercure struct {
// Triggers use of LRU topic selector cache and avoidance of select priority queue (recommend 10,000 - 1,000,000)
LRUShardSize *int64 `json:"lru_shard_size,omitempty"`

// The name of the authorization cookie. Defaults to "mercureAuthorization".
CookieName string `json:"cookie_name,omitempty"`

hub *mercure.Hub
logger *zap.Logger
}
Expand Down Expand Up @@ -157,6 +160,7 @@ func (m *Mercure) Provision(ctx caddy.Context) error { //nolint:funlen
mercure.WithTransport(destructor.(*transportDestructor).transport),
mercure.WithMetrics(metrics),
mercure.WithPublisherJWT([]byte(m.PublisherJWT.Key), m.PublisherJWT.Alg),
mercure.WithCookieName(m.CookieName),
}
if m.logger.Core().Enabled(zapcore.DebugLevel) {
opts = append(opts, mercure.WithDebug())
Expand Down Expand Up @@ -336,6 +340,13 @@ func (m *Mercure) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { //nolint:fu
}

m.LRUShardSize = &v

case "cookie_name":
if !d.NextArg() {
return d.ArgErr()
}

m.CookieName = d.Val()
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions caddy/caddy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
const (
publisherJWT = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.vhMwOaN5K68BTIhWokMLOeOJO4EPfT64brd8euJOA4M"
publisherJWTRSA = "eyJhbGciOiJSUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.iwryQ5k-CWNCNQLPg7CtgTdDWbG_CurSxDK8kMjTZfprGhh7Yli1SFt8WB3U4zbZ2wxUO7UfprZq3hnl8nSrozO9KDTCDwCYhMgRlcrdwm6XL1uXFwMJt4VSmp1srCQotv0FgT11jF8Km1vMQQOnUC27Va9fbfRtITVsjxsveYeMJqusVWO6F3vAvkM35oL8E8qgBbfrG_lnuhb_9Ws6RIq4YOslkOar_gopEs00CITxmV_aHVHRYzeW7QpycxjC7m8Mp-lKzaUewvJuKWI5HsM134xfaH8RAHSvh6H9pVQAiJ9tyc17bAx46M98WMsHFokVwz3rd7PoGGou6A7y5RzeGpiSxykTWCPPcBnxJ1gwUYqEYGTnRjl9JmhHY_VfQP4edyU-zhmMCCSie8rvkRDilAQGd5kj5m1voSn-EqA13sSe69evXxVUIB2nO70qHCcHBBHxunLqTIIerpc3F9_WWM4_Q_0j9CoTd2aFyuq_sdc6RcmAE3uTznp2DyKNQkT1EfpY7xCCe1MR-Webez5Ioa1EMDP0KrvLdnNRmuM3THSu1pqcvPV7Di7dJci5QWsYEmaP8cLuuZXdAhy_UoSgzbvfT_8mlDoJ9VvDXLJ39OwGYIyZiZ9VTNXm8mxre993cqg7boZRS8x70VRxnjmNxm40SgEvb6CHYO0lSBU"
subscriberJWT = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.g3w81T7YQLKLrgovor9uEKUiOCAx6DmAAbq18qmDwsY"
)

func TestMercure(t *testing.T) {
Expand Down Expand Up @@ -183,3 +184,74 @@ func TestSubscriptionAPI(t *testing.T) {
resp := tester.AssertResponseCode(req, http.StatusOK)
resp.Body.Close()
}

func TestCookieName(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9080 {
route {
mercure {
publisher_jwt !ChangeMe!
subscriber_jwt !ChangeMe!
cookie_name foo
publish_origins http://localhost:9080
}
respond 404
}
}
`, "caddyfile")

var connected sync.WaitGroup
var received sync.WaitGroup
connected.Add(1)
received.Add(1)

go func() {
cx, cancel := context.WithCancel(context.Background())
req, _ := http.NewRequest("GET", "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
req.Header.Add("Origin", "http://localhost:9080")
req.AddCookie(&http.Cookie{Name: "foo", Value: subscriberJWT})
req = req.WithContext(cx)
resp := tester.AssertResponseCode(req, http.StatusOK)

connected.Done()

var receivedBody strings.Builder
buf := make([]byte, 1024)
for {
_, err := resp.Body.Read(buf)
if errors.Is(err, io.EOF) {
panic("EOF")
}

receivedBody.Write(buf)
if strings.Contains(receivedBody.String(), "data: bar\n") {
cancel()

break
}
}

resp.Body.Close()
received.Done()
}()

connected.Wait()

body := url.Values{"topic": {"http://example.com/foo/1"}, "data": {"bar"}, "id": {"bar"}, "private": {"1"}}
req, err := http.NewRequest("POST", "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
require.Nil(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Origin", "http://localhost:9080")
req.AddCookie(&http.Cookie{Name: "foo", Value: publisherJWT})

resp := tester.AssertResponseCode(req, http.StatusOK)
resp.Body.Close()

received.Wait()
}
14 changes: 9 additions & 5 deletions demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package mercure

import (
"embed"
"fmt"
"io"
"mime"
"net/http"
Expand All @@ -18,7 +17,7 @@ var uiContent embed.FS
// Add a query parameter named "body" to define the content to return in the response's body.
// Add a query parameter named "jwt" set a "mercureAuthorization" cookie containing this token.
// The Content-Type header will automatically be set according to the URL's extension.
func Demo(w http.ResponseWriter, r *http.Request) {
func (h *Hub) Demo(w http.ResponseWriter, r *http.Request) {
// JSON-LD is the preferred format
mime.AddExtensionType(".jsonld", "application/ld+json")

Expand All @@ -29,16 +28,21 @@ func Demo(w http.ResponseWriter, r *http.Request) {
body := query.Get("body")
jwt := query.Get("jwt")

hubLink := "<" + defaultHubURL + ">; rel=\"mercure\""
if h.cookieName != defaultCookieName {
hubLink = hubLink + "; cookie-name=\"" + h.cookieName + "\""
}

header := w.Header()
// Several Link headers are set on purpose to allow testing advanced discovery mechanism
header.Add("Link", "<"+defaultHubURL+">; rel=\"mercure\"")
header.Add("Link", fmt.Sprintf("<%s>; rel=\"self\"", url))
header.Add("Link", hubLink)
header.Add("Link", "<"+url+">; rel=\"self\"")
if mimeType != "" {
header.Set("Content-Type", mimeType)
}

cookie := &http.Cookie{
Name: "mercureAuthorization",
Name: h.cookieName,
Path: defaultHubURL,
Value: jwt,
HttpOnly: r.TLS != nil,
Expand Down
8 changes: 6 additions & 2 deletions demo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
func TestEmptyBodyAndJWT(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/demo/foo.jsonld", nil)
w := httptest.NewRecorder()
Demo(w, req)

h, _ := NewHub()
h.Demo(w, req)

resp := w.Result()
assert.Equal(t, "application/ld+json", resp.Header.Get("Content-Type"))
Expand All @@ -30,7 +32,9 @@ func TestEmptyBodyAndJWT(t *testing.T) {
func TestBodyAndJWT(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/demo/foo/bar.xml?body=<hello/>&jwt=token", nil)
w := httptest.NewRecorder()
Demo(w, req)

h, _ := NewHub()
h.Demo(w, req)

resp := w.Result()
assert.Contains(t, resp.Header.Get("Content-Type"), "xml") // Before Go 1.17, the charset wasn't set
Expand Down
31 changes: 16 additions & 15 deletions docs/hub/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,22 @@ Note that HTTPS is automatically disabled if you set the server port to 80.

The following Mercure-specific directives are available:

| Directive | Description | Default |
|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------|
| `publisher_jwt <key> [<algorithm>]` | the JWT key and algorithm to use for publishers, can contain [placeholders](https://caddyserver.com/docs/conventions#placeholders) | |
| `subscriber_jwt <key> [<algorithm>]` | the JWT key and algorithm to use for subscribers, can contain [placeholders](https://caddyserver.com/docs/conventions#placeholders) | |
| `anonymous` | allow subscribers with no valid JWT to connect | `false` |
| `publish_origins <origins...>` | a list of origins allowed publishing, can be `*` for all (only applicable when using cookie-based auth) | |
| `cors_origins <origin...>` | a list of allowed CORS origins, ([troubleshoot CORS issues](troubleshooting.md#cors-issues)) | |
| `subscriptions` | expose the subscription web API and dispatch private updates when a subscription between the Hub and a subscriber is established or closed. The topic follows the template `/.well-known/mercure/subscriptions/{topicSelector}/{subscriberID}` | |
| `heartbeat` | interval between heartbeats (useful with some proxies, and old browsers), set to `0s` disable | `40s` |
| `transport_url <url>` | URL representation of the transport to use. Use `local://local` to disabled history, (example `bolt:///var/run/mercure.db?size=100&cleanup_frequency=0.4`), see also [the cluster mode](cluster.md) | `bolt://mercure.db` |
| `dispatch_timeout <duration>` | maximum duration of the dispatch of a single update, set to `0s` disable | `5s` |
| `write_timeout <duration>` | maximum duration before closing the connection, set to `0s` disable | `600s` |
| `ui` | enable the UI and expose demo endpoints | |
| `demo` | enable the UI but do not expose demo endpoints | |
| `cache <num-counters> <max-cost>` | cache configuration (see [Ristretto's docs](https://github.com/dgraph-io/ristretto)), set to -1 to disable the cache | `6e7` `1e8` (100MB) |
| Directive | Description | Default |
|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------|
| `publisher_jwt <key> [<algorithm>]` | the JWT key and algorithm to use for publishers, can contain [placeholders](https://caddyserver.com/docs/conventions#placeholders) | |
| `subscriber_jwt <key> [<algorithm>]` | the JWT key and algorithm to use for subscribers, can contain [placeholders](https://caddyserver.com/docs/conventions#placeholders) | |
| `anonymous` | allow subscribers with no valid JWT to connect | `false` |
| `publish_origins <origins...>` | a list of origins allowed publishing, can be `*` for all (only applicable when using cookie-based auth) | |
| `cors_origins <origin...>` | a list of allowed CORS origins, ([troubleshoot CORS issues](troubleshooting.md#cors-issues)) | |
| `cookie_name <name>` | the name of the cookie to use for the authorization mechanism | `mercureAuthorization` |
| `subscriptions` | expose the subscription web API and dispatch private updates when a subscription between the Hub and a subscriber is established or closed. The topic follows the template `/.well-known/mercure/subscriptions/{topicSelector}/{subscriberID}` | |
| `heartbeat` | interval between heartbeats (useful with some proxies, and old browsers), set to `0s` disable | `40s` |
| `transport_url <url>` | URL representation of the transport to use. Use `local://local` to disabled history, (example `bolt:///var/run/mercure.db?size=100&cleanup_frequency=0.4`), see also [the cluster mode](cluster.md) | `bolt://mercure.db` |
| `dispatch_timeout <duration>` | maximum duration of the dispatch of a single update, set to `0s` disable | `5s` |
| `write_timeout <duration>` | maximum duration before closing the connection, set to `0s` disable | `600s` |
| `ui` | enable the UI and expose demo endpoints | |
| `demo` | enable the UI but do not expose demo endpoints | |
| `cache <num-counters> <max-cost>` | cache configuration (see [Ristretto's docs](https://github.com/dgraph-io/ristretto)), set to -1 to disable the cache | `6e7` `1e8` (100MB) |

See also [the list of built-in Caddyfile directives](https://caddyserver.com/docs/caddyfile/directives).

Expand Down
2 changes: 1 addition & 1 deletion docs/hub/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 401 Unauthorized

* Double-check that the request to the hub includes a `mercureAuthorization` cookie or an `Authorization` HTTP header
* Double-check that the request to the hub includes an authorization cookie (the default name is `mercureAuthorization`) or an `Authorization` HTTP header
* If the cookie isn't set, you may have to explicitly include [the request credentials](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) (`new EventSource(url, {withCredentials: true})` and `fetch(url, {credentials: 'include'})`)
* Check the logs written by the hub on `stderr`, they contain the exact reason why the token has been rejected
* Be sure to set a **secret key** (and not a JWT) in `JWT_KEY` (or in `SUBSCRIBER_JWT_KEY` and `PUBLISHER_JWT_KEY`)
Expand Down
4 changes: 2 additions & 2 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (h *Hub) initHandler() {

csp := "default-src 'self'"
if h.demo {
router.PathPrefix(defaultDemoURL).HandlerFunc(Demo).Methods("GET", "HEAD")
router.PathPrefix(defaultDemoURL).HandlerFunc(h.Demo).Methods("GET", "HEAD")
}
if h.ui {
public, err := fs.Sub(uiContent, "public")
Expand Down Expand Up @@ -201,7 +201,7 @@ func (h *Hub) chainHandlers() http.Handler { //nolint:funlen

csp := "default-src 'self'"
if h.demo {
r.PathPrefix("/demo").HandlerFunc(Demo).Methods("GET", "HEAD")
r.PathPrefix("/demo").HandlerFunc(h.Demo).Methods("GET", "HEAD")
}

if h.ui {
Expand Down
Loading

0 comments on commit 99bf84c

Please sign in to comment.