2024-01-12 19:19:52 +01:00
|
|
|
package pairing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/sirupsen/logrus"
|
2024-04-05 11:31:40 +02:00
|
|
|
|
2024-01-12 19:19:52 +01:00
|
|
|
"github.com/twofas/2fas-server/internal/common/logging"
|
2024-04-05 11:31:40 +02:00
|
|
|
"github.com/twofas/2fas-server/internal/pass/connection"
|
2024-01-24 20:57:31 +01:00
|
|
|
"github.com/twofas/2fas-server/internal/pass/sign"
|
2024-01-12 19:19:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type Pairing struct {
|
2024-04-05 11:31:40 +02:00
|
|
|
store store
|
|
|
|
signSvc *sign.Service
|
|
|
|
pairingRequestTokenValidityDuration time.Duration
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type store interface {
|
|
|
|
AddExtension(ctx context.Context, extensionID string)
|
|
|
|
ExtensionExists(ctx context.Context, extensionID string) bool
|
|
|
|
GetPairingInfo(ctx context.Context, extensionID string) (PairingInfo, error)
|
|
|
|
SetPairingInfo(ctx context.Context, extensionID string, pi PairingInfo) error
|
|
|
|
}
|
|
|
|
|
2024-04-05 11:31:40 +02:00
|
|
|
func NewApp(signService *sign.Service, pairingRequestTokenValidityDuration time.Duration) *Pairing {
|
2024-01-12 19:19:52 +01:00
|
|
|
return &Pairing{
|
2024-04-05 11:31:40 +02:00
|
|
|
store: NewMemoryStore(),
|
|
|
|
signSvc: signService,
|
|
|
|
pairingRequestTokenValidityDuration: pairingRequestTokenValidityDuration,
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
const (
|
|
|
|
pairingTokenValidityDuration = 3 * time.Minute
|
|
|
|
)
|
|
|
|
|
2024-01-12 19:19:52 +01:00
|
|
|
type ConfigureBrowserExtensionRequest struct {
|
|
|
|
ExtensionID string `json:"extension_id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfigureBrowserExtensionResponse struct {
|
|
|
|
BrowserExtensionPairingToken string `json:"browser_extension_pairing_token"`
|
|
|
|
ConnectionToken string `json:"connection_token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) ConfigureBrowserExtension(ctx context.Context, req ConfigureBrowserExtensionRequest) (ConfigureBrowserExtensionResponse, error) {
|
|
|
|
p.store.AddExtension(ctx, req.ExtensionID)
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
pairingToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: req.ExtensionID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeBrowserExtensionWait,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ConfigureBrowserExtensionResponse{}, fmt.Errorf("failed to generate pairing token: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
mobileToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: req.ExtensionID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeMobileConfirm,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ConfigureBrowserExtensionResponse{}, fmt.Errorf("Failed to generate mobile confirm token: %v", err)
|
|
|
|
}
|
2024-01-12 19:19:52 +01:00
|
|
|
return ConfigureBrowserExtensionResponse{
|
2024-01-24 20:57:31 +01:00
|
|
|
ConnectionToken: mobileToken,
|
2024-01-12 19:19:52 +01:00
|
|
|
BrowserExtensionPairingToken: pairingToken,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type ExtensionWaitForConnectionInput struct {
|
|
|
|
ResponseWriter http.ResponseWriter
|
|
|
|
HttpReq *http.Request
|
|
|
|
}
|
|
|
|
|
|
|
|
type WaitForConnectionResponse struct {
|
|
|
|
BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"`
|
2024-04-05 11:31:40 +02:00
|
|
|
BrowserExtensionSyncToken string `json:"browser_extension_sync_token"`
|
2024-01-12 19:19:52 +01:00
|
|
|
Status string `json:"status"`
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
}
|
|
|
|
|
2024-04-05 11:31:40 +02:00
|
|
|
func (p *Pairing) ServePairingWS(w http.ResponseWriter, r *http.Request, extID string) error {
|
2024-01-12 19:19:52 +01:00
|
|
|
log := logging.WithField("extension_id", extID)
|
2024-04-05 11:31:40 +02:00
|
|
|
conn, err := connection.Upgrade(w, r)
|
2024-01-12 19:19:52 +01:00
|
|
|
if err != nil {
|
2024-04-05 11:31:40 +02:00
|
|
|
return fmt.Errorf("failed to upgrade connection: %w", err)
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("Starting pairing WS")
|
|
|
|
|
2024-04-05 11:31:40 +02:00
|
|
|
if pairing, pairingDone := p.isExtensionPaired(r.Context(), extID, log); pairingDone {
|
|
|
|
if err := p.sendTokenAndCloseConn(extID, pairing, conn); err != nil {
|
2024-01-12 19:19:52 +01:00
|
|
|
log.Errorf("Failed to send token: %v", err)
|
|
|
|
}
|
2024-04-05 11:31:40 +02:00
|
|
|
return nil
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxWaitTime = 3 * time.Minute
|
|
|
|
checkIfConnectedInterval = time.Second
|
|
|
|
)
|
|
|
|
maxWaitC := time.After(maxWaitTime)
|
|
|
|
// TODO: consider returning event from store on change.
|
|
|
|
connectedCheckTicker := time.NewTicker(checkIfConnectedInterval)
|
|
|
|
defer connectedCheckTicker.Stop()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-maxWaitC:
|
|
|
|
log.Info("Closing paring ws after timeout")
|
2024-04-05 11:31:40 +02:00
|
|
|
return nil
|
2024-01-12 19:19:52 +01:00
|
|
|
case <-connectedCheckTicker.C:
|
2024-04-05 11:31:40 +02:00
|
|
|
if pairing, pairingDone := p.isExtensionPaired(r.Context(), extID, log); pairingDone {
|
|
|
|
if err := p.sendTokenAndCloseConn(extID, pairing, conn); err != nil {
|
2024-01-12 19:19:52 +01:00
|
|
|
log.Errorf("Failed to send token: %v", err)
|
2024-04-05 11:31:40 +02:00
|
|
|
return nil
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
2024-04-05 11:31:40 +02:00
|
|
|
log.WithField("device_id", pairing.Device.DeviceID).Infof("Paring ws finished")
|
|
|
|
return nil
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-05 11:31:40 +02:00
|
|
|
func (p *Pairing) isExtensionPaired(ctx context.Context, extID string, log *logrus.Entry) (PairingInfo, bool) {
|
2024-01-12 19:19:52 +01:00
|
|
|
pairingInfo, err := p.store.GetPairingInfo(ctx, extID)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn("Failed to get pairing info")
|
2024-04-05 11:31:40 +02:00
|
|
|
return PairingInfo{}, false
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
2024-04-05 11:31:40 +02:00
|
|
|
return pairingInfo, pairingInfo.IsPaired()
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
|
2024-04-05 11:31:40 +02:00
|
|
|
func (p *Pairing) sendTokenAndCloseConn(extID string, pairingInfo PairingInfo, conn *websocket.Conn) error {
|
2024-01-24 20:57:31 +01:00
|
|
|
extProxyToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: extID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeBrowserExtensionProxy,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2024-04-05 11:31:40 +02:00
|
|
|
return fmt.Errorf("failed to generate ext proxy token: %v", err)
|
|
|
|
}
|
|
|
|
var syncToken string
|
|
|
|
if pairingInfo.Device.FCMToken != "" {
|
|
|
|
syncToken, err = p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: pairingInfo.Device.FCMToken,
|
|
|
|
ExpiresAt: time.Now().Add(p.pairingRequestTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeBrowserExtensionSyncRequest,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to generate proxy sync request token: %v", err)
|
|
|
|
}
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
}
|
|
|
|
|
2024-01-12 19:19:52 +01:00
|
|
|
if err := conn.WriteJSON(WaitForConnectionResponse{
|
2024-01-24 20:57:31 +01:00
|
|
|
BrowserExtensionProxyToken: extProxyToken,
|
2024-04-05 11:31:40 +02:00
|
|
|
BrowserExtensionSyncToken: syncToken,
|
2024-01-12 19:19:52 +01:00
|
|
|
Status: "ok",
|
2024-04-05 11:31:40 +02:00
|
|
|
DeviceID: pairingInfo.Device.DeviceID,
|
2024-01-12 19:19:52 +01:00
|
|
|
}); err != nil {
|
|
|
|
return fmt.Errorf("failed to write to extension: %v", err)
|
|
|
|
}
|
|
|
|
return conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPairingInfo returns paired device and information if pairing was done.
|
|
|
|
func (p *Pairing) GetPairingInfo(ctx context.Context, extensionID string) (PairingInfo, error) {
|
|
|
|
return p.store.GetPairingInfo(ctx, extensionID)
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfirmPairingRequest struct {
|
|
|
|
FCMToken string `json:"fcm_token"`
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
}
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
type ConfirmPairingResponse struct {
|
|
|
|
ProxyToken string `json:"proxy_token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) ConfirmPairing(ctx context.Context, req ConfirmPairingRequest, extensionID string) (ConfirmPairingResponse, error) {
|
|
|
|
mobileProxyToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: extensionID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeMobileProxy,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ConfirmPairingResponse{}, fmt.Errorf("Failed to generate ext proxy token: %v", err)
|
|
|
|
}
|
|
|
|
if err := p.store.SetPairingInfo(ctx, extensionID, PairingInfo{
|
2024-01-12 19:19:52 +01:00
|
|
|
Device: MobileDevice{
|
|
|
|
DeviceID: req.DeviceID,
|
|
|
|
FCMToken: req.FCMToken,
|
|
|
|
},
|
|
|
|
PairedAt: time.Now().UTC(),
|
2024-01-24 20:57:31 +01:00
|
|
|
}); err != nil {
|
|
|
|
return ConfirmPairingResponse{}, err
|
|
|
|
}
|
2024-04-05 11:31:40 +02:00
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
return ConfirmPairingResponse{ProxyToken: mobileProxyToken}, nil
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|