2fas-server/internal/api/browser_extension/app/command/request_2fa_token.go
2023-03-12 12:43:08 +01:00

162 lines
4.0 KiB
Go

package command
import (
"context"
"firebase.google.com/go/v4/messaging"
"fmt"
"github.com/avast/retry-go/v4"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/twofas/2fas-server/internal/api/browser_extension/domain"
"github.com/twofas/2fas-server/internal/common/logging"
"github.com/twofas/2fas-server/internal/common/push"
"net/url"
"time"
)
var tokenPushNotificationTtl = time.Minute * 3
type Request2FaTokenPushNotification struct {
ExtensionId string `json:"extension_id"`
IssuerDomain string `json:"issuer_domain"`
RequestId string `json:"request_id"`
}
type Request2FaToken struct {
Id string `validate:"required,uuid4"`
ExtensionId string `uri:"extension_id" validate:"required,uuid4"`
Domain string `json:"domain" validate:"required,lte=256"`
}
func New2FaTokenRequestFromGin(c *gin.Context) *Request2FaToken {
id := uuid.New()
cmd := &Request2FaToken{}
cmd.Id = id.String()
c.BindJSON(&cmd)
c.BindUri(&cmd)
u, err := url.Parse(cmd.Domain)
if err != nil {
cmd.Domain = ""
return cmd
}
cmd.Domain = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
return cmd
}
type Request2FaTokenHandler struct {
BrowserExtensionsRepository domain.BrowserExtensionRepository
BrowserExtension2FaRequestRepository domain.BrowserExtension2FaRequestRepository
PairedDevicesRepository domain.BrowserExtensionDevicesRepository
Pusher push.Pusher
}
func (h *Request2FaTokenHandler) Handle(cmd *Request2FaToken) error {
extId, _ := uuid.Parse(cmd.ExtensionId)
browserExtension, err := h.BrowserExtensionsRepository.FindById(extId)
if err != nil {
return err
}
tokenRequestId, _ := uuid.Parse(cmd.Id)
browserExtension2FaRequest := domain.NewBrowserExtension2FaRequest(tokenRequestId, browserExtension.Id, cmd.Domain)
err = h.BrowserExtension2FaRequestRepository.Save(browserExtension2FaRequest)
if err != nil {
return err
}
pairedDevices := h.PairedDevicesRepository.FindAll(browserExtension.Id)
data := map[string]interface{}{
"extension_id": extId.String(),
"request_id": cmd.Id,
"domain": cmd.Domain,
"type": "browser_extension_request",
}
for _, device := range pairedDevices {
var err error
var notification *messaging.Message
switch device.Platform {
case domain.Android:
notification = createPushNotificationForAndroid(device.FcmToken, data)
case domain.IOS:
notification = createPushNotificationForIos(device.FcmToken, data)
}
err = retry.Do(
func() error {
return h.Pusher.Send(context.Background(), notification)
},
retry.Attempts(5),
retry.LastErrorOnly(true),
)
if err != nil && !messaging.IsUnregistered(err) {
logging.WithFields(logging.Fields{
"extension_id": extId.String(),
"device_id": device.Id.String(),
"token_request_id": cmd.Id,
"domain": cmd.Domain,
"platform": device.Platform,
"type": "browser_extension_request",
"error": err.Error(),
}).Error("Cannot send push notification for \"2fa_request\"")
}
}
return nil
}
func createPushNotificationForIos(token string, data map[string]interface{}) *messaging.Message {
ttl := time.Now().Add(tokenPushNotificationTtl)
return &messaging.Message{
APNS: &messaging.APNSConfig{
Headers: map[string]string{
"apns-expiration": fmt.Sprintf("%d", ttl.Unix()),
},
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
Alert: &messaging.ApsAlert{
Title: "2FA request",
Body: fmt.Sprintf("2FA request for %s", data["domain"]),
},
Category: "authReq",
},
CustomData: data,
},
},
Token: token,
}
}
func createPushNotificationForAndroid(token string, data map[string]interface{}) *messaging.Message {
androidData := make(map[string]string, len(data))
for key, value := range data {
androidData[key] = value.(string)
}
androidData["click_action"] = "auth_request"
return &messaging.Message{
Android: &messaging.AndroidConfig{
Data: androidData,
TTL: &tokenPushNotificationTtl,
},
Token: token,
}
}