Skip to content

on transaction send fingerprint and ipaddress to unit21 #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func userRoute(services service.Services, e *echo.Echo) {
}

func loginRoute(services service.Services, e *echo.Echo) {
handler := handler.NewLogin(e, services.Auth)
handler := handler.NewLogin(e, services.Auth, services.Device)
handler.RegisterRoutes(e.Group("/login"), middleware.APIKeyAuth(services.Auth))
}

Expand Down
17 changes: 15 additions & 2 deletions api/handler/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package handler
import (
b64 "encoding/base64"
"net/http"
"os"
"strings"

"github.com/String-xyz/string-api/pkg/model"
"github.com/String-xyz/string-api/pkg/service"
"github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
)

Expand All @@ -23,11 +25,12 @@ type Login interface {

type login struct {
Service service.Auth
Device service.Device
Group *echo.Group
}

func NewLogin(route *echo.Echo, service service.Auth) Login {
return &login{service, nil}
func NewLogin(route *echo.Echo, service service.Auth, device service.Device) Login {
return &login{service, device, nil}
}

func (l login) NoncePayload(c echo.Context) error {
Expand Down Expand Up @@ -78,12 +81,22 @@ func (l login) VerifySignature(c echo.Context) error {
LogStringError(c, err, "login: verify signature")
return BadRequestError(c, "Invalid Payload")
}

// Upsert IP address in user's device
var claims = &service.JWTClaims{}
_, _ = jwt.ParseWithClaims(resp.JWT.Token, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET_KEY")), nil
})
ip := c.RealIP()
l.Device.UpsertDeviceIP(claims.DeviceId, ip)

// set auth cookies
err = SetAuthCookies(c, resp.JWT)
if err != nil {
LogStringError(c, err, "login: unable to set auth cookies")
return InternalError(c)
}

return c.JSON(http.StatusOK, resp)
}

Expand Down
4 changes: 3 additions & 1 deletion api/handler/transact.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ func (t transaction) Transact(c echo.Context) error {
}
userId := c.Get("userId").(string)
deviceId := c.Get("deviceId").(string)
res, err := t.Service.Execute(body, userId, deviceId)
ip := c.RealIP()

res, err := t.Service.Execute(body, userId, deviceId, ip)
if err != nil && (strings.Contains(err.Error(), "risk:") || strings.Contains(err.Error(), "payment:")) {
LogStringError(c, err, "transact: execute")
return Unprocessable(c)
Expand Down
9 changes: 9 additions & 0 deletions pkg/internal/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,12 @@ func BetterStringify(jsonBody any) (betterString string, err error) {

return
}

func SliceContains(elems []string, v string) bool {
for _, s := range elems {
if v == s {
return true
}
}
return false
}
61 changes: 48 additions & 13 deletions pkg/internal/unit21/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ type Transaction interface {
}

type TransactionRepos struct {
User repository.User
TxLeg repository.TxLeg
Asset repository.Asset
User repository.User
TxLeg repository.TxLeg
Asset repository.Asset
Device repository.Device
}

type transaction struct {
Expand All @@ -37,12 +38,18 @@ func (t transaction) Evaluate(transaction model.Transaction) (pass bool, err err
return false, common.StringError(err)
}

digitalData, err := t.getEventDigitalData(transaction)
if err != nil {
log.Err(err).Msg("Failed to gather Unit21 digital data")
return false, common.StringError(err)
}

url := os.Getenv("UNIT21_RTR_URL")
if url == "" {
url = "https://rtr.sandbox2.unit21.com/evaluate"
}

body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData))
body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData, digitalData))
if err != nil {
log.Err(err).Msg("Unit21 Transaction evaluate failed")
return false, common.StringError(err)
Expand All @@ -67,14 +74,19 @@ func (t transaction) Evaluate(transaction model.Transaction) (pass bool, err err

func (t transaction) Create(transaction model.Transaction) (unit21Id string, err error) {
transactionData, err := t.getTransactionData(transaction)

if err != nil {
log.Err(err).Msg("Failed to gather Unit21 transaction source")
return "", common.StringError(err)
}

digitalData, err := t.getEventDigitalData(transaction)
if err != nil {
log.Err(err).Msg("Failed to gather Unit21 digital data")
return "", common.StringError(err)
}

url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/events/create"
body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData))
body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData, digitalData))
if err != nil {
log.Err(err).Msg("Unit21 Transaction create failed")
return "", common.StringError(err)
Expand All @@ -98,9 +110,15 @@ func (t transaction) Update(transaction model.Transaction) (unit21Id string, err
return "", common.StringError(err)
}

digitalData, err := t.getEventDigitalData(transaction)
if err != nil {
log.Err(err).Msg("Failed to gather Unit21 digital data")
return "", common.StringError(err)
}

orgName := os.Getenv("UNIT21_ORG_NAME")
url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/" + orgName + "/events/" + transaction.ID + "/update"
body, err := u21Put(url, mapToUnit21TransactionEvent(transaction, transactionData))
body, err := u21Put(url, mapToUnit21TransactionEvent(transaction, transactionData, digitalData))

if err != nil {
log.Err(err).Msg("Unit21 Transaction create failed:")
Expand Down Expand Up @@ -213,7 +231,26 @@ func (t transaction) getTransactionData(transaction model.Transaction) (txData t
return
}

func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData transactionData) *u21Event {
func (t transaction) getEventDigitalData(transaction model.Transaction) (digitalData eventDigitalData, err error) {
if transaction.DeviceID == "" {
return
}

device, err := t.repos.Device.GetById(transaction.DeviceID)
if err != nil {
log.Err(err).Msg("Failed to get transaction device")
err = common.StringError(err)
return
}

digitalData = eventDigitalData{
IPAddress: transaction.IPAddress,
ClientFingerprint: device.Fingerprint,
}
return
}

func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData transactionData, digitalData eventDigitalData) *u21Event {
var transactionTagArr []string
if transaction.Tags != nil {
for key, value := range transaction.Tags {
Expand All @@ -233,11 +270,9 @@ func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData
},
TransactionData: &transactionData,
ActionData: nil,
DigitalData: &eventDigitalData{
IPAddress: transaction.IPAddress,
},
LocationData: nil,
CustomData: nil,
DigitalData: &digitalData,
LocationData: nil,
CustomData: nil,
}

return jsonBody
Expand Down
3 changes: 2 additions & 1 deletion pkg/internal/unit21/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ type actionData struct {
}

type eventDigitalData struct {
IPAddress string `json:"ip_address,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
ClientFingerprint string `json:"client_fingerprint,omitempty"`
}

type eventCustomData struct {
Expand Down
3 changes: 2 additions & 1 deletion pkg/model/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ type NetworkUpdates struct {
}

type DeviceUpdates struct {
ValidatedAt *time.Time `json:"validatedAt" db:"validated_at"`
ValidatedAt *time.Time `json:"validatedAt" db:"validated_at"`
IpAddresses *pq.StringArray `json:"ipAddresses" db:"ip_addresses"`
}

type RefreshTokenPayload struct {
Expand Down
4 changes: 2 additions & 2 deletions pkg/repository/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func (t transaction[T]) Create(insert model.Transaction) (model.Transaction, err
m := model.Transaction{}
// TODO: Add platform_id once it becomes available
rows, err := t.store.NamedQuery(`
INSERT INTO transaction (status, network_id, device_id, platform_id)
VALUES(:status, :network_id, :device_id, :platform_id) RETURNING id`, insert)
INSERT INTO transaction (status, network_id, device_id, platform_id, ip_address)
VALUES(:status, :network_id, :device_id, :platform_id, :ip_address) RETURNING id`, insert)
if err != nil {
return m, common.StringError(err)
}
Expand Down
94 changes: 56 additions & 38 deletions pkg/service/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

type Device interface {
VerifyDevice(encrypted string) error
UpsertDeviceIP(deviceId string, Ip string) (err error)
CreateDeviceIfNeeded(userID, visitorID, requestID string) (model.Device, error)
CreateUnknownDevice(userID string) (model.Device, error)
InvalidateUnknownDevice(device model.Device) error
Expand All @@ -27,30 +28,36 @@ func NewDevice(repos repository.Repositories, f Fingerprint) Device {
return &device{repos, f}
}

func (d device) createDevice(userID string, visitor FPVisitor, description string) (model.Device, error) {
addresses := pq.StringArray{}
if visitor.IPAddress.String != "" {
addresses = pq.StringArray{visitor.IPAddress.String}
func (d device) VerifyDevice(encrypted string) error {
key := os.Getenv("STRING_ENCRYPTION_KEY")
received, err := common.Decrypt[DeviceVerification](encrypted, key)
if err != nil {
return common.StringError(err)
}

return d.repos.Device.Create(model.Device{
UserID: userID,
Fingerprint: visitor.VisitorID,
Type: visitor.Type,
IpAddresses: addresses,
Description: description,
LastUsedAt: time.Now(),
})
now := time.Now()
if now.Unix()-received.Timestamp > (60 * 15) {
return common.StringError(errors.New("link expired"))
}
err = d.repos.Device.Update(received.DeviceID, model.DeviceUpdates{ValidatedAt: &now})
return err
}

func (d device) CreateUnknownDevice(userID string) (model.Device, error) {
visitor := FPVisitor{
VisitorID: "unknown",
Type: "unknown",
UserAgent: "unknown",
func (d device) UpsertDeviceIP(deviceId string, ip string) (err error) {
device, err := d.repos.Device.GetById(deviceId)
if err != nil {
return
}
device, err := d.createDevice(userID, visitor, "an unknown device")
return device, common.StringError(err)
contains := common.SliceContains(device.IpAddresses, ip)
if !contains {
ipAddresses := append(device.IpAddresses, ip)
updates := &model.DeviceUpdates{IpAddresses: &ipAddresses}
err = d.repos.Device.Update(deviceId, updates)
if err != nil {
return
}
}
return
}

func (d device) CreateDeviceIfNeeded(userID, visitorID, requestID string) (model.Device, error) {
Expand Down Expand Up @@ -88,19 +95,39 @@ func (d device) CreateDeviceIfNeeded(userID, visitorID, requestID string) (model
}
}

func (d device) VerifyDevice(encrypted string) error {
key := os.Getenv("STRING_ENCRYPTION_KEY")
received, err := common.Decrypt[DeviceVerification](encrypted, key)
if err != nil {
return common.StringError(err)
func (d device) CreateUnknownDevice(userID string) (model.Device, error) {
visitor := FPVisitor{
VisitorID: "unknown",
Type: "unknown",
UserAgent: "unknown",
}
device, err := d.createDevice(userID, visitor, "an unknown device")
return device, common.StringError(err)
}

now := time.Now()
if now.Unix()-received.Timestamp > (60 * 15) {
return common.StringError(errors.New("link expired"))
func (d device) InvalidateUnknownDevice(device model.Device) error {
if device.Fingerprint != "unknown" {
return nil // only unknown devices can be invalidated
}
err = d.repos.Device.Update(received.DeviceID, model.DeviceUpdates{ValidatedAt: &now})
return err

device.ValidatedAt = &time.Time{} // Zero time to set it to nil
return d.repos.Device.Update(device.ID, device)
}

func (d device) createDevice(userID string, visitor FPVisitor, description string) (model.Device, error) {
addresses := pq.StringArray{}
if visitor.IPAddress.String != "" {
addresses = pq.StringArray{visitor.IPAddress.String}
}

return d.repos.Device.Create(model.Device{
UserID: userID,
Fingerprint: visitor.VisitorID,
Type: visitor.Type,
IpAddresses: addresses,
Description: description,
LastUsedAt: time.Now(),
})
}

func (d device) getOrCreateUnknownDevice(userId, visitorId string) (model.Device, error) {
Expand All @@ -123,12 +150,3 @@ func (d device) getOrCreateUnknownDevice(userId, visitorId string) (model.Device
func isDeviceValidated(device model.Device) bool {
return device.ValidatedAt != nil && !device.ValidatedAt.IsZero()
}

func (d device) InvalidateUnknownDevice(device model.Device) error {
if device.Fingerprint != "unknown" {
return nil // only unknown devices can be invalidated
}

device.ValidatedAt = &time.Time{} // Zero time to set it to nil
return d.repos.Device.Update(device.ID, device)
}
Loading