web/satellite, satellite/console: reworked registration email validation

Reworked email validation for new users (for old users trying to login or reset password validation remains the same).
Regular expression was built according to RFC 5322 and then extended to include international characters.

Change-Id: Id0224fee21a1ec0f8a2dcca5b8431197dee6b9d3
This commit is contained in:
Vitalii Shpital 2022-02-18 14:52:23 +02:00
parent 8b0988708a
commit 60b209e47d
6 changed files with 111 additions and 31 deletions

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"net/http" "net/http"
"regexp" "regexp"
"strings"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -182,6 +183,15 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
return return
} }
// trim leading and trailing spaces of email address.
registerData.Email = strings.TrimSpace(registerData.Email)
isValidEmail := validateEmail(registerData.Email)
if !isValidEmail {
a.serveJSONError(w, console.ErrValidation.Wrap(errs.New("Invalid email.")))
return
}
// remove special characters from submitted name so that malicious link cannot be injected into verification or password reset emails. // remove special characters from submitted name so that malicious link cannot be injected into verification or password reset emails.
registerData.FullName = replaceURLCharacters(registerData.FullName) registerData.FullName = replaceURLCharacters(registerData.FullName)
registerData.ShortName = replaceURLCharacters(registerData.ShortName) registerData.ShortName = replaceURLCharacters(registerData.ShortName)
@ -323,6 +333,15 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
} }
} }
// validateEmail validates email to have correct form and syntax.
func validateEmail(email string) bool {
// This regular expression was built according to RFC 5322 and then extended to include international characters.
re := regexp.MustCompile(`^(?:[a-z0-9\p{L}!#$%&'*+/=?^_{|}~\x60-]+(?:\.[a-z0-9\p{L}!#$%&'*+/=?^_{|}~\x60-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9\p{L}](?:[a-z0-9\p{L}-]*[a-z0-9\p{L}])?\.)+[a-z0-9\p{L}](?:[a-z\p{L}]*[a-z\p{L}])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9\p{L}-]*[a-z0-9\p{L}]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$`)
match := re.MatchString(email)
return match
}
// loadSession looks for a cookie for the session id. // loadSession looks for a cookie for the session id.
// this cookie is set from the reverse proxy if the user opts into cookies from Storj. // this cookie is set from the reverse proxy if the user opts into cookies from Storj.
func loadSession(req *http.Request) string { func loadSession(req *http.Request) string {

View File

@ -72,7 +72,7 @@ func TestAuth_Register(t *testing.T) {
}{ }{
FullName: "testuser" + strconv.Itoa(i), FullName: "testuser" + strconv.Itoa(i),
ShortName: "test", ShortName: "test",
Email: "user@test" + strconv.Itoa(i), Email: "user@test" + strconv.Itoa(i) + ".com",
Partner: test.Partner, Partner: test.Partner,
Password: "abc123", Password: "abc123",
IsProfessional: true, IsProfessional: true,

View File

@ -47,7 +47,6 @@ func TestSignup_Content(t *testing.T) {
uitest.Run(t, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet, browser *rod.Browser) { uitest.Run(t, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet, browser *rod.Browser) {
signupPageURL := planet.Satellites[0].ConsoleURL() + "/signup" signupPageURL := planet.Satellites[0].ConsoleURL() + "/signup"
fullName := "John Doe" fullName := "John Doe"
invalidEmailAddress := "test@email"
password := "qazwsx123" password := "qazwsx123"
page := openPage(browser, signupPageURL) page := openPage(browser, signupPageURL)
@ -65,18 +64,52 @@ func TestSignup_Content(t *testing.T) {
// User signup with invalid email // User signup with invalid email
page.MustElement("[aria-roledescription=name] input").MustInput(fullName) page.MustElement("[aria-roledescription=name] input").MustInput(fullName)
page.MustElement("[aria-roledescription=email] input").MustInput(invalidEmailAddress)
page.MustElement("[aria-roledescription=password] input").MustInput(password) page.MustElement("[aria-roledescription=password] input").MustInput(password)
page.MustElement("[aria-roledescription=retype-password] input").MustInput(password) page.MustElement("[aria-roledescription=retype-password] input").MustInput(password)
page.MustElement(".checkmark").MustClick() page.MustElement(".checkmark").MustClick()
page.Keyboard.MustPress(input.Enter)
waitVueTick(page)
invalidEmailMessage := page.MustElement("[aria-roledescription=email] [aria-roledescription=error-text]").MustText() invalidEmailAddresses := []string{
require.Contains(t, invalidEmailMessage, "Invalid Email") "t@t@t.t",
"test",
"t@!t.t1",
"t@#t.t",
"t@$t.t",
"t%t.t",
"t@^t.t",
"t@&t.t",
"t@*t.t",
"t@(t.t",
"t@)t.t",
"t@=t.t",
"t@[t.t",
"t@]t.t",
"t@{t.t",
"t@}t.t",
"t@/t.t",
"t@\\t.t",
"t@|t.t",
"t@:t.t",
"t@;t.t",
"t@,t.t",
"t@\"t.t",
"t@'t.t",
"t@<t.t",
"t@>t.t",
"t@_t.t",
"t@?t.t",
}
for _, e := range invalidEmailAddresses {
page.MustElement("[aria-roledescription=email] input").MustInput(e)
page.Keyboard.MustPress(input.Enter)
waitVueTick(page)
invalidEmailMessage := page.MustElement("[aria-roledescription=email] [aria-roledescription=error-text]").MustText()
require.Contains(t, invalidEmailMessage, "Invalid Email")
page.MustElement("[aria-roledescription=email] input").MustSelectAllText().MustInput("")
}
// User signup with no email or password // User signup with no email or password
page.MustElement("[aria-roledescription=email] input").MustSelectAllText().MustInput("")
page.MustElement("[aria-roledescription=password] input").MustSelectAllText().MustInput("") page.MustElement("[aria-roledescription=password] input").MustSelectAllText().MustInput("")
page.MustElement("[aria-roledescription=retype-password] input").MustSelectAllText().MustInput("") page.MustElement("[aria-roledescription=retype-password] input").MustSelectAllText().MustInput("")
page.Keyboard.MustPress(input.Enter) page.Keyboard.MustPress(input.Enter)
@ -86,6 +119,29 @@ func TestSignup_Content(t *testing.T) {
require.Contains(t, invalidEmailMessage1, "Invalid Email") require.Contains(t, invalidEmailMessage1, "Invalid Email")
invalidPasswordMessage := page.MustElement("[aria-roledescription=password] [aria-roledescription=error-text]").MustText() invalidPasswordMessage := page.MustElement("[aria-roledescription=password] [aria-roledescription=error-text]").MustText()
require.Contains(t, invalidPasswordMessage, "Invalid Password") require.Contains(t, invalidPasswordMessage, "Invalid Password")
validEmailAddresses := []string{
"тест@тест.тест ",
" अजअज@अज.अज",
" test@email.test ",
}
for i, e := range validEmailAddresses {
page.MustElement("[aria-roledescription=name] input").MustInput(fullName)
page.MustElement("[aria-roledescription=password] input").MustInput(password)
page.MustElement("[aria-roledescription=retype-password] input").MustInput(password)
page.MustElement("[aria-roledescription=email] input").MustInput(e)
if i != 0 {
page.MustElement(".checkmark").MustClick()
}
page.Keyboard.MustPress(input.Enter)
waitVueTick(page)
successTitle := page.MustElement("[aria-roledescription=title]").MustText()
require.Contains(t, successTitle, "You're almost there!")
page.MustElement("[href=\"/login\"]").MustClick()
page.MustElement("[href=\"/signup\"]").MustClick()
}
}) })
} }

View File

@ -82,7 +82,7 @@ export default class ForgotPassword extends Vue {
* Sets the email field to the given value. * Sets the email field to the given value.
*/ */
public setEmail(value: string): void { public setEmail(value: string): void {
this.email = value; this.email = value.trim();
this.emailError = ''; this.emailError = '';
} }
@ -151,7 +151,7 @@ export default class ForgotPassword extends Vue {
* Returns whether the email address is properly structured. * Returns whether the email address is properly structured.
*/ */
private validateFields(): boolean { private validateFields(): boolean {
const isEmailValid = Validator.email(this.email.trim()); const isEmailValid = Validator.email(this.email);
if (!isEmailValid) { if (!isEmailValid) {
this.emailError = 'Invalid Email'; this.emailError = 'Invalid Email';

View File

@ -214,14 +214,14 @@ export default class Login extends Vue {
public onConfirmInput(value: string): void { public onConfirmInput(value: string): void {
this.isMFAError = false; this.isMFAError = false;
this.isRecoveryCodeState ? this.recoveryCode = value : this.passcode = value; this.isRecoveryCodeState ? this.recoveryCode = value.trim() : this.passcode = value.trim();
} }
/** /**
* Sets email string on change. * Sets email string on change.
*/ */
public setEmail(value: string): void { public setEmail(value: string): void {
this.email = value; this.email = value.trim();
this.emailError = ''; this.emailError = '';
} }
@ -279,7 +279,7 @@ export default class Login extends Vue {
} }
try { try {
await this.auth.token(this.email.trim(), this.password, this.passcode.trim(), this.recoveryCode.trim()); await this.auth.token(this.email, this.password, this.passcode, this.recoveryCode);
} catch (error) { } catch (error) {
if (error instanceof ErrorMFARequired) { if (error instanceof ErrorMFARequired) {
if (this.isMFARequired) this.isMFAError = true; if (this.isMFARequired) this.isMFAError = true;
@ -319,7 +319,7 @@ export default class Login extends Vue {
private validateFields(): boolean { private validateFields(): boolean {
let isNoErrors = true; let isNoErrors = true;
if (!Validator.email(this.email.trim())) { if (!Validator.email(this.email)) {
this.emailError = 'Invalid Email'; this.emailError = 'Invalid Email';
isNoErrors = false; isNoErrors = false;
} }

View File

@ -252,10 +252,9 @@ import { Validator } from '@/utils/validation';
export default class RegisterArea extends Vue { export default class RegisterArea extends Vue {
private readonly user = new User(); private readonly user = new User();
// tardigrade logic // DCS logic
private secret = ''; private secret = '';
private userId = '';
private isTermsAccepted = false; private isTermsAccepted = false;
private password = ''; private password = '';
private repeatedPassword = ''; private repeatedPassword = '';
@ -286,7 +285,7 @@ export default class RegisterArea extends Vue {
public isPasswordStrengthShown = false; public isPasswordStrengthShown = false;
// tardigrade logic // DCS logic
public isDropdownShown = false; public isDropdownShown = false;
// Employee Count dropdown options // Employee Count dropdown options
@ -391,7 +390,7 @@ export default class RegisterArea extends Vue {
* Sets user's password field from value string. * Sets user's password field from value string.
*/ */
public setPassword(value: string): void { public setPassword(value: string): void {
this.user.password = value.trim(); this.user.password = value;
this.password = value; this.password = value;
this.passwordError = ''; this.passwordError = '';
} }
@ -432,13 +431,6 @@ export default class RegisterArea extends Vue {
return this.$store.state.appStateModule.couponCodeSigunpUIEnabled; return this.$store.state.appStateModule.couponCodeSigunpUIEnabled;
} }
/**
* Returns the email of the created user.
*/
public get email(): string {
return this.user.email;
}
/** /**
* Sets user's company name field from value string. * Sets user's company name field from value string.
*/ */
@ -493,12 +485,12 @@ export default class RegisterArea extends Vue {
private validateFields(): boolean { private validateFields(): boolean {
let isNoErrors = true; let isNoErrors = true;
if (!this.user.fullName.trim()) { if (!this.user.fullName) {
this.fullNameError = 'Invalid Name'; this.fullNameError = 'Name can\'t be empty';
isNoErrors = false; isNoErrors = false;
} }
if (!Validator.email(this.user.email.trim())) { if (!this.isEmailValid()) {
this.emailError = 'Invalid Email'; this.emailError = 'Invalid Email';
isNoErrors = false; isNoErrors = false;
} }
@ -510,17 +502,17 @@ export default class RegisterArea extends Vue {
if (this.isProfessional) { if (this.isProfessional) {
if (!this.user.companyName.trim()) { if (!this.user.companyName) {
this.companyNameError = 'No Company Name filled in'; this.companyNameError = 'No Company Name filled in';
isNoErrors = false; isNoErrors = false;
} }
if (!this.user.position.trim()) { if (!this.user.position) {
this.positionError = 'No Position filled in'; this.positionError = 'No Position filled in';
isNoErrors = false; isNoErrors = false;
} }
if (!this.user.employeeCount.trim()) { if (!this.user.employeeCount) {
this.employeeCountError = 'No Company Size filled in'; this.employeeCountError = 'No Company Size filled in';
isNoErrors = false; isNoErrors = false;
} }
@ -553,6 +545,19 @@ export default class RegisterArea extends Vue {
return (navigator['brave'] && await navigator['brave'].isBrave() || false) return (navigator['brave'] && await navigator['brave'].isBrave() || false)
} }
/**
* Validates email string.
* We'll have this email validation for new users instead of using regular Validator.email method because of backwards compatibility.
* We don't want to block old users who managed to create and verify their accounts with some weird email addresses.
*/
private isEmailValid(): boolean {
// This regular expression fulfills our needs to validate international emails.
// It was built according to RFC 5322 and then extended to include international characters.
// eslint-disable-next-line no-misleading-character-class
const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u0080-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u137F\u1380-\u139F\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1B00-\u1B7F\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u1E00-\u1EFF\u1F00-\u1FFF\u20D0-\u20FF\u2100-\u214F\u2C00-\u2C5F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2F00-\u2FDF\u2FF0-\u2FFF\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA700-\uA71F\uA800-\uA82F\uA840-\uA87F\uAC00-\uD7AF\uF900-\uFAFF]+\.)+[a-zA-Z\u0080-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u137F\u1380-\u139F\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1B00-\u1B7F\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u1E00-\u1EFF\u1F00-\u1FFF\u20D0-\u20FF\u2100-\u214F\u2C00-\u2C5F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2F00-\u2FDF\u2FF0-\u2FFF\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA700-\uA71F\uA800-\uA82F\uA840-\uA87F\uAC00-\uD7AF\uF900-\uFAFF]{2,}))$/;
return regex.test(this.user.email);
}
/** /**
* Creates user and toggles successful registration area visibility. * Creates user and toggles successful registration area visibility.
*/ */