Skip to content

Commit

Permalink
Simplified email registration (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-myles authored May 22, 2024
1 parent a963e59 commit 5bcdbf5
Show file tree
Hide file tree
Showing 93 changed files with 322 additions and 1,033 deletions.
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ linters-settings:

linters:
disable:
# Non adequate behaviour at 1.58 version.
- mnd
- tagalign
# TODO remove asap!
- gomoddirectives
Expand Down
1 change: 0 additions & 1 deletion auth/email_link/DDL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ CREATE TABLE IF NOT EXISTS email_link_sign_ins (
previously_issued_token_seq BIGINT DEFAULT 0 NOT NULL,
confirmation_code_wrong_attempts_count BIGINT DEFAULT 0 NOT NULL,
email TEXT NOT NULL,
otp TEXT NOT NULL,
confirmation_code TEXT,
user_id TEXT,
phone_number_to_email_migration_user_id TEXT,
Expand Down
14 changes: 3 additions & 11 deletions auth/email_link/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ type (
Client interface {
IceUserIDClient
SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (loginSession string, err error)
SignIn(ctx context.Context, emailLinkPayload, confirmationCode string) error
SignIn(ctx context.Context, loginFlowToken, confirmationCode string) (tokens *Tokens, emailConfirmed bool, err error)
RegenerateTokens(ctx context.Context, prevToken string) (tokens *Tokens, err error)
Status(ctx context.Context, loginSession string) (tokens *Tokens, emailConfirmed bool, err error)
UpdateMetadata(ctx context.Context, userID string, metadata *users.JSON) (*users.JSON, error)
}
IceUserIDClient interface {
Expand Down Expand Up @@ -120,18 +119,12 @@ type (
Email string `json:"email,omitempty" example:"[email protected]"`
DeviceUniqueID string `json:"deviceUniqueId,omitempty" example:"6FB988F3-36F4-433D-9C7C-555887E57EB2" db:"device_unique_id"`
}
magicLinkToken struct {
*jwt.RegisteredClaims
OTP string `json:"otp" example:"c8f64979-9cea-4649-a89a-35607e734e68"`
OldEmail string `json:"oldEmail,omitempty"`
NotifyEmail string `json:"notifyEmail,omitempty"`
DeviceUniqueID string `json:"deviceUniqueId,omitempty"`
}
loginFlowToken struct {
*jwt.RegisteredClaims
DeviceUniqueID string `json:"deviceUniqueId,omitempty"`
ConfirmationCode string `json:"confirmationCode,omitempty"`
ClientIP string `json:"clientIP,omitempty"` //nolint:tagliatelle //.
OldEmail string `json:"oldEmail,omitempty"`
NotifyEmail string `json:"notifyEmail,omitempty"`
LoginSessionNumber int64 `json:"loginSessionNumber,omitempty"`
}
emailLinkSignIn struct {
Expand All @@ -143,7 +136,6 @@ type (
UserID *string `json:"userId" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"`
PhoneNumberToEmailMigrationUserID *string `json:"-" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"`
Email string `json:"email,omitempty" example:"[email protected]"`
OTP string `json:"otp,omitempty" example:"207d0262-2554-4df9-b954-08cb42718b25"`
Language string `json:"language,omitempty" example:"en"`
DeviceUniqueID string `json:"deviceUniqueId,omitempty" example:"6FB988F3-36F4-433D-9C7C-555887E57EB2" db:"device_unique_id"`
ConfirmationCode string `json:"confirmationCode,omitempty" example:"123"`
Expand Down
6 changes: 3 additions & 3 deletions auth/email_link/email_modify.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ func (c *client) handleEmailModification(ctx context.Context, els *emailLinkSign
}
}
if notifyEmail != "" {
resetEmailOTP, now := generateOTP(), time.Now()
now := time.Now()
resetConfirmationCode := generateConfirmationCode()
uErr := c.upsertEmailLinkSignIn(ctx, oldEmail, els.DeviceUniqueID, resetEmailOTP, resetConfirmationCode, now)
uErr := c.upsertEmailLinkSignIn(ctx, oldEmail, els.DeviceUniqueID, resetConfirmationCode, now)
if uErr != nil {
return multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.resetEmailModification(ctx, usr.ID, oldEmail), "[reset] resetEmailModification failed for email:%v", oldEmail),
Expand All @@ -53,7 +53,7 @@ func (c *client) handleEmailModification(ctx context.Context, els *emailLinkSign
}
resetEmailPayload, rErr := c.generateMagicLinkPayload(
&loginID{Email: oldEmail, DeviceUniqueID: els.DeviceUniqueID},
newEmail, "", resetEmailOTP, now)
newEmail, now)
if rErr != nil {
return multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.resetEmailModification(ctx, usr.ID, oldEmail), "[reset] resetEmailModification failed for email:%v", oldEmail),
Expand Down
2 changes: 1 addition & 1 deletion auth/email_link/emaillink.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func (c *client) deleteOldLoginAttempts(ctx context.Context) error {
}

func (c *client) startOldLoginAttemptsCleaner(ctx context.Context) {
ticker := stdlibtime.NewTicker(stdlibtime.Duration(1+rand.Intn(24)) * stdlibtime.Minute) //nolint:gosec,gomnd // Not an issue.
ticker := stdlibtime.NewTicker(stdlibtime.Duration(1+rand.Intn(24)) * stdlibtime.Minute) //nolint:gosec,gomnd,mnd // Not an issue.
defer ticker.Stop()

for {
Expand Down
79 changes: 25 additions & 54 deletions auth/email_link/link_start_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
stdlibtime "time"

"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"

Expand Down Expand Up @@ -42,9 +41,8 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
return "", errors.Wrapf(vErr, "can't validate modification email for:%#v", oldID)
}
}
otp := generateOTP()
confirmationCode := generateConfirmationCode()
loginSession, err = c.generateLoginSession(&id, confirmationCode, clientIP, loginSessionNumber)
loginSession, err = c.generateLoginSession(&id, clientIP, oldEmail, loginSessionNumber)
if err != nil {
return "", errors.Wrap(err, "can't call generateLoginSession")
}
Expand All @@ -53,9 +51,9 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
return "", errors.Wrapf(ipErr, "failed increment login attempts for IP:%v (session num %v)", clientIP, loginSessionNumber)
}
}
if uErr := c.upsertEmailLinkSignIn(ctx, id.Email, id.DeviceUniqueID, otp, confirmationCode, now); uErr != nil {
if uErr := c.upsertEmailLinkSignIn(ctx, id.Email, id.DeviceUniqueID, confirmationCode, now); uErr != nil {
if errors.Is(uErr, ErrUserDuplicate) {
oldLoginSession, oErr := c.restoreOldLoginSession(ctx, &id, clientIP, loginSessionNumber)
oldLoginSession, oErr := c.restoreOldLoginSession(ctx, &id, clientIP, oldEmail, loginSessionNumber)
if oErr != nil {
return "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(oErr, "failed to calculate oldLoginSession"),
Expand All @@ -71,14 +69,7 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
errors.Wrapf(uErr, "failed to store/update email link sign ins for id:%#v", id),
).ErrorOrNil()
}
payload, pErr := c.generateMagicLinkPayload(&id, oldEmail, oldEmail, otp, now)
if pErr != nil {
return "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
errors.Wrapf(pErr, "can't generate magic link payload for id: %#v", id),
).ErrorOrNil()
}
if sErr := c.sendMagicLink(ctx, &id, oldEmail, payload, language); sErr != nil {
if sErr := c.sendConfirmationCode(ctx, &id, oldEmail, confirmationCode, language); sErr != nil {
return "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
errors.Wrapf(sErr, "can't send magic link for id:%#v", id),
Expand All @@ -88,15 +79,8 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
return loginSession, nil
}

func (c *client) restoreOldLoginSession(ctx context.Context, id *loginID, clientIP string, loginSessionNumber int64) (string, error) {
existingSignIn, dErr := c.getEmailLinkSignIn(ctx, id, true)
if dErr != nil {
return "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
errors.Wrapf(dErr, "can't get email link sign in information by:%#v", id),
).ErrorOrNil()
}
oldLoginSession, dErr := c.generateLoginSession(id, existingSignIn.ConfirmationCode, clientIP, loginSessionNumber)
func (c *client) restoreOldLoginSession(ctx context.Context, id *loginID, clientIP, oldEmail string, loginSessionNumber int64) (string, error) {
oldLoginSession, dErr := c.generateLoginSession(id, clientIP, oldEmail, loginSessionNumber)
if dErr != nil {
return "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
Expand Down Expand Up @@ -164,30 +148,29 @@ func (c *client) validateEmailModification(ctx context.Context, newEmail string,
return nil
}

func (c *client) sendMagicLink(ctx context.Context, id *loginID, oldEmail, payload, language string) error {
authLink := c.getAuthLink(payload, language)
func (c *client) sendConfirmationCode(ctx context.Context, id *loginID, oldEmail, confirmationCode, language string) error {
var emailType string
if oldEmail != "" {
emailType = modifyEmailType
} else {
emailType = signInEmailType
}

return errors.Wrapf(c.sendEmailWithType(ctx, emailType, id.Email, language, authLink), "failed to send validation email for id:%#v", id)
return errors.Wrapf(c.sendEmailWithType(ctx, emailType, id.Email, language, confirmationCode), "failed to send validation email for id:%#v", id)
}

func (c *client) sendEmailWithType(ctx context.Context, emailType, toEmail, language, link string) error {
func (c *client) sendEmailWithType(ctx context.Context, emailType, toEmail, language, confirmationCode string) error {
var tmpl *emailTemplate
tmpl, ok := allEmailLinkTemplates[emailType][language]
if !ok {
tmpl = allEmailLinkTemplates[emailType][defaultLanguage]
}
data := struct {
Email string
Link string
Email string
ConfirmationCode string
}{
Email: toEmail,
Link: link,
Email: toEmail,
ConfirmationCode: confirmationCode,
}

return errors.Wrapf(c.emailClient.Send(ctx, &email.Parcel{
Expand All @@ -206,30 +189,27 @@ func (c *client) sendEmailWithType(ctx context.Context, emailType, toEmail, lang
}), "failed to send email with type:%v for user with email:%v", emailType, toEmail)
}

//nolint:revive,lll // .
func (c *client) upsertEmailLinkSignIn(ctx context.Context, toEmail, deviceUniqueID, otp, code string, now *time.Time) error {
//nolint:lll // .
func (c *client) upsertEmailLinkSignIn(ctx context.Context, toEmail, deviceUniqueID, code string, now *time.Time) error {
confirmationCodeWrongAttempts := 0
params := []any{now.Time, toEmail, deviceUniqueID, otp, code, confirmationCodeWrongAttempts, userIDForPhoneNumberToEmailMigration(ctx)}
params := []any{now.Time, toEmail, deviceUniqueID, code, confirmationCodeWrongAttempts, userIDForPhoneNumberToEmailMigration(ctx)}
sql := fmt.Sprintf(`INSERT INTO email_link_sign_ins (
created_at,
email,
device_unique_id,
otp,
confirmation_code,
confirmation_code_wrong_attempts_count,
phone_number_to_email_migration_user_id)
VALUES ($1, $2, $3, $4, $5, $6, NULLIF($7,''))
VALUES ($1, $2, $3, $4, $5, NULLIF($6,''))
ON CONFLICT (email, device_unique_id) DO UPDATE
SET otp = EXCLUDED.otp,
created_at = EXCLUDED.created_at,
SET created_at = EXCLUDED.created_at,
confirmation_code = EXCLUDED.confirmation_code,
confirmation_code_wrong_attempts_count = EXCLUDED.confirmation_code_wrong_attempts_count,
phone_number_to_email_migration_user_id = COALESCE(NULLIF(EXCLUDED.phone_number_to_email_migration_user_id,''),email_link_sign_ins.phone_number_to_email_migration_user_id),
email_confirmed_at = null,
user_id = null
WHERE (extract(epoch from email_link_sign_ins.created_at)::bigint/%[1]v) != (extract(epoch from EXCLUDED.created_at::timestamp)::bigint/%[1]v)
AND (email_link_sign_ins.otp != EXCLUDED.otp
OR email_link_sign_ins.confirmation_code != EXCLUDED.confirmation_code
AND (email_link_sign_ins.confirmation_code != EXCLUDED.confirmation_code
OR email_link_sign_ins.confirmation_code_wrong_attempts_count != EXCLUDED.confirmation_code_wrong_attempts_count)`,
uint64(duplicatedSignInRequestsInLessThan/stdlibtime.Second))
rowsInserted, err := storage.Exec(ctx, c.db, sql, params...)
Expand Down Expand Up @@ -259,8 +239,8 @@ func (c *client) upsertIPLoginAttempt(ctx context.Context, id *loginID, clientIP
return nil
}

func (c *client) generateMagicLinkPayload(id *loginID, oldEmail, notifyEmail, otp string, now *time.Time) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, magicLinkToken{
func (c *client) generateMagicLinkPayload(id *loginID, oldEmail string, now *time.Time) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, loginFlowToken{
RegisteredClaims: &jwt.RegisteredClaims{
Issuer: jwtIssuer,
Subject: id.Email,
Expand All @@ -269,24 +249,18 @@ func (c *client) generateMagicLinkPayload(id *loginID, oldEmail, notifyEmail, ot
NotBefore: jwt.NewNumericDate(*now.Time),
IssuedAt: jwt.NewNumericDate(*now.Time),
},
OTP: otp,
OldEmail: oldEmail,
NotifyEmail: notifyEmail,
DeviceUniqueID: id.DeviceUniqueID,
})
payload, err := token.SignedString([]byte(c.cfg.EmailValidation.JwtSecret))
if err != nil {
return "", errors.Wrapf(err, "can't generate link payload for id:%#v,otp:%v,now:%v", id, otp, now)
return "", errors.Wrapf(err, "can't generate link payload for id:%#v,now:%v", id, now)
}

return payload, nil
}

func (c *client) getAuthLink(token, language string) string {
return fmt.Sprintf("%s?token=%s&lang=%s", c.cfg.EmailValidation.AuthLink, token, language)
}

func (c *client) generateLoginSession(id *loginID, confirmationCode, clientIP string, loginSessionNumber int64) (string, error) {
func (c *client) generateLoginSession(id *loginID, clientIP, oldEmail string, loginSessionNumber int64) (string, error) {
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, loginFlowToken{
RegisteredClaims: &jwt.RegisteredClaims{
Expand All @@ -298,8 +272,9 @@ func (c *client) generateLoginSession(id *loginID, confirmationCode, clientIP st
IssuedAt: jwt.NewNumericDate(*now.Time),
},
DeviceUniqueID: id.DeviceUniqueID,
ConfirmationCode: confirmationCode,
LoginSessionNumber: loginSessionNumber,
OldEmail: oldEmail,
NotifyEmail: oldEmail,
ClientIP: clientIP,
})
payload, err := token.SignedString([]byte(c.cfg.LoginSession.JwtSecret))
Expand All @@ -310,10 +285,6 @@ func (c *client) generateLoginSession(id *loginID, confirmationCode, clientIP st
return payload, nil
}

func generateOTP() string {
return uuid.NewString()
}

func generateConfirmationCode() string {
result, err := rand.Int(rand.Reader, big.NewInt(999)) //nolint:gomnd // It's max value.
log.Panic(err, "random wrong")
Expand Down
Loading

0 comments on commit 5bcdbf5

Please sign in to comment.