Skip to content

Commit f816b39

Browse files
author
akfoster
authored
send fingerprint and ipaddress (#130)
1 parent 9c1c14e commit f816b39

File tree

11 files changed

+145
-66
lines changed

11 files changed

+145
-66
lines changed

api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func userRoute(services service.Services, e *echo.Echo) {
9696
}
9797

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

api/handler/login.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package handler
33
import (
44
b64 "encoding/base64"
55
"net/http"
6+
"os"
67
"strings"
78

89
"github.com/String-xyz/string-api/pkg/model"
910
"github.com/String-xyz/string-api/pkg/service"
11+
"github.com/golang-jwt/jwt"
1012
"github.com/labstack/echo/v4"
1113
)
1214

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

2426
type login struct {
2527
Service service.Auth
28+
Device service.Device
2629
Group *echo.Group
2730
}
2831

29-
func NewLogin(route *echo.Echo, service service.Auth) Login {
30-
return &login{service, nil}
32+
func NewLogin(route *echo.Echo, service service.Auth, device service.Device) Login {
33+
return &login{service, device, nil}
3134
}
3235

3336
func (l login) NoncePayload(c echo.Context) error {
@@ -78,12 +81,22 @@ func (l login) VerifySignature(c echo.Context) error {
7881
LogStringError(c, err, "login: verify signature")
7982
return BadRequestError(c, "Invalid Payload")
8083
}
84+
85+
// Upsert IP address in user's device
86+
var claims = &service.JWTClaims{}
87+
_, _ = jwt.ParseWithClaims(resp.JWT.Token, claims, func(t *jwt.Token) (interface{}, error) {
88+
return []byte(os.Getenv("JWT_SECRET_KEY")), nil
89+
})
90+
ip := c.RealIP()
91+
l.Device.UpsertDeviceIP(claims.DeviceId, ip)
92+
8193
// set auth cookies
8294
err = SetAuthCookies(c, resp.JWT)
8395
if err != nil {
8496
LogStringError(c, err, "login: unable to set auth cookies")
8597
return InternalError(c)
8698
}
99+
87100
return c.JSON(http.StatusOK, resp)
88101
}
89102

api/handler/transact.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ func (t transaction) Transact(c echo.Context) error {
3838
}
3939
userId := c.Get("userId").(string)
4040
deviceId := c.Get("deviceId").(string)
41-
res, err := t.Service.Execute(body, userId, deviceId)
41+
ip := c.RealIP()
42+
43+
res, err := t.Service.Execute(body, userId, deviceId, ip)
4244
if err != nil && (strings.Contains(err.Error(), "risk:") || strings.Contains(err.Error(), "payment:")) {
4345
LogStringError(c, err, "transact: execute")
4446
return Unprocessable(c)

pkg/internal/common/util.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,12 @@ func BetterStringify(jsonBody any) (betterString string, err error) {
115115

116116
return
117117
}
118+
119+
func SliceContains(elems []string, v string) bool {
120+
for _, s := range elems {
121+
if v == s {
122+
return true
123+
}
124+
}
125+
return false
126+
}

pkg/internal/unit21/transaction.go

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ type Transaction interface {
1717
}
1818

1919
type TransactionRepos struct {
20-
User repository.User
21-
TxLeg repository.TxLeg
22-
Asset repository.Asset
20+
User repository.User
21+
TxLeg repository.TxLeg
22+
Asset repository.Asset
23+
Device repository.Device
2324
}
2425

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

41+
digitalData, err := t.getEventDigitalData(transaction)
42+
if err != nil {
43+
log.Err(err).Msg("Failed to gather Unit21 digital data")
44+
return false, common.StringError(err)
45+
}
46+
4047
url := os.Getenv("UNIT21_RTR_URL")
4148
if url == "" {
4249
url = "https://rtr.sandbox2.unit21.com/evaluate"
4350
}
4451

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

6875
func (t transaction) Create(transaction model.Transaction) (unit21Id string, err error) {
6976
transactionData, err := t.getTransactionData(transaction)
70-
7177
if err != nil {
7278
log.Err(err).Msg("Failed to gather Unit21 transaction source")
7379
return "", common.StringError(err)
7480
}
7581

82+
digitalData, err := t.getEventDigitalData(transaction)
83+
if err != nil {
84+
log.Err(err).Msg("Failed to gather Unit21 digital data")
85+
return "", common.StringError(err)
86+
}
87+
7688
url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/events/create"
77-
body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData))
89+
body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData, digitalData))
7890
if err != nil {
7991
log.Err(err).Msg("Unit21 Transaction create failed")
8092
return "", common.StringError(err)
@@ -98,9 +110,15 @@ func (t transaction) Update(transaction model.Transaction) (unit21Id string, err
98110
return "", common.StringError(err)
99111
}
100112

113+
digitalData, err := t.getEventDigitalData(transaction)
114+
if err != nil {
115+
log.Err(err).Msg("Failed to gather Unit21 digital data")
116+
return "", common.StringError(err)
117+
}
118+
101119
orgName := os.Getenv("UNIT21_ORG_NAME")
102120
url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/" + orgName + "/events/" + transaction.ID + "/update"
103-
body, err := u21Put(url, mapToUnit21TransactionEvent(transaction, transactionData))
121+
body, err := u21Put(url, mapToUnit21TransactionEvent(transaction, transactionData, digitalData))
104122

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

216-
func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData transactionData) *u21Event {
234+
func (t transaction) getEventDigitalData(transaction model.Transaction) (digitalData eventDigitalData, err error) {
235+
if transaction.DeviceID == "" {
236+
return
237+
}
238+
239+
device, err := t.repos.Device.GetById(transaction.DeviceID)
240+
if err != nil {
241+
log.Err(err).Msg("Failed to get transaction device")
242+
err = common.StringError(err)
243+
return
244+
}
245+
246+
digitalData = eventDigitalData{
247+
IPAddress: transaction.IPAddress,
248+
ClientFingerprint: device.Fingerprint,
249+
}
250+
return
251+
}
252+
253+
func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData transactionData, digitalData eventDigitalData) *u21Event {
217254
var transactionTagArr []string
218255
if transaction.Tags != nil {
219256
for key, value := range transaction.Tags {
@@ -233,11 +270,9 @@ func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData
233270
},
234271
TransactionData: &transactionData,
235272
ActionData: nil,
236-
DigitalData: &eventDigitalData{
237-
IPAddress: transaction.IPAddress,
238-
},
239-
LocationData: nil,
240-
CustomData: nil,
273+
DigitalData: &digitalData,
274+
LocationData: nil,
275+
CustomData: nil,
241276
}
242277

243278
return jsonBody

pkg/internal/unit21/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ type actionData struct {
170170
}
171171

172172
type eventDigitalData struct {
173-
IPAddress string `json:"ip_address,omitempty"`
173+
IPAddress string `json:"ip_address,omitempty"`
174+
ClientFingerprint string `json:"client_fingerprint,omitempty"`
174175
}
175176

176177
type eventCustomData struct {

pkg/model/request.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ type NetworkUpdates struct {
128128
}
129129

130130
type DeviceUpdates struct {
131-
ValidatedAt *time.Time `json:"validatedAt" db:"validated_at"`
131+
ValidatedAt *time.Time `json:"validatedAt" db:"validated_at"`
132+
IpAddresses *pq.StringArray `json:"ipAddresses" db:"ip_addresses"`
132133
}
133134

134135
type RefreshTokenPayload struct {

pkg/repository/transaction.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ func (t transaction[T]) Create(insert model.Transaction) (model.Transaction, err
2525
m := model.Transaction{}
2626
// TODO: Add platform_id once it becomes available
2727
rows, err := t.store.NamedQuery(`
28-
INSERT INTO transaction (status, network_id, device_id, platform_id)
29-
VALUES(:status, :network_id, :device_id, :platform_id) RETURNING id`, insert)
28+
INSERT INTO transaction (status, network_id, device_id, platform_id, ip_address)
29+
VALUES(:status, :network_id, :device_id, :platform_id, :ip_address) RETURNING id`, insert)
3030
if err != nil {
3131
return m, common.StringError(err)
3232
}

pkg/service/device.go

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
type Device interface {
1515
VerifyDevice(encrypted string) error
16+
UpsertDeviceIP(deviceId string, Ip string) (err error)
1617
CreateDeviceIfNeeded(userID, visitorID, requestID string) (model.Device, error)
1718
CreateUnknownDevice(userID string) (model.Device, error)
1819
InvalidateUnknownDevice(device model.Device) error
@@ -27,30 +28,36 @@ func NewDevice(repos repository.Repositories, f Fingerprint) Device {
2728
return &device{repos, f}
2829
}
2930

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

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

46-
func (d device) CreateUnknownDevice(userID string) (model.Device, error) {
47-
visitor := FPVisitor{
48-
VisitorID: "unknown",
49-
Type: "unknown",
50-
UserAgent: "unknown",
46+
func (d device) UpsertDeviceIP(deviceId string, ip string) (err error) {
47+
device, err := d.repos.Device.GetById(deviceId)
48+
if err != nil {
49+
return
5150
}
52-
device, err := d.createDevice(userID, visitor, "an unknown device")
53-
return device, common.StringError(err)
51+
contains := common.SliceContains(device.IpAddresses, ip)
52+
if !contains {
53+
ipAddresses := append(device.IpAddresses, ip)
54+
updates := &model.DeviceUpdates{IpAddresses: &ipAddresses}
55+
err = d.repos.Device.Update(deviceId, updates)
56+
if err != nil {
57+
return
58+
}
59+
}
60+
return
5461
}
5562

5663
func (d device) CreateDeviceIfNeeded(userID, visitorID, requestID string) (model.Device, error) {
@@ -88,19 +95,39 @@ func (d device) CreateDeviceIfNeeded(userID, visitorID, requestID string) (model
8895
}
8996
}
9097

91-
func (d device) VerifyDevice(encrypted string) error {
92-
key := os.Getenv("STRING_ENCRYPTION_KEY")
93-
received, err := common.Decrypt[DeviceVerification](encrypted, key)
94-
if err != nil {
95-
return common.StringError(err)
98+
func (d device) CreateUnknownDevice(userID string) (model.Device, error) {
99+
visitor := FPVisitor{
100+
VisitorID: "unknown",
101+
Type: "unknown",
102+
UserAgent: "unknown",
96103
}
104+
device, err := d.createDevice(userID, visitor, "an unknown device")
105+
return device, common.StringError(err)
106+
}
97107

98-
now := time.Now()
99-
if now.Unix()-received.Timestamp > (60 * 15) {
100-
return common.StringError(errors.New("link expired"))
108+
func (d device) InvalidateUnknownDevice(device model.Device) error {
109+
if device.Fingerprint != "unknown" {
110+
return nil // only unknown devices can be invalidated
101111
}
102-
err = d.repos.Device.Update(received.DeviceID, model.DeviceUpdates{ValidatedAt: &now})
103-
return err
112+
113+
device.ValidatedAt = &time.Time{} // Zero time to set it to nil
114+
return d.repos.Device.Update(device.ID, device)
115+
}
116+
117+
func (d device) createDevice(userID string, visitor FPVisitor, description string) (model.Device, error) {
118+
addresses := pq.StringArray{}
119+
if visitor.IPAddress.String != "" {
120+
addresses = pq.StringArray{visitor.IPAddress.String}
121+
}
122+
123+
return d.repos.Device.Create(model.Device{
124+
UserID: userID,
125+
Fingerprint: visitor.VisitorID,
126+
Type: visitor.Type,
127+
IpAddresses: addresses,
128+
Description: description,
129+
LastUsedAt: time.Now(),
130+
})
104131
}
105132

106133
func (d device) getOrCreateUnknownDevice(userId, visitorId string) (model.Device, error) {
@@ -123,12 +150,3 @@ func (d device) getOrCreateUnknownDevice(userId, visitorId string) (model.Device
123150
func isDeviceValidated(device model.Device) bool {
124151
return device.ValidatedAt != nil && !device.ValidatedAt.IsZero()
125152
}
126-
127-
func (d device) InvalidateUnknownDevice(device model.Device) error {
128-
if device.Fingerprint != "unknown" {
129-
return nil // only unknown devices can be invalidated
130-
}
131-
132-
device.ValidatedAt = &time.Time{} // Zero time to set it to nil
133-
return d.repos.Device.Update(device.ID, device)
134-
}

0 commit comments

Comments
 (0)