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"
"net/http"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
@ -182,6 +183,15 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
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.
registerData.FullName = replaceURLCharacters(registerData.FullName)
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.
// this cookie is set from the reverse proxy if the user opts into cookies from Storj.
func loadSession(req *http.Request) string {

View File

@ -72,7 +72,7 @@ func TestAuth_Register(t *testing.T) {
}{
FullName: "testuser" + strconv.Itoa(i),
ShortName: "test",
Email: "user@test" + strconv.Itoa(i),
Email: "user@test" + strconv.Itoa(i) + ".com",
Partner: test.Partner,
Password: "abc123",
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) {
signupPageURL := planet.Satellites[0].ConsoleURL() + "/signup"
fullName := "John Doe"
invalidEmailAddress := "test@email"
password := "qazwsx123"
page := openPage(browser, signupPageURL)
@ -65,18 +64,52 @@ func TestSignup_Content(t *testing.T) {
// User signup with invalid email
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=retype-password] input").MustInput(password)
page.MustElement(".checkmark").MustClick()
page.Keyboard.MustPress(input.Enter)
waitVueTick(page)
invalidEmailMessage := page.MustElement("[aria-roledescription=email] [aria-roledescription=error-text]").MustText()
require.Contains(t, invalidEmailMessage, "Invalid Email")
invalidEmailAddresses := []string{
"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
page.MustElement("[aria-roledescription=email] input").MustSelectAllText().MustInput("")
page.MustElement("[aria-roledescription=password] input").MustSelectAllText().MustInput("")
page.MustElement("[aria-roledescription=retype-password] input").MustSelectAllText().MustInput("")
page.Keyboard.MustPress(input.Enter)
@ -86,6 +119,29 @@ func TestSignup_Content(t *testing.T) {
require.Contains(t, invalidEmailMessage1, "Invalid Email")
invalidPasswordMessage := page.MustElement("[aria-roledescription=password] [aria-roledescription=error-text]").MustText()
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.
*/
public setEmail(value: string): void {
this.email = value;
this.email = value.trim();
this.emailError = '';
}
@ -151,7 +151,7 @@ export default class ForgotPassword extends Vue {
* Returns whether the email address is properly structured.
*/
private validateFields(): boolean {
const isEmailValid = Validator.email(this.email.trim());
const isEmailValid = Validator.email(this.email);
if (!isEmailValid) {
this.emailError = 'Invalid Email';

View File

@ -214,14 +214,14 @@ export default class Login extends Vue {
public onConfirmInput(value: string): void {
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.
*/
public setEmail(value: string): void {
this.email = value;
this.email = value.trim();
this.emailError = '';
}
@ -279,7 +279,7 @@ export default class Login extends Vue {
}
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) {
if (error instanceof ErrorMFARequired) {
if (this.isMFARequired) this.isMFAError = true;
@ -319,7 +319,7 @@ export default class Login extends Vue {
private validateFields(): boolean {
let isNoErrors = true;
if (!Validator.email(this.email.trim())) {
if (!Validator.email(this.email)) {
this.emailError = 'Invalid Email';
isNoErrors = false;
}

View File

@ -252,10 +252,9 @@ import { Validator } from '@/utils/validation';
export default class RegisterArea extends Vue {
private readonly user = new User();
// tardigrade logic
// DCS logic
private secret = '';
private userId = '';
private isTermsAccepted = false;
private password = '';
private repeatedPassword = '';
@ -286,7 +285,7 @@ export default class RegisterArea extends Vue {
public isPasswordStrengthShown = false;
// tardigrade logic
// DCS logic
public isDropdownShown = false;
// Employee Count dropdown options
@ -391,7 +390,7 @@ export default class RegisterArea extends Vue {
* Sets user's password field from value string.
*/
public setPassword(value: string): void {
this.user.password = value.trim();
this.user.password = value;
this.password = value;
this.passwordError = '';
}
@ -432,13 +431,6 @@ export default class RegisterArea extends Vue {
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.
*/
@ -493,12 +485,12 @@ export default class RegisterArea extends Vue {
private validateFields(): boolean {
let isNoErrors = true;
if (!this.user.fullName.trim()) {
this.fullNameError = 'Invalid Name';
if (!this.user.fullName) {
this.fullNameError = 'Name can\'t be empty';
isNoErrors = false;
}
if (!Validator.email(this.user.email.trim())) {
if (!this.isEmailValid()) {
this.emailError = 'Invalid Email';
isNoErrors = false;
}
@ -510,17 +502,17 @@ export default class RegisterArea extends Vue {
if (this.isProfessional) {
if (!this.user.companyName.trim()) {
if (!this.user.companyName) {
this.companyNameError = 'No Company Name filled in';
isNoErrors = false;
}
if (!this.user.position.trim()) {
if (!this.user.position) {
this.positionError = 'No Position filled in';
isNoErrors = false;
}
if (!this.user.employeeCount.trim()) {
if (!this.user.employeeCount) {
this.employeeCountError = 'No Company Size filled in';
isNoErrors = false;
}
@ -553,6 +545,19 @@ export default class RegisterArea extends Vue {
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.
*/