From bc22800bf46838282a22fe18f3e0d36c34c32ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Dry=C5=9B?= Date: Tue, 7 May 2024 14:04:44 +0200 Subject: [PATCH] feat/rework pass sync (#47) chore: rework endpoints for push --- e2e-tests/pass/http.go | 40 ++++++++---- e2e-tests/pass/sync_test.go | 17 ++++-- e2e-tests/pass/ws.go | 2 +- internal/pass/server.go | 11 ++-- internal/pass/sign/lib.go | 1 + internal/pass/sync/auth.go | 9 +++ internal/pass/sync/handlers.go | 107 ++++++++++++++++++++++++--------- internal/pass/sync/sync.go | 70 ++++++++++----------- 8 files changed, 170 insertions(+), 87 deletions(-) diff --git a/e2e-tests/pass/http.go b/e2e-tests/pass/http.go index 3dd78e0..b18f3f0 100644 --- a/e2e-tests/pass/http.go +++ b/e2e-tests/pass/http.go @@ -101,17 +101,6 @@ func confirmSyncMobileRequest(connectionToken string) (string, error) { return resp.ProxyToken, nil } -func getMobileToken(fcm string) (string, error) { - var resp struct { - MobileSyncConfirmToken string `json:"mobile_sync_confirm_token"` - } - if err := request("GET", fmt.Sprintf("/mobile/sync/%s/token", fcm), "", nil, &resp); err != nil { - return "", fmt.Errorf("failed to get mobile token: %w", err) - } - - return resp.MobileSyncConfirmToken, nil -} - func request(method, path, auth string, req, resp interface{}) error { url := getApiURL() + path var body io.Reader @@ -151,3 +140,32 @@ func request(method, path, auth string, req, resp interface{}) error { return nil } + +type RequestSyncResponse struct { + BrowserExtensionWaitToken string `json:"browser_extension_wait_token"` + MobileConfirmToken string `json:"mobile_confirm_token"` +} + +func browserExtensionRequestSync(token string) (RequestSyncResponse, error) { + var resp RequestSyncResponse + + if err := request("POST", "/browser_extension/sync/request", token, nil, &resp); err != nil { + return resp, fmt.Errorf("failed to configure browser: %w", err) + } + + return resp, nil +} + +func browserExtensionPushToMobile(token string) error { + req := struct { + Body string `json:"push_body"` + }{ + Body: "sent from browser extension", + } + resp := struct{}{} + if err := request("POST", "/browser_extension/sync/push", token, req, &resp); err != nil { + return fmt.Errorf("failed to configure browser: %w", err) + } + + return nil +} diff --git a/e2e-tests/pass/sync_test.go b/e2e-tests/pass/sync_test.go index 9a4e34a..b224d92 100644 --- a/e2e-tests/pass/sync_test.go +++ b/e2e-tests/pass/sync_test.go @@ -14,6 +14,7 @@ func TestSyncHappyFlow(t *testing.T) { browserExtensionDone := make(chan struct{}) mobileParingDone := make(chan struct{}) + confirmMobileChannel := make(chan string) fcm := uuid.NewString() deviceID := getDeviceID() @@ -26,7 +27,15 @@ func TestSyncHappyFlow(t *testing.T) { return } - proxyToken, err := browserExtensionWaitForSyncConfirm(syncToken) + requestSyncResp, err := browserExtensionRequestSync(syncToken) + if err != nil { + t.Errorf("Error when Browser Extension requested sync confirm: %v", err) + return + } + + confirmMobileChannel <- requestSyncResp.MobileConfirmToken + + proxyToken, err := browserExtensionWaitForSyncConfirm(requestSyncResp.BrowserExtensionWaitToken) if err != nil { t.Errorf("Error when Browser Extension waited for sync confirm: %v", err) return @@ -51,11 +60,7 @@ func TestSyncHappyFlow(t *testing.T) { return } - confirmToken, err := getMobileToken(fcm) - if err != nil { - t.Errorf("Failed to fetch mobile token: %v", err) - return - } + confirmToken := <-confirmMobileChannel proxyToken, err := confirmSyncMobile(confirmToken) if err != nil { diff --git a/e2e-tests/pass/ws.go b/e2e-tests/pass/ws.go index 1fce375..70b9479 100644 --- a/e2e-tests/pass/ws.go +++ b/e2e-tests/pass/ws.go @@ -24,7 +24,7 @@ func getWsURL() string { } func browserExtensionWaitForSyncConfirm(token string) (string, error) { - url := getWsURL() + "/browser_extension/sync/request" + url := getWsURL() + "/browser_extension/sync/wait" var resp struct { BrowserExtensionSyncToken string `json:"browser_extension_proxy_token"` diff --git a/internal/pass/server.go b/internal/pass/server.go index 654fff2..026e8ef 100644 --- a/internal/pass/server.go +++ b/internal/pass/server.go @@ -14,7 +14,6 @@ import ( "github.com/twofas/2fas-server/config" httphelpers "github.com/twofas/2fas-server/internal/common/http" - "github.com/twofas/2fas-server/internal/common/logging" "github.com/twofas/2fas-server/internal/common/recovery" "github.com/twofas/2fas-server/internal/pass/connection" "github.com/twofas/2fas-server/internal/pass/pairing" @@ -100,16 +99,14 @@ func NewServer(cfg config.PassConfig) *Server { router.POST("/mobile/pairing/confirm", pairing.MobileConfirmHandler(pairingApp)) router.GET("/mobile/pairing/proxy", pairing.MobileProxyWSHandler(pairingApp, proxyPairingApp)) - router.GET("/browser_extension/sync/request", sync.ExtensionRequestSync(syncApp)) + router.POST("/browser_extension/sync/request", sync.ExtensionRequestSync(syncApp)) + router.POST("/browser_extension/sync/push", sync.ExtensionRequestPush(syncApp)) + router.GET("/browser_extension/sync/wait", sync.ExtensionRequestWait(syncApp)) + router.GET("/browser_extension/sync/proxy", sync.ExtensionProxyWSHandler(syncApp, proxySyncApp)) router.POST("/mobile/sync/confirm", sync.MobileConfirmHandler(syncApp)) router.GET("/mobile/sync/proxy", sync.MobileProxyWSHandler(syncApp, proxySyncApp)) - if cfg.FakeMobilePush { - logging.Info("Enabled '/mobile/sync/:fcm/token' endpoint. This should happen in test env only!") - router.GET("/mobile/sync/:fcm/token", sync.MobileGenerateSyncToken(syncApp)) - } - return &Server{ router: router, addr: cfg.Addr, diff --git a/internal/pass/sign/lib.go b/internal/pass/sign/lib.go index 96b6fc1..737347b 100644 --- a/internal/pass/sign/lib.go +++ b/internal/pass/sign/lib.go @@ -59,6 +59,7 @@ const ( ConnectionTypeBrowserExtensionWait ConnectionType = "be/wait" ConnectionTypeBrowserExtensionProxy ConnectionType = "be/proxy" ConnectionTypeBrowserExtensionSyncRequest ConnectionType = "be/sync/request" + ConnectionTypeBrowserExtensionSyncWait ConnectionType = "be/sync/wait" ConnectionTypeBrowserExtensionSync ConnectionType = "be/sync/proxy" ConnectionTypeMobileProxy ConnectionType = "mobile/proxy" ConnectionTypeMobileConfirm ConnectionType = "mobile/confirm" diff --git a/internal/pass/sync/auth.go b/internal/pass/sync/auth.go index 5f6e607..181352c 100644 --- a/internal/pass/sync/auth.go +++ b/internal/pass/sync/auth.go @@ -16,6 +16,15 @@ func (s *Syncing) VerifyExtRequestSyncToken(ctx context.Context, proxyToken stri return fcmToken, nil } +// VerifyExtWaitForSyncToken verifies wait for sync request token and returns fcm_token. +func (s *Syncing) VerifyExtWaitForSyncToken(ctx context.Context, proxyToken string) (string, error) { + fcmToken, err := s.signSvc.CanI(proxyToken, sign.ConnectionTypeBrowserExtensionSyncWait) + if err != nil { + return "", fmt.Errorf("failed to check token signature: %w", err) + } + return fcmToken, nil +} + // VerifyExtSyncToken verifies sync token and returns fcm_token. func (s *Syncing) VerifyExtSyncToken(ctx context.Context, proxyToken string) (string, error) { fcmToken, err := s.signSvc.CanI(proxyToken, sign.ConnectionTypeBrowserExtensionSync) diff --git a/internal/pass/sync/handlers.go b/internal/pass/sync/handlers.go index bce86bc..671e6ac 100644 --- a/internal/pass/sync/handlers.go +++ b/internal/pass/sync/handlers.go @@ -13,21 +13,82 @@ import ( func ExtensionRequestSync(syncingApp *Syncing) gin.HandlerFunc { return func(gCtx *gin.Context) { - token, err := connection.TokenFromWSProtocol(gCtx.Request) + log := logging.FromContext(gCtx.Request.Context()) + + token, err := tokenFromRequest(gCtx) if err != nil { - logging.Errorf("Failed to get token from request: %v", err) + log.Errorf("Failed to get token from request: %v", err) gCtx.Status(http.StatusForbidden) return } fcmToken, err := syncingApp.VerifyExtRequestSyncToken(gCtx, token) if err != nil { - logging.Errorf("Failed to verify proxy token: %v", err) + log.Errorf("Failed to verify proxy token: %v", err) + gCtx.String(http.StatusUnauthorized, "Invalid auth token") + return + } + + resp, err := syncingApp.RequestSync(gCtx, fcmToken) + if err != nil { + log.Errorf("Failed to request sync: %v", err) + gCtx.Status(http.StatusInternalServerError) + return + } + gCtx.JSON(http.StatusOK, resp) + } +} + +type PushToMobileRequest struct { + Body string `json:"push_body"` +} + +func ExtensionRequestPush(syncingApp *Syncing) gin.HandlerFunc { + return func(gCtx *gin.Context) { + log := logging.FromContext(gCtx.Request.Context()) + + token, err := tokenFromRequest(gCtx) + if err != nil { + log.Errorf("Failed to get token from request: %v", err) + gCtx.Status(http.StatusForbidden) + return + } + fcmToken, err := syncingApp.VerifyExtWaitForSyncToken(gCtx, token) + if err != nil { + log.Errorf("Failed to verify proxy token: %v", err) + gCtx.String(http.StatusUnauthorized, "Invalid auth token") + return + } + var req PushToMobileRequest + if err := gCtx.BindJSON(&req); err != nil { + gCtx.String(http.StatusBadRequest, err.Error()) + return + } + + log.Infof("Send push to mobile %q: %q", fcmToken, req.Body) + + gCtx.Status(http.StatusOK) + } +} + +func ExtensionRequestWait(syncingApp *Syncing) gin.HandlerFunc { + return func(gCtx *gin.Context) { + log := logging.FromContext(gCtx.Request.Context()) + + token, err := connection.TokenFromWSProtocol(gCtx.Request) + if err != nil { + log.Errorf("Failed to get token from request: %v", err) + gCtx.Status(http.StatusForbidden) + return + } + fcmToken, err := syncingApp.VerifyExtWaitForSyncToken(gCtx, token) + if err != nil { + log.Errorf("Failed to verify proxy token: %v", err) gCtx.String(http.StatusUnauthorized, "Invalid auth token") return } if err := syncingApp.ServeSyncingRequestWS(gCtx.Writer, gCtx.Request, fcmToken); err != nil { - logging.Errorf("Failed to verify proxy token: %v", err) + log.Errorf("Failed to verify proxy token: %v", err) gCtx.Status(http.StatusInternalServerError) return } @@ -36,15 +97,17 @@ func ExtensionRequestSync(syncingApp *Syncing) gin.HandlerFunc { func ExtensionProxyWSHandler(syncingApp *Syncing, proxy *connection.ProxyServer) gin.HandlerFunc { return func(gCtx *gin.Context) { + log := logging.FromContext(gCtx.Request.Context()) + token, err := connection.TokenFromWSProtocol(gCtx.Request) if err != nil { - logging.Errorf("Failed to get token from request: %v", err) + log.Errorf("Failed to get token from request: %v", err) gCtx.Status(http.StatusForbidden) return } fcmToken, err := syncingApp.VerifyExtSyncToken(gCtx, token) if err != nil { - logging.Errorf("Failed to verify proxy token: %v", err) + log.Errorf("Failed to verify proxy token: %v", err) gCtx.Status(http.StatusInternalServerError) return } @@ -54,7 +117,7 @@ func ExtensionProxyWSHandler(syncingApp *Syncing, proxy *connection.ProxyServer) return } if err := proxy.ServeExtensionProxyToMobileWS(gCtx.Writer, gCtx.Request, fcmToken); err != nil { - logging.Errorf("Failed to serve ws: %v", err) + log.Errorf("Failed to serve ws: %v", err) gCtx.Status(http.StatusInternalServerError) } } @@ -62,15 +125,17 @@ func ExtensionProxyWSHandler(syncingApp *Syncing, proxy *connection.ProxyServer) func MobileConfirmHandler(syncApp *Syncing) gin.HandlerFunc { return func(gCtx *gin.Context) { + log := logging.FromContext(gCtx.Request.Context()) + token, err := tokenFromRequest(gCtx) if err != nil { - logging.Errorf("Failed to get token from request: %v", err) + log.Errorf("Failed to get token from request: %v", err) gCtx.Status(http.StatusForbidden) return } fcmToken, err := syncApp.VerifyMobileSyncConfirmToken(gCtx, token) if err != nil { - logging.Errorf("Failed to verify connection token: %v", err) + log.Errorf("Failed to verify connection token: %v", err) gCtx.Status(http.StatusUnauthorized) return } @@ -80,7 +145,7 @@ func MobileConfirmHandler(syncApp *Syncing) gin.HandlerFunc { gCtx.String(http.StatusBadRequest, "no sync request was created for this token") return } - logging.Errorf("Failed to ConfirmPairing: %v", err) + log.Errorf("Failed to ConfirmPairing: %v", err) gCtx.Status(http.StatusInternalServerError) return } @@ -90,15 +155,17 @@ func MobileConfirmHandler(syncApp *Syncing) gin.HandlerFunc { func MobileProxyWSHandler(syncingApp *Syncing, proxy *connection.ProxyServer) gin.HandlerFunc { return func(gCtx *gin.Context) { + log := logging.FromContext(gCtx.Request.Context()) + token, err := connection.TokenFromWSProtocol(gCtx.Request) if err != nil { - logging.Errorf("Failed to get token from request: %v", err) + log.Errorf("Failed to get token from request: %v", err) gCtx.Status(http.StatusForbidden) return } fcmToken, err := syncingApp.VerifyMobileSyncConfirmToken(gCtx, token) if err != nil { - logging.Errorf("Invalid connection token: %v", err) + log.Errorf("Invalid connection token: %v", err) gCtx.Status(http.StatusUnauthorized) return } @@ -108,21 +175,7 @@ func MobileProxyWSHandler(syncingApp *Syncing, proxy *connection.ProxyServer) gi return } if err := proxy.ServeMobileProxyToExtensionWS(gCtx.Writer, gCtx.Request, fcmToken); err != nil { - logging.Errorf("Failed to serve ws: %v", err) - gCtx.Status(http.StatusInternalServerError) - } - } -} - -func MobileGenerateSyncToken(syncingApp *Syncing) gin.HandlerFunc { - return func(gCtx *gin.Context) { - fcm := gCtx.Param("fcm") - if fcm == "" { - gCtx.Status(http.StatusBadRequest) - return - } - if err := syncingApp.sendMobileToken(fcm, gCtx.Writer); err != nil { - logging.Errorf("Failed to send mobile token: %v", err) + log.Errorf("Failed to serve ws: %v", err) gCtx.Status(http.StatusInternalServerError) } } diff --git a/internal/pass/sync/sync.go b/internal/pass/sync/sync.go index fb02dba..49bd4ad 100644 --- a/internal/pass/sync/sync.go +++ b/internal/pass/sync/sync.go @@ -2,12 +2,12 @@ package sync import ( "context" - "encoding/json" "errors" "fmt" "net/http" "time" + "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/twofas/2fas-server/internal/common/logging" @@ -37,10 +37,6 @@ const ( syncTokenValidityDuration = 3 * time.Minute ) -type ConfigureBrowserExtensionRequest struct { - ExtensionID string `json:"extension_id"` -} - type ConfigureBrowserExtensionResponse struct { BrowserExtensionPairingToken string `json:"browser_extension_pairing_token"` ConnectionToken string `json:"connection_token"` @@ -51,11 +47,6 @@ type ExtensionWaitForConnectionInput struct { HttpReq *http.Request } -type RequestSyncResponse struct { - BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"` - Status string `json:"status"` -} - type MobileSyncPayload struct { MobileSyncToken string `json:"mobile_sync_token"` } @@ -112,6 +103,11 @@ func (s *Syncing) requestSync(ctx context.Context, fcmToken string) { s.store.RequestSync(fcmToken) } +type WaitForSyncResponse struct { + BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"` + Status string `json:"status"` +} + func (s *Syncing) sendTokenAndCloseConn(fcmToken string, conn *websocket.Conn) error { extProxyToken, err := s.signSvc.SignAndEncode(sign.Message{ ConnectionID: fcmToken, @@ -122,7 +118,7 @@ func (s *Syncing) sendTokenAndCloseConn(fcmToken string, conn *websocket.Conn) e return fmt.Errorf("failed to generate ext proxy token: %v", err) } - if err := conn.WriteJSON(RequestSyncResponse{ + if err := conn.WriteJSON(WaitForSyncResponse{ BrowserExtensionProxyToken: extProxyToken, Status: "ok", }); err != nil { @@ -131,30 +127,6 @@ func (s *Syncing) sendTokenAndCloseConn(fcmToken string, conn *websocket.Conn) e return conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) } -func (s *Syncing) sendMobileToken(fcmToken string, resp http.ResponseWriter) error { - extProxyToken, err := s.signSvc.SignAndEncode(sign.Message{ - ConnectionID: fcmToken, - ExpiresAt: time.Now().Add(syncTokenValidityDuration), - ConnectionType: sign.ConnectionTypeMobileSyncConfirm, - }) - if err != nil { - return fmt.Errorf("failed to generate ext proxy token: %v", err) - } - - bb, err := json.Marshal(struct { - MobileSyncConfirmToken string `json:"mobile_sync_confirm_token"` - }{ - MobileSyncConfirmToken: extProxyToken, - }) - if err != nil { - return fmt.Errorf("failed to marshal the response: %v", err) - } - if _, err := resp.Write(bb); err != nil { - return fmt.Errorf("failed to write the response: %v", err) - } - return nil -} - type ConfirmSyncResponse struct { ProxyToken string `json:"proxy_token"` } @@ -178,3 +150,31 @@ func (s *Syncing) confirmSync(ctx context.Context, fcmToken string) (ConfirmSync return ConfirmSyncResponse{ProxyToken: mobileProxyToken}, nil } + +type RequestSyncResponse struct { + BrowserExtensionWaitToken string `json:"browser_extension_wait_token"` + MobileConfirmToken string `json:"mobile_confirm_token"` +} + +func (s *Syncing) RequestSync(ctx *gin.Context, token string) (RequestSyncResponse, error) { + mobileConfirmToken, err := s.signSvc.SignAndEncode(sign.Message{ + ConnectionID: token, + ExpiresAt: time.Now().Add(syncTokenValidityDuration), + ConnectionType: sign.ConnectionTypeMobileSyncConfirm, + }) + if err != nil { + return RequestSyncResponse{}, fmt.Errorf("failed to generate mobile confirm token: %v", err) + } + browserExtensionWaitToken, err := s.signSvc.SignAndEncode(sign.Message{ + ConnectionID: token, + ExpiresAt: time.Now().Add(syncTokenValidityDuration), + ConnectionType: sign.ConnectionTypeBrowserExtensionSyncWait, + }) + if err != nil { + return RequestSyncResponse{}, fmt.Errorf("failed to generate browser extension wait token: %v", err) + } + return RequestSyncResponse{ + BrowserExtensionWaitToken: browserExtensionWaitToken, + MobileConfirmToken: mobileConfirmToken, + }, nil +}