Skip to content

STR-357 :: If fingerprint analytics fails, continue anyways #99

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 11 commits into from
Jan 31, 2023
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func loginRoute(services service.Services, e *echo.Echo) {
}

func verificationRoute(services service.Services, e *echo.Echo) {
handler := handler.NewVerification(e, services.Verification)
handler := handler.NewVerification(e, services.Verification, services.Device)
handler.RegisterRoutes(e.Group("/verification"))
}

Expand Down
7 changes: 6 additions & 1 deletion api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic
verificationRepos := repository.Repositories{Contact: repos.Contact, User: repos.User, Device: repos.Device}
verification := service.NewVerification(verificationRepos)

auth := service.NewAuth(repos, fingerprint, verification)
// device service
deviceRepos := repository.Repositories{Device: repos.Device}
device := service.NewDevice(deviceRepos, fingerprint)

auth := service.NewAuth(repos, verification, device)
apiKey := service.NewAPIKeyStrategy(repos.Auth)
cost := service.NewCost(config.Redis)
executor := service.NewExecutor()
Expand All @@ -61,5 +65,6 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic
Transaction: transaction,
User: user,
Verification: verification,
Device: device,
}
}
7 changes: 7 additions & 0 deletions api/handler/http_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,10 @@ func LinkExpired(c echo.Context, message ...string) error {
}
return c.JSON(http.StatusForbidden, JSONError{Message: "Forbidden", Code: "LINK_EXPIRED"})
}

func InvalidEmail(c echo.Context, message ...string) error {
if len(message) > 0 {
return c.JSON(http.StatusUnprocessableEntity, JSONError{Message: strings.Join(message, " "), Code: "INVALID_EMAIL"})
}
return c.JSON(http.StatusUnprocessableEntity, JSONError{Message: "Invalid email", Code: "INVALID_EMAIL"})
}
10 changes: 7 additions & 3 deletions api/handler/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,14 @@ func (l login) VerifySignature(c echo.Context) error {
body.Nonce = string(decodedNonce)

resp, err := l.Service.VerifySignedPayload(body)
if err != nil && strings.Contains(err.Error(), "unknown device") {
return Unprocessable(c)
}
if err != nil {
if strings.Contains(err.Error(), "unknown device") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of checking the error is fine, but we can also check the top level error like so:

Suggested change
if strings.Contains(err.Error(), "unknown device") {
if err != nil && errors.Cause(err).Error() == "unknown device" {

Which you can see on api/handler/quotes.go:41

return Unprocessable(c)
}
if strings.Contains(err.Error(), "invalid email") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea here as above. You could also say:

Suggested change
if strings.Contains(err.Error(), "invalid email") {
if errors.Cause(err).Error() == "invalid email" {

return InvalidEmail(c)
}

LogStringError(c, err, "login: verify signature")
return BadRequestError(c, "Invalid Payload")
}
Expand Down
11 changes: 6 additions & 5 deletions api/handler/verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ type Verification interface {
}

type verification struct {
service service.Verification
group *echo.Group
service service.Verification
deviceService service.Device
group *echo.Group
}

func NewVerification(route *echo.Echo, service service.Verification) Verification {
return &verification{service, nil}
func NewVerification(route *echo.Echo, service service.Verification, deviceService service.Device) Verification {
return &verification{service, deviceService, nil}
}

func (v verification) VerifyEmail(c echo.Context) error {
Expand All @@ -39,7 +40,7 @@ func (v verification) VerifyEmail(c echo.Context) error {

func (v verification) VerifyDevice(c echo.Context) error {
token := c.QueryParam("token")
err := v.service.VerifyDevice(token)
err := v.deviceService.VerifyDevice(token)
if err != nil {
LogStringError(c, err, "verification: device verification")
return BadRequestError(c)
Expand Down
1 change: 1 addition & 0 deletions pkg/model/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type User struct {
FirstName string `json:"firstName" db:"first_name"`
MiddleName string `json:"middleName" db:"middle_name"`
LastName string `json:"lastName" db:"last_name"`
Email string `json:"email"`
}

// See PLATFORM in Migrations 0005
Expand Down
6 changes: 3 additions & 3 deletions pkg/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ type WalletSignaturePayload struct {
}

type FingerprintPayload struct {
VisitorID string `json:"visitorId" validate:"required"`
RequestID string `json:"requestId" validate:"required"`
VisitorID string `json:"visitorId"`
RequestID string `json:"requestId"`
}

type WalletSignaturePayloadSigned struct {
Nonce string `json:"nonce" validate:"required"`
Signature string `json:"signature" validate:"required"`
Fingerprint FingerprintPayload `json:"fingerprint" validate:"required"`
Fingerprint FingerprintPayload `json:"fingerprint"`
}
81 changes: 38 additions & 43 deletions pkg/service/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/String-xyz/string-api/pkg/repository"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -51,21 +50,21 @@ type Auth interface {
// if signaure is valid it returns a JWT to authenticate the user
VerifySignedPayload(model.WalletSignaturePayloadSigned) (UserCreateResponse, error)

GenerateJWT(model.Device) (JWT, error)
GenerateJWT(string, ...model.Device) (JWT, error)
ValidateAPIKey(key string) bool
RefreshToken(token string, walletAddress string) (UserCreateResponse, error)
InvalidateRefreshToken(token string) error
}

type auth struct {
repos repository.Repositories
fingerprint Fingerprint
verification Verification
device Device
}

// reusing UserRepos here
func NewAuth(r repository.Repositories, f Fingerprint, v Verification) Auth {
return &auth{r, f, v}
func NewAuth(r repository.Repositories, v Verification, d Device) Auth {
return &auth{r, v, d}
}

func (a auth) PayloadToSign(walletAddress string) (SignablePayload, error) {
Expand Down Expand Up @@ -107,65 +106,49 @@ func (a auth) VerifySignedPayload(request model.WalletSignaturePayloadSigned) (U
return resp, common.StringError(err)
}

created, device, err := a.createDeviceIfNeeded(user.ID, request.Fingerprint.VisitorID, request.Fingerprint.RequestID)
if err != nil {
user.Email = getValidatedEmailOrEmpty(a.repos.Contact, user.ID)

device, err := a.device.CreateDeviceIfNeeded(user.ID, request.Fingerprint.VisitorID, request.Fingerprint.RequestID)
if err != nil && !strings.Contains(err.Error(), "not found") {
return resp, common.StringError(err)
}

if created || device.ValidatedAt == nil {
go a.verification.SendDeviceVerification(user.ID, device.ID, device.Description)
// Send verification email if device is unknown and user has a validated email
if user.Email != "" && !isDeviceValidated(device) {
go a.verification.SendDeviceVerification(user.ID, user.Email, device.ID, device.Description)
return resp, common.StringError(errors.New("unknown device"))
}

// Create the JWT
jwt, err := a.GenerateJWT(device)
jwt, err := a.GenerateJWT(user.ID, device)
if err != nil {
return resp, common.StringError(err)
}
return UserCreateResponse{JWT: jwt, User: user}, nil
}

func (a auth) createDeviceIfNeeded(userID, visitorID, requestID string) (bool, model.Device, error) {
device, err := a.repos.Device.GetByUserIdAndFingerprint(userID, visitorID)
if err == nil {
return false, device, nil
}
// create device only if the error is not found
if err != nil && err == repository.ErrNotFound {
visitor, fpErr := a.fingerprint.GetVisitor(visitorID, requestID)
if fpErr != nil {
return false, model.Device{}, common.StringError(fpErr)
}
device, dErr := a.createDevice(userID, visitor)
return dErr == nil, device, dErr
// Invalidate device if it is unknown and was validated so it cannot be used again
err = a.device.InvalidateUnknownDevice(device)
if err != nil {
return resp, common.StringError(err)
}

return false, device, common.StringError(err)
}

func (a auth) createDevice(userID string, visitor model.FPVisitor) (model.Device, error) {
return a.repos.Device.Create(model.Device{
UserID: userID,
Fingerprint: visitor.VisitorID,
Type: visitor.Type,
IpAddresses: pq.StringArray{visitor.IPAddress},
Description: visitor.UserAgent,
LastUsedAt: time.Now(),
ValidatedAt: nil,
})
return UserCreateResponse{JWT: jwt, User: user}, nil
}

// GenerateJWT generates a jwt token and a refresh token which is saved on redis
func (a auth) GenerateJWT(m model.Device) (JWT, error) {
func (a auth) GenerateJWT(userId string, m ...model.Device) (JWT, error) {
claims := JWTClaims{}
refreshToken := uuidWithoutHyphens()
t := &JWT{
IssuedAt: time.Now(),
ExpAt: time.Now().Add(time.Minute * 15),
}

claims.DeviceId = m.ID
claims.UserId = m.UserID
// set device id if available
if len(m) > 0 {
claims.DeviceId = m[0].ID
}

claims.UserId = userId
claims.ExpiresAt = t.ExpAt.Unix()
claims.IssuedAt = t.IssuedAt.Unix()
// replace this signing method with RSA or something similar
Expand All @@ -177,7 +160,7 @@ func (a auth) GenerateJWT(m model.Device) (JWT, error) {
t.Token = signed

// create and save
refreshObj, err := a.repos.Auth.CreateJWTRefresh(common.ToSha256(refreshToken), m.UserID)
refreshObj, err := a.repos.Auth.CreateJWTRefresh(common.ToSha256(refreshToken), userId)
if err != nil {
return *t, err
}
Expand Down Expand Up @@ -240,7 +223,7 @@ func (a auth) RefreshToken(refreshToken string, walletAddress string) (UserCreat
}

// create new jwt
jwt, err := a.GenerateJWT(device)
jwt, err := a.GenerateJWT(userId, device)
if err != nil {
return resp, common.StringError(err)
}
Expand All @@ -256,6 +239,9 @@ func (a auth) RefreshToken(refreshToken string, walletAddress string) (UserCreat
if err != nil {
return resp, common.StringError(err)
}

// get email
user.Email = getValidatedEmailOrEmpty(a.repos.Contact, user.ID)
resp.User = user

return resp, nil
Expand Down Expand Up @@ -295,3 +281,12 @@ func uuidWithoutHyphens() string {
s := uuid.New().String()
return strings.Replace(s, "-", "", -1)
}

func getValidatedEmailOrEmpty(contactRepo repository.Contact, userId string) string {
contact, err := contactRepo.GetByUserIdAndStatus(userId, "validated")
if err != nil {
return ""
}

return contact.Data
}
1 change: 1 addition & 0 deletions pkg/service/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type Services struct {
Transaction Transaction
User User
Verification Verification
Device Device
}
Loading