Require auth for fabric session websocket
This commit is contained in:
@@ -198,6 +198,8 @@ type FabricSessionEventLogEntry struct {
|
||||
Event string `json:"event"`
|
||||
ClusterID string `json:"cluster_id,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
AcceptedBy string `json:"accepted_by,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
SessionEvent fabricproto.SessionEventType `json:"session_event,omitempty"`
|
||||
StreamID uint64 `json:"stream_id,omitempty"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
@@ -207,11 +209,31 @@ type FabricSessionEventLogEntry struct {
|
||||
ObservedAt time.Time `json:"observed_at"`
|
||||
}
|
||||
|
||||
type fabricSessionAuthorityPayload struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
SourceNodeID string `json:"source_node_id,omitempty"`
|
||||
SelectedEntryNodeID string `json:"selected_entry_node_id,omitempty"`
|
||||
TokenHash string `json:"token_hash"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type fabricSessionAuthDecision struct {
|
||||
AcceptedBy string
|
||||
SessionID string
|
||||
}
|
||||
|
||||
func (s Server) handleFabricSessionWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
decision, ok := s.validateFabricSessionRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(_ *http.Request) bool { return true },
|
||||
}
|
||||
@@ -225,6 +247,8 @@ func (s Server) handleFabricSessionWebSocket(w http.ResponseWriter, r *http.Requ
|
||||
Event: "fabric_session_websocket_opened",
|
||||
ClusterID: s.Local.ClusterID,
|
||||
NodeID: s.Local.NodeID,
|
||||
AcceptedBy: decision.AcceptedBy,
|
||||
SessionID: decision.SessionID,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
ObservedAt: time.Now().UTC(),
|
||||
})
|
||||
@@ -235,6 +259,8 @@ func (s Server) handleFabricSessionWebSocket(w http.ResponseWriter, r *http.Requ
|
||||
Event: "fabric_session_event",
|
||||
ClusterID: s.Local.ClusterID,
|
||||
NodeID: s.Local.NodeID,
|
||||
AcceptedBy: decision.AcceptedBy,
|
||||
SessionID: decision.SessionID,
|
||||
SessionEvent: event.Type,
|
||||
StreamID: event.StreamID,
|
||||
Sequence: event.Sequence,
|
||||
@@ -251,6 +277,8 @@ func (s Server) handleFabricSessionWebSocket(w http.ResponseWriter, r *http.Requ
|
||||
Event: "fabric_session_websocket_closed",
|
||||
ClusterID: s.Local.ClusterID,
|
||||
NodeID: s.Local.NodeID,
|
||||
AcceptedBy: decision.AcceptedBy,
|
||||
SessionID: decision.SessionID,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Reason: err.Error(),
|
||||
ObservedAt: time.Now().UTC(),
|
||||
@@ -261,11 +289,83 @@ func (s Server) handleFabricSessionWebSocket(w http.ResponseWriter, r *http.Requ
|
||||
Event: "fabric_session_websocket_closed",
|
||||
ClusterID: s.Local.ClusterID,
|
||||
NodeID: s.Local.NodeID,
|
||||
AcceptedBy: decision.AcceptedBy,
|
||||
SessionID: decision.SessionID,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
ObservedAt: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s Server) validateFabricSessionRequest(w http.ResponseWriter, r *http.Request) (fabricSessionAuthDecision, bool) {
|
||||
var decision fabricSessionAuthDecision
|
||||
token := fabricSessionBearerToken(r)
|
||||
if !strings.HasPrefix(token, "rap_fsn_") {
|
||||
http.Error(w, "fabric session token is required", http.StatusUnauthorized)
|
||||
return decision, false
|
||||
}
|
||||
payload, err := s.verifyFabricSessionAuthority(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return decision, false
|
||||
}
|
||||
decision.AcceptedBy = "legacy_unsigned"
|
||||
if payload != nil {
|
||||
decision.AcceptedBy = "signed"
|
||||
decision.SessionID = strings.TrimSpace(payload.SessionID)
|
||||
}
|
||||
return decision, true
|
||||
}
|
||||
|
||||
func (s Server) verifyFabricSessionAuthority(r *http.Request, token string) (*fabricSessionAuthorityPayload, error) {
|
||||
publicKey := strings.TrimSpace(s.ClusterAuthorityPublicKey)
|
||||
payloadHeader := strings.TrimSpace(r.Header.Get("X-RAP-Fabric-Session-Authority-Payload"))
|
||||
signatureHeader := strings.TrimSpace(r.Header.Get("X-RAP-Fabric-Session-Authority-Signature"))
|
||||
if payloadHeader == "" && signatureHeader == "" {
|
||||
if publicKey != "" {
|
||||
return nil, fmt.Errorf("%w: signed fabric session authority is required", ErrUnauthorizedChannel)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if publicKey == "" {
|
||||
return nil, ErrUnauthorizedChannel
|
||||
}
|
||||
if payloadHeader == "" || signatureHeader == "" {
|
||||
return nil, fmt.Errorf("%w: fabric session authority payload and signature are required together", ErrUnauthorizedChannel)
|
||||
}
|
||||
payloadRaw, err := decodeHeaderJSON(payloadHeader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid fabric session authority payload", ErrUnauthorizedChannel)
|
||||
}
|
||||
signatureRaw, err := decodeHeaderJSON(signatureHeader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid fabric session authority signature", ErrUnauthorizedChannel)
|
||||
}
|
||||
var signature authority.Signature
|
||||
if err := json.Unmarshal(signatureRaw, &signature); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid fabric session authority signature", ErrUnauthorizedChannel)
|
||||
}
|
||||
if err := authority.VerifyRaw(publicKey, payloadRaw, signature); err != nil {
|
||||
return nil, fmt.Errorf("%w: fabric session authority signature rejected", ErrUnauthorizedChannel)
|
||||
}
|
||||
var payload fabricSessionAuthorityPayload
|
||||
if err := json.Unmarshal(payloadRaw, &payload); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid fabric session authority payload", ErrUnauthorizedChannel)
|
||||
}
|
||||
if payload.SchemaVersion != "rap.fabric_session_authority.v1" ||
|
||||
payload.ClusterID != s.Local.ClusterID ||
|
||||
payload.TokenHash != fabricSessionTokenHash(token) ||
|
||||
strings.TrimSpace(payload.SessionID) == "" {
|
||||
return nil, fmt.Errorf("%w: fabric session authority payload mismatch", ErrUnauthorizedChannel)
|
||||
}
|
||||
if payload.SelectedEntryNodeID != "" && s.Local.NodeID != "" && payload.SelectedEntryNodeID != s.Local.NodeID {
|
||||
return nil, fmt.Errorf("%w: fabric session entry node mismatch", ErrUnauthorizedChannel)
|
||||
}
|
||||
if !payload.ExpiresAt.IsZero() && !payload.ExpiresAt.After(time.Now().UTC()) {
|
||||
return nil, fmt.Errorf("%w: fabric session lease expired", ErrUnauthorizedChannel)
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func (s Server) logFabricSession(entry FabricSessionEventLogEntry) {
|
||||
if s.FabricSessionLogger != nil {
|
||||
s.FabricSessionLogger(entry)
|
||||
@@ -1693,6 +1793,25 @@ func fabricServiceChannelBearerToken(r *http.Request) string {
|
||||
return strings.TrimSpace(r.URL.Query().Get("service_channel_token"))
|
||||
}
|
||||
|
||||
func fabricSessionTokenHash(token string) string {
|
||||
sum := sha256.Sum256([]byte(strings.TrimSpace(token)))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func fabricSessionBearerToken(r *http.Request) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
if token := strings.TrimSpace(r.Header.Get("X-RAP-Fabric-Session-Token")); token != "" {
|
||||
return token
|
||||
}
|
||||
auth := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
if len(auth) > len("Bearer ") && strings.EqualFold(auth[:len("Bearer ")], "Bearer ") {
|
||||
return strings.TrimSpace(auth[len("Bearer "):])
|
||||
}
|
||||
return strings.TrimSpace(r.URL.Query().Get("fabric_session_token"))
|
||||
}
|
||||
|
||||
func isAllowedFabricServiceVPNChannel(channel string) bool {
|
||||
return isAllowedFabricServiceChannelForClass(FabricServiceClassVPNPackets, channel)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user