Skip to content

Commit f8b6424

Browse files
committed
Update documentation
1 parent 5d958d2 commit f8b6424

2 files changed

Lines changed: 237 additions & 12 deletions

File tree

README.md

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and authorizing requests with API key, IAM, Lambda authorizer, Cognito User Pool
1919
- [API key](#api-key)
2020
- [IAM](#iam)
2121
- [Token-based authorization](#token-based-authorization)
22+
- [Authorizers](#authorizers)
2223
- [Publishing events](#publishing-events)
2324
- [Subscribing to events](#subscribing-to-events)
2425
- [Closing resources](#closing-resources)
@@ -109,7 +110,7 @@ func publish(ctx context.Context) error {
109110
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
110111
Endpoint: wsEndpoint,
111112
Subprotocols: []string{appsync.ProtocolEvents},
112-
Authorizer: authz,
113+
Authorizers: appsync.Authorizers{Default: authz},
113114
})
114115
if err != nil {
115116
return err
@@ -197,6 +198,75 @@ authz, err := authorizer.Token(authorizer.TokenAuthorizerConfig{
197198
})
198199
```
199200

201+
## Authorizers
202+
203+
`ConnectionOptions.Authorizers` controls which authorizer is used for each operation type.
204+
205+
```go
206+
type Authorizers struct {
207+
Default authorizer.Authorizer
208+
Connect authorizer.Authorizer
209+
Publish authorizer.Authorizer
210+
Subscribe authorizer.Authorizer
211+
}
212+
```
213+
214+
Fallback resolution: if a specific field (`Connect`, `Publish`, or `Subscribe`) is `nil`, `Default` is used.
215+
216+
**Single authorizer for all operations (most common):**
217+
218+
```go
219+
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
220+
Endpoint: wsEndpoint,
221+
Subprotocols: []string{appsync.ProtocolEvents},
222+
Authorizers: appsync.Authorizers{Default: authz},
223+
})
224+
```
225+
226+
**Different authorizers per operation:**
227+
228+
```go
229+
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
230+
Endpoint: wsEndpoint,
231+
Subprotocols: []string{appsync.ProtocolEvents},
232+
Authorizers: appsync.Authorizers{
233+
Connect: connectAuthz,
234+
Publish: publishAuthz,
235+
Subscribe: subscribeAuthz,
236+
},
237+
})
238+
```
239+
240+
**Subscribe-only or publish-only clients:**
241+
242+
Authorizers for `Publish` and `Subscribe` are resolved at the point of use. If an operation is never called, its authorizer never needs to be configured. This makes it possible to follow the principle of least privilege by omitting `Default` and only configuring the authorizer for the operations the client actually uses.
243+
244+
Subscribe-only client — no publish authorizer configured:
245+
246+
```go
247+
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
248+
Endpoint: wsEndpoint,
249+
Subprotocols: []string{appsync.ProtocolEvents},
250+
Authorizers: appsync.Authorizers{
251+
Connect: connectAuthz,
252+
Subscribe: subscribeAuthz,
253+
},
254+
})
255+
```
256+
257+
Publish-only client — no subscribe authorizer configured:
258+
259+
```go
260+
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
261+
Endpoint: wsEndpoint,
262+
Subprotocols: []string{appsync.ProtocolEvents},
263+
Authorizers: appsync.Authorizers{
264+
Connect: connectAuthz,
265+
Publish: publishAuthz,
266+
},
267+
})
268+
```
269+
200270
## Publishing events
201271

202272
`Publish` sends a payload to a channel.
@@ -216,6 +286,16 @@ if err != nil {
216286
`Payload` is a raw byte slice. The library does not require a Go struct, but AppSync event payloads are commonly JSON.
217287
If you want structured data, marshal it before publishing.
218288

289+
The optional `Authorizer` field overrides `Authorizers.Publish` for a single call:
290+
291+
```go
292+
err := client.Publish(ctx, appsync.PublishCommandInput{
293+
Channel: "default/notifications",
294+
Payload: payload,
295+
Authorizer: perRequestAuthz,
296+
})
297+
```
298+
219299
## Subscribing to events
220300

221301
Use `Subscribe` to create a channel subscription.
@@ -230,6 +310,15 @@ if err != nil {
230310
defer sub.Close(context.Background())
231311
```
232312

313+
The optional `Authorizer` field overrides `Authorizers.Subscribe` for a single call:
314+
315+
```go
316+
sub, err := client.Subscribe(ctx, appsync.SubscribeCommandInput{
317+
Channel: "default/notifications",
318+
Authorizer: perRequestAuthz,
319+
})
320+
```
321+
233322
Read event messages with `Next`:
234323

235324
```go
@@ -371,7 +460,7 @@ if err != nil {
371460
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
372461
Endpoint: wsEndpoint,
373462
Subprotocols: []string{appsync.ProtocolEvents},
374-
Authorizer: authz,
463+
Authorizers: appsync.Authorizers{Default: authz},
375464
})
376465
```
377466

@@ -408,7 +497,7 @@ For connection and unsubscribe calls, `input.Channel` is empty and `input.Payloa
408497
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
409498
Endpoint: wsEndpoint,
410499
Subprotocols: []string{appsync.ProtocolEvents},
411-
Authorizer: authz,
500+
Authorizers: appsync.Authorizers{Default: authz},
412501
Backpressure: appsync.Backpressure{
413502
ConnectionInbound: 100,
414503
ConnectionOutbound: 100,
@@ -481,14 +570,7 @@ Runnable examples are available in:
481570
- [`examples/iam`](examples/iam)
482571
- [`examples/token`](examples/token)
483572
- [`examples/custom-authorizer`](examples/custom-authorizer)
484-
485-
## Limitations
486-
487-
A `Client` uses one authorizer for the entire connection lifecycle.
488-
489-
The same authorizer is used to establish the WebSocket connection and to authorize
490-
`subscribe`, `publish`, and `unsubscribe` messages. Using different authorizers for
491-
connection setup and individual operation messages is not currently supported.
573+
- [`examples/multi-authorizer`](examples/multi-authorizer)
492574

493575
## Tips
494576

@@ -525,7 +607,6 @@ in the future for some reason.
525607

526608
Missing features:
527609

528-
- authorizer per request
529610
- HTTP Publish
530611
- Batch Publish
531612
- something else I missed probably

examples/multi-authorizer/main.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"github.com/exanubes/appsync"
12+
"github.com/exanubes/appsync/authorizer"
13+
)
14+
15+
func requiredEnv(name string) string {
16+
value := os.Getenv(name)
17+
if value == "" {
18+
log.Fatalf("%s is required", name)
19+
}
20+
return value
21+
}
22+
23+
// This example demonstrates using different authorizers for different operations.
24+
//
25+
// Scenario: a channel that accepts API key authorization for subscribing but requires
26+
// a Lambda token to publish. A third per-request token is used for a single publish call.
27+
//
28+
// Environment variables:
29+
// APPSYNC_HTTP_ENDPOINT - AppSync HTTP endpoint
30+
// APPSYNC_WS_ENDPOINT - AppSync WebSocket realtime endpoint
31+
// APPSYNC_API_KEY - API key (used for subscribe)
32+
// APPSYNC_PUBLISH_TOKEN - Lambda/Cognito token (used for publish)
33+
// APPSYNC_OVERRIDE_TOKEN - Token used to override authorizer on a single publish
34+
// APPSYNC_CHANNEL - Channel to subscribe and publish to
35+
36+
func main() {
37+
var (
38+
httpEndpoint = requiredEnv("APPSYNC_HTTP_ENDPOINT")
39+
wsEndpoint = requiredEnv("APPSYNC_WS_ENDPOINT")
40+
apiKey = requiredEnv("APPSYNC_API_KEY")
41+
publishToken = requiredEnv("APPSYNC_PUBLISH_TOKEN")
42+
overrideToken = requiredEnv("APPSYNC_OVERRIDE_TOKEN")
43+
channel = requiredEnv("APPSYNC_CHANNEL")
44+
)
45+
46+
ctx := context.Background()
47+
48+
// connectAuthz and subscribeAuthz use API key authorization.
49+
connectAuthz, err := authorizer.ApiKey(authorizer.ApiKeyAuthorizerConfig{
50+
ApiKey: apiKey,
51+
Endpoint: httpEndpoint,
52+
})
53+
if err != nil {
54+
log.Fatal(err)
55+
}
56+
57+
// publishAuthz uses a token (e.g. Lambda authorizer) for publish operations.
58+
publishAuthz, err := authorizer.Token(authorizer.TokenAuthorizerConfig{
59+
AuthToken: publishToken,
60+
Endpoint: httpEndpoint,
61+
})
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
66+
// overrideAuthz is a one-off authorizer passed directly to a single Publish call.
67+
overrideAuthz, err := authorizer.Token(authorizer.TokenAuthorizerConfig{
68+
AuthToken: overrideToken,
69+
Endpoint: httpEndpoint,
70+
})
71+
if err != nil {
72+
log.Fatal(err)
73+
}
74+
75+
client, err := appsync.Connect(ctx, appsync.ConnectionOptions{
76+
Endpoint: wsEndpoint,
77+
Subprotocols: []string{appsync.ProtocolEvents},
78+
Authorizers: appsync.Authorizers{
79+
Connect: connectAuthz,
80+
Subscribe: connectAuthz,
81+
Publish: publishAuthz,
82+
},
83+
})
84+
if err != nil {
85+
log.Fatal(err)
86+
}
87+
defer client.Close(ctx)
88+
89+
sub, err := client.Subscribe(ctx, appsync.SubscribeCommandInput{
90+
Channel: channel,
91+
})
92+
if err != nil {
93+
log.Fatal(err)
94+
}
95+
defer sub.Close(ctx)
96+
97+
type event struct {
98+
Message string `json:"message"`
99+
}
100+
101+
// Regular publish — uses Authorizers.Publish (publishAuthz).
102+
payload, err := json.Marshal(event{Message: "hello from publish authorizer"})
103+
if err != nil {
104+
log.Fatal(err)
105+
}
106+
107+
if err = client.Publish(ctx, appsync.PublishCommandInput{
108+
Channel: channel,
109+
Payload: payload,
110+
}); err != nil {
111+
log.Fatal(err)
112+
}
113+
114+
var received event
115+
if err = sub.DecodeNext(ctx, &received); err != nil {
116+
if errors.Is(err, appsync.ErrSubscriptionClosed) {
117+
return
118+
}
119+
log.Fatal(err)
120+
}
121+
fmt.Println("received (publish authorizer):", received.Message)
122+
123+
// Per-request override — uses overrideAuthz for this single call only.
124+
overridePayload, err := json.Marshal(event{Message: "hello from override authorizer"})
125+
if err != nil {
126+
log.Fatal(err)
127+
}
128+
129+
if err = client.Publish(ctx, appsync.PublishCommandInput{
130+
Channel: channel,
131+
Payload: overridePayload,
132+
Authorizer: overrideAuthz,
133+
}); err != nil {
134+
log.Fatal(err)
135+
}
136+
137+
if err = sub.DecodeNext(ctx, &received); err != nil {
138+
if errors.Is(err, appsync.ErrSubscriptionClosed) {
139+
return
140+
}
141+
log.Fatal(err)
142+
}
143+
fmt.Println("received (override authorizer):", received.Message)
144+
}

0 commit comments

Comments
 (0)