Goa websocket with subprotocols (auth)
Goa websocket with subprotocols (auth)
How I figured out to pass an API key to my websocket endpoint using goa and
React. It wasn’t straight forward!
Firstly, you have to setup an websocket.Upgrader{} and assign CheckOrigin
and Subprotocols values.
upgrader := &websocket.Upgrader{}
upgrader.CheckOrigin = func(r *http.Request) bool { return true } // setup authorised origins; this is a demo
// You have to tell it that Sec-Websocket-Protocol and your custom auth header are valid subprotocols
// or it won't work! and you will get an error like this:
// Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received
upgrader.Subprotocols = []string{"Sec-Websocket-Protocol", "x-api-key"}
Then you have to add some headers to your request. But before we do that, in Goa
we need a custom middleware to read the subprotocol. Using
Authorization: Bearer asdadad, or in my case x-api-key: 123 like a
traditional HTTP request doesn’t work.
# authorization (subprotocol) header on a websocket 101 Switching
Sec-WebSocket-Protocol: x-api-key, key_000000000000
Because its x-api-key, key_000000000000, you have to split the string and do
some manipulation. Things you don’t have to do in normal HTTP requests.
Here’s how I do it using a Goa middleware
// set custom context keys
type xApiKeyWSType string
const apiKeyCtxKey xApiKeyWSType = "x-api-key"
func WebsocketConnection() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req := r
// if not a websocket connection, continue
if upgrade := r.Header.Get("Connection"); upgrade != "Upgrade" {
// Not a websocket connection
h.ServeHTTP(w, req)
}
// Get the header with the key
if auth := r.Header.Get("Sec-WebSocket-Protocol"); auth != "" {
// the header will return a string: x-api-key, key_000000000000
// we need to split, then trim white space
header := strings.Split(auth, ",")
header[1] = strings.TrimSpace(header[1])
ctx := context.WithValue(r.Context(), apiKeyCtxKey, header[1])
req = r.WithContext(ctx)
}
h.ServeHTTP(w, req)
})
}
}
Then in your security scheme, you’ll need to get that context key we just set.
func (a *ApiKey) Validate(
ctx context.Context,
key string,
scheme *security.APIKeyScheme,
db *repository.Queries,
) (context.Context, error) {
if key == "" {
key = GetXApiKeyWS(ctx)
}
// truncated but do something with the key.
// a typical HTTP request will have the key available already but
// we need to grab it via the GetXApiKeyWS function
return ctx, nil
}
It took me a few hours of reading issues, code, websocket specs to figure out
why wscat and insomnia could connect but not React. Inspecting the headers
was a big clue - no response headers were set from the server. But, if I
connected to a globally available unauthenticated server such as
wss://echo.websocket.events they were.
It’s all working now and apart from this I’m really happy with how easy it is to
get going with Goa and websockets. From a design.go/DSL prespective its
simple. I just replaced Result with StreamingResult in my Services'
Method. Had to plumb in the upgrader but it was simple. The hard part was
making sure access was still secure behind my API keys (and soon to be JWTs).
Tags:
#goa #websockets #react #go