Skip to content

Commit

Permalink
console: Add protocol authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
adriansmares committed Nov 1, 2023
1 parent 6f944b4 commit 2f72b77
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 0 deletions.
8 changes: 8 additions & 0 deletions pkg/console/internal/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
"go.thethings.network/lorawan-stack/v3/pkg/config"
"go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/eventsmux"
"go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/middleware"
"go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/subscriptions"
"go.thethings.network/lorawan-stack/v3/pkg/events"
"go.thethings.network/lorawan-stack/v3/pkg/log"
Expand All @@ -36,6 +37,11 @@ import (
"nhooyr.io/websocket"
)

const (
authorizationProtocolPrefix = "ttn.lorawan.v3.header.authorization.bearer."
protocolV1 = "ttn.lorawan.v3.console.internal.events.v1"
)

// Component is the interface of the component to the events API handler.
type Component interface {
task.Starter
Expand All @@ -57,6 +63,7 @@ func (h *eventsHandler) RegisterRoutes(server *web.Server) {
router.Use(
mux.MiddlewareFunc(webmiddleware.Namespace("console/internal/events")),
ratelimit.HTTPMiddleware(h.component.RateLimiter(), "http:console:internal:events"),
mux.MiddlewareFunc(middleware.ProtocolAuthentication(authorizationProtocolPrefix)),
mux.MiddlewareFunc(webmiddleware.Metadata("Authorization")),
)
router.Path("/").HandlerFunc(h.handleEvents).Methods(http.MethodGet)
Expand All @@ -72,6 +79,7 @@ func (h *eventsHandler) handleEvents(w http.ResponseWriter, r *http.Request) {
}

conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{protocolV1},
InsecureSkipVerify: true, // CORS is not enabled for APIs.
CompressionMode: websocket.CompressionContextTakeover,
})
Expand Down
78 changes: 78 additions & 0 deletions pkg/console/internal/events/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package middleware

import (
"fmt"
"net/http"
"net/textproto"
"strings"

"go.thethings.network/lorawan-stack/v3/pkg/auth"
)

var (
protocolHeader = textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Protocol")
authorizationHeader = textproto.CanonicalMIMEHeaderKey("Authorization")
connectionHeader = textproto.CanonicalMIMEHeaderKey("Connection")
upgradeHeader = textproto.CanonicalMIMEHeaderKey("Upgrade")
)

func isWebSocketRequest(r *http.Request) bool {
h := r.Header
return strings.EqualFold(h.Get(connectionHeader), "upgrade") &&
strings.EqualFold(h.Get(upgradeHeader), "websocket")
}

// ProtocolAuthentication returns a middleware that authenticates WebSocket requests using the subprotocol.
// The subprotocol must be prefixed with the given prefix.
// The token is extracted from the subprotocol and used to authenticate the request.
// If the token is valid, the subprotocol is removed from the request.
// If the token is invalid, the request is not authenticated.
func ProtocolAuthentication(prefix string) func(http.Handler) http.Handler {
prefixLen := len(prefix)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isWebSocketRequest(r) {
next.ServeHTTP(w, r)
return
}
if r.Header.Get(authorizationHeader) != "" {
next.ServeHTTP(w, r)
return
}
protocols := strings.Split(strings.TrimSpace(r.Header.Get(protocolHeader)), ",")
newProtocols := make([]string, 0, len(protocols))
token := ""
for _, protocol := range protocols {
p := strings.TrimSpace(protocol)
if len(p) >= prefixLen && strings.EqualFold(prefix, p[:prefixLen]) {
token = p[prefixLen:]
continue
}
newProtocols = append(newProtocols, p)
}
if _, _, _, err := auth.SplitToken(token); err == nil {
if len(newProtocols) > 0 {
r.Header.Set(protocolHeader, strings.Join(newProtocols, ","))
} else {
r.Header.Del(protocolHeader)
}
r.Header.Set(authorizationHeader, fmt.Sprintf("Bearer %s", token))
}
next.ServeHTTP(w, r)
})
}
}
16 changes: 16 additions & 0 deletions pkg/console/internal/events/middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package middleware implements the events middleware.
package middleware
5 changes: 5 additions & 0 deletions pkg/console/internal/events/protocol/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ The authentication for the internal API is similar to other APIs available in Th

Upon connecting, no authorization will take place - the endpoint only will check that the provided token is valid (i.e. exists and it is not expired).

The [standard WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) [does not support custom request headers](https://github.com/whatwg/websockets/issues/16). As a result of this limitation, the backend allows the Console to provide the token as a [protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#parameters). Specifically, given a `Bearer` token `t`, the following protocols should be provided to the `WebSocket` constructor:

- `ttn.lorawan.v3.console.internal.events.v1`
- `ttn.lorawan.v3.header.authorization.bearer.t`

### Message Format

Both requests and responses sent over the WebSocket connection are JSON encoded. All messages are JSON objects and are required to contain at least the following two fields:
Expand Down

0 comments on commit 2f72b77

Please sign in to comment.