satellite/console,web/satellite: Add MFA to password reset
Users will be required to enter a MFA passcode or recovery code upon attempting a password reset for an account with MFA enabled. Change-Id: I08d07597035d5a25849dbc70f7fd686753530610
This commit is contained in:
parent
b2d342aa9b
commit
66e6a75e2a
@ -688,8 +688,10 @@ func (a *Auth) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
var resetPassword struct {
|
||||
RecoveryToken string `json:"token"`
|
||||
NewPassword string `json:"password"`
|
||||
RecoveryToken string `json:"token"`
|
||||
NewPassword string `json:"password"`
|
||||
MFAPasscode string `json:"mfaPasscode"`
|
||||
MFARecoveryCode string `json:"mfaRecoveryCode"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&resetPassword)
|
||||
@ -697,7 +699,24 @@ func (a *Auth) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
a.serveJSONError(w, err)
|
||||
}
|
||||
|
||||
err = a.service.ResetPassword(ctx, resetPassword.RecoveryToken, resetPassword.NewPassword, time.Now())
|
||||
err = a.service.ResetPassword(ctx, resetPassword.RecoveryToken, resetPassword.NewPassword, resetPassword.MFAPasscode, resetPassword.MFARecoveryCode, time.Now())
|
||||
|
||||
if console.ErrMFAMissing.Has(err) || console.ErrMFAPasscode.Has(err) || console.ErrMFARecoveryCode.Has(err) {
|
||||
w.WriteHeader(a.getStatusCode(err))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err = json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": a.getUserErrorMessage(err),
|
||||
"code": "mfa_required",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
a.log.Error("failed to write json response", zap.Error(ErrUtils.Wrap(err)))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
a.serveJSONError(w, err)
|
||||
}
|
||||
|
@ -526,9 +526,14 @@ func TestMFAEndpoints(t *testing.T) {
|
||||
func TestResetPasswordEndpoint(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Console.RateLimit.Burst = 10
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
newPass := "123a123"
|
||||
sat := planet.Satellites[0]
|
||||
service := sat.API.Console.Service
|
||||
|
||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||
FullName: "Test User",
|
||||
@ -536,17 +541,23 @@ func TestResetPasswordEndpoint(t *testing.T) {
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, token)
|
||||
tokenStr := token.Secret.String()
|
||||
newPass := user.FullName
|
||||
|
||||
tryReset := func(token, password string) int {
|
||||
getNewResetToken := func() *console.ResetPasswordToken {
|
||||
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, token)
|
||||
return token
|
||||
}
|
||||
|
||||
tryPasswordReset := func(tokenStr, password, mfaPasscode, mfaRecoveryCode string) (int, bool) {
|
||||
url := sat.ConsoleURL() + "/api/v0/auth/reset-password"
|
||||
|
||||
bodyBytes, err := json.Marshal(map[string]string{
|
||||
"password": password,
|
||||
"token": token,
|
||||
"password": password,
|
||||
"token": tokenStr,
|
||||
"mfaPasscode": mfaPasscode,
|
||||
"mfaRecoveryCode": mfaRecoveryCode,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -557,13 +568,61 @@ func TestResetPasswordEndpoint(t *testing.T) {
|
||||
|
||||
result, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var response struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
if result.ContentLength > 0 {
|
||||
err = json.NewDecoder(result.Body).Decode(&response)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.NoError(t, result.Body.Close())
|
||||
return result.StatusCode
|
||||
|
||||
return result.StatusCode, response.Code == "mfa_required"
|
||||
}
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, tryReset("badToken", newPass))
|
||||
require.Equal(t, http.StatusBadRequest, tryReset(tokenStr, "bad"))
|
||||
require.Equal(t, http.StatusOK, tryReset(tokenStr, newPass))
|
||||
token := getNewResetToken()
|
||||
|
||||
status, mfaError := tryPasswordReset("badToken", newPass, "", "")
|
||||
require.Equal(t, http.StatusUnauthorized, status)
|
||||
require.False(t, mfaError)
|
||||
|
||||
status, mfaError = tryPasswordReset(token.Secret.String(), "bad", "", "")
|
||||
require.Equal(t, http.StatusBadRequest, status)
|
||||
require.False(t, mfaError)
|
||||
|
||||
status, mfaError = tryPasswordReset(token.Secret.String(), newPass, "", "")
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.False(t, mfaError)
|
||||
token = getNewResetToken()
|
||||
|
||||
// Enable MFA.
|
||||
getNewAuthContext := func() context.Context {
|
||||
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
return authCtx
|
||||
}
|
||||
authCtx := getNewAuthContext()
|
||||
|
||||
key, err := service.ResetMFASecretKey(authCtx)
|
||||
require.NoError(t, err)
|
||||
authCtx = getNewAuthContext()
|
||||
|
||||
passcode, err := console.NewMFAPasscode(key, token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.EnableUserMFA(authCtx, passcode, token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
status, mfaError = tryPasswordReset(token.Secret.String(), newPass, "", "")
|
||||
require.Equal(t, http.StatusBadRequest, status)
|
||||
require.True(t, mfaError)
|
||||
|
||||
status, mfaError = tryPasswordReset(token.Secret.String(), newPass, "", "")
|
||||
require.Equal(t, http.StatusBadRequest, status)
|
||||
require.True(t, mfaError)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -775,7 +775,7 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
|
||||
}
|
||||
|
||||
// ResetPassword - is a method for resetting user password.
|
||||
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string, t time.Time) (err error) {
|
||||
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string, passcode string, recoveryCode string, t time.Time) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
secret, err := ResetPasswordSecretFromBase64(resetPasswordToken)
|
||||
@ -792,6 +792,31 @@ func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, passwor
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
if user.MFAEnabled {
|
||||
if recoveryCode != "" {
|
||||
found := false
|
||||
for _, code := range user.MFARecoveryCodes {
|
||||
if code == recoveryCode {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ErrUnauthorized.Wrap(ErrMFARecoveryCode.New(mfaRecoveryInvalidErrMsg))
|
||||
}
|
||||
} else if passcode != "" {
|
||||
valid, err := ValidateMFAPasscode(passcode, user.MFASecretKey, t)
|
||||
if err != nil {
|
||||
return ErrValidation.Wrap(ErrMFAPasscode.Wrap(err))
|
||||
}
|
||||
if !valid {
|
||||
return ErrValidation.Wrap(ErrMFAPasscode.New(mfaPasscodeInvalidErrMsg))
|
||||
}
|
||||
} else {
|
||||
return ErrMFAMissing.New(mfaRequiredErrMsg)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ValidatePassword(password); err != nil {
|
||||
return ErrValidation.Wrap(err)
|
||||
}
|
||||
|
@ -384,22 +384,21 @@ func TestMFA(t *testing.T) {
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
var auth console.Authorization
|
||||
var authCtx context.Context
|
||||
updateAuth := func() {
|
||||
authCtx, err = sat.AuthenticatedContext(ctx, user.ID)
|
||||
getNewAuthorization := func() (context.Context, console.Authorization) {
|
||||
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
auth, err = console.GetAuth(authCtx)
|
||||
auth, err := console.GetAuth(authCtx)
|
||||
require.NoError(t, err)
|
||||
return authCtx, auth
|
||||
}
|
||||
updateAuth()
|
||||
authCtx, auth := getNewAuthorization()
|
||||
|
||||
var key string
|
||||
t.Run("TestResetMFASecretKey", func(t *testing.T) {
|
||||
key, err = service.ResetMFASecretKey(authCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.NotEmpty(t, auth.User.MFASecretKey)
|
||||
})
|
||||
|
||||
@ -411,11 +410,11 @@ func TestMFA(t *testing.T) {
|
||||
err = service.EnableUserMFA(authCtx, badCode, time.Time{})
|
||||
require.True(t, console.ErrValidation.Has(err))
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
_, err = service.ResetMFARecoveryCodes(authCtx)
|
||||
require.True(t, console.ErrUnauthorized.Has(err))
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.False(t, auth.User.MFAEnabled)
|
||||
})
|
||||
|
||||
@ -424,11 +423,11 @@ func TestMFA(t *testing.T) {
|
||||
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
err = service.EnableUserMFA(authCtx, goodCode, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.True(t, auth.User.MFAEnabled)
|
||||
require.Equal(t, auth.User.MFASecretKey, key)
|
||||
})
|
||||
@ -464,7 +463,7 @@ func TestMFA(t *testing.T) {
|
||||
_, err = service.ResetMFARecoveryCodes(authCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.Len(t, auth.User.MFARecoveryCodes, console.MFARecoveryCodeCount)
|
||||
|
||||
for _, code := range auth.User.MFARecoveryCodes {
|
||||
@ -482,7 +481,7 @@ func TestMFA(t *testing.T) {
|
||||
require.True(t, console.ErrMFARecoveryCode.Has(err))
|
||||
require.Empty(t, token)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
}
|
||||
|
||||
_, err = service.ResetMFARecoveryCodes(authCtx)
|
||||
@ -494,11 +493,11 @@ func TestMFA(t *testing.T) {
|
||||
badCode, err := console.NewMFAPasscode(key, time.Time{}.Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
err = service.DisableUserMFA(authCtx, badCode, time.Time{}, "")
|
||||
require.True(t, console.ErrValidation.Has(err))
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.True(t, auth.User.MFAEnabled)
|
||||
require.NotEmpty(t, auth.User.MFASecretKey)
|
||||
require.NotEmpty(t, auth.User.MFARecoveryCodes)
|
||||
@ -509,11 +508,11 @@ func TestMFA(t *testing.T) {
|
||||
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
err = service.DisableUserMFA(authCtx, goodCode, time.Time{}, auth.User.MFARecoveryCodes[0])
|
||||
require.True(t, console.ErrMFAConflict.Has(err))
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.True(t, auth.User.MFAEnabled)
|
||||
require.NotEmpty(t, auth.User.MFASecretKey)
|
||||
require.NotEmpty(t, auth.User.MFARecoveryCodes)
|
||||
@ -524,11 +523,11 @@ func TestMFA(t *testing.T) {
|
||||
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
err = service.DisableUserMFA(authCtx, goodCode, time.Time{}, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.False(t, auth.User.MFAEnabled)
|
||||
require.Empty(t, auth.User.MFASecretKey)
|
||||
require.Empty(t, auth.User.MFARecoveryCodes)
|
||||
@ -543,15 +542,15 @@ func TestMFA(t *testing.T) {
|
||||
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
err = service.EnableUserMFA(authCtx, goodCode, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
_, err = service.ResetMFARecoveryCodes(authCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.True(t, auth.User.MFAEnabled)
|
||||
require.NotEmpty(t, auth.User.MFASecretKey)
|
||||
require.NotEmpty(t, auth.User.MFARecoveryCodes)
|
||||
@ -560,7 +559,7 @@ func TestMFA(t *testing.T) {
|
||||
err = service.DisableUserMFA(authCtx, "", time.Time{}, auth.User.MFARecoveryCodes[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
updateAuth()
|
||||
authCtx, auth = getNewAuthorization()
|
||||
require.False(t, auth.User.MFAEnabled)
|
||||
require.Empty(t, auth.User.MFASecretKey)
|
||||
require.Empty(t, auth.User.MFARecoveryCodes)
|
||||
@ -572,7 +571,6 @@ func TestResetPassword(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
newPass := "123a123"
|
||||
sat := planet.Satellites[0]
|
||||
service := sat.API.Console.Service
|
||||
|
||||
@ -582,25 +580,73 @@ func TestResetPassword(t *testing.T) {
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, token)
|
||||
tokenStr := token.Secret.String()
|
||||
newPass := user.FullName
|
||||
|
||||
getNewResetToken := func() *console.ResetPasswordToken {
|
||||
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, token)
|
||||
return token
|
||||
}
|
||||
token := getNewResetToken()
|
||||
|
||||
// Expect error when providing bad token.
|
||||
err = service.ResetPassword(ctx, "badToken", newPass, token.CreatedAt)
|
||||
err = service.ResetPassword(ctx, "badToken", newPass, "", "", token.CreatedAt)
|
||||
require.True(t, console.ErrRecoveryToken.Has(err))
|
||||
|
||||
// Expect error when providing good but expired token.
|
||||
err = service.ResetPassword(ctx, tokenStr, newPass, token.CreatedAt.Add(sat.Config.Console.TokenExpirationTime).Add(time.Second))
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt.Add(sat.Config.Console.TokenExpirationTime).Add(time.Second))
|
||||
require.True(t, console.ErrTokenExpiration.Has(err))
|
||||
|
||||
// Expect error when providing good token with bad (too short) password.
|
||||
err = service.ResetPassword(ctx, tokenStr, "bad", token.CreatedAt)
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), "bad", "", "", token.CreatedAt)
|
||||
require.True(t, console.ErrValidation.Has(err))
|
||||
|
||||
// Expect success when providing good token and good password.
|
||||
err = service.ResetPassword(ctx, tokenStr, newPass, token.CreatedAt)
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
token = getNewResetToken()
|
||||
|
||||
// Enable MFA.
|
||||
getNewAuthorization := func() (context.Context, console.Authorization) {
|
||||
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
auth, err := console.GetAuth(authCtx)
|
||||
require.NoError(t, err)
|
||||
return authCtx, auth
|
||||
}
|
||||
authCtx, _ := getNewAuthorization()
|
||||
|
||||
key, err := service.ResetMFASecretKey(authCtx)
|
||||
require.NoError(t, err)
|
||||
authCtx, auth := getNewAuthorization()
|
||||
|
||||
passcode, err := console.NewMFAPasscode(key, token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.EnableUserMFA(authCtx, passcode, token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect error when providing bad passcode.
|
||||
badPasscode, err := console.NewMFAPasscode(key, token.CreatedAt.Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, "", token.CreatedAt)
|
||||
require.True(t, console.ErrMFAPasscode.Has(err))
|
||||
|
||||
for _, recoveryCode := range auth.User.MFARecoveryCodes {
|
||||
// Expect success when providing bad passcode and good recovery code.
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, recoveryCode, token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
token = getNewResetToken()
|
||||
|
||||
// Expect error when providing bad passcode and already-used recovery code.
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, recoveryCode, token.CreatedAt)
|
||||
require.True(t, console.ErrMFARecoveryCode.Has(err))
|
||||
}
|
||||
|
||||
// Expect success when providing good passcode.
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, passcode, "", token.CreatedAt)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -51,8 +51,8 @@ export class AuthHttpApi implements UsersApi {
|
||||
const body = {
|
||||
email,
|
||||
password,
|
||||
mfaPasscode: mfaPasscode ? mfaPasscode : null,
|
||||
mfaRecoveryCode: mfaRecoveryCode ? mfaRecoveryCode : null,
|
||||
mfaPasscode: mfaPasscode || null,
|
||||
mfaRecoveryCode: mfaRecoveryCode || null,
|
||||
};
|
||||
|
||||
const response = await this.http.post(path, JSON.stringify(body));
|
||||
@ -374,24 +374,40 @@ export class AuthHttpApi implements UsersApi {
|
||||
/**
|
||||
* Used to reset user's password.
|
||||
*
|
||||
* @param token - user's password reset token
|
||||
* @param password - user's new password
|
||||
* @param mfaPasscode - MFA passcode
|
||||
* @param mfaRecoveryCode - MFA recovery code
|
||||
* @throws Error
|
||||
*/
|
||||
public async resetPassword(token: string, password: string): Promise<void> {
|
||||
public async resetPassword(token: string, password: string, mfaPasscode: string, mfaRecoveryCode: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/reset-password`;
|
||||
|
||||
const body = {
|
||||
token: token,
|
||||
password: password,
|
||||
mfaPasscode: mfaPasscode || null,
|
||||
mfaRecoveryCode: mfaRecoveryCode || null,
|
||||
};
|
||||
|
||||
const response = await this.http.post(path, JSON.stringify(body));
|
||||
const text = await response.text();
|
||||
let errMsg = 'Cannot reset password';
|
||||
|
||||
if (text) {
|
||||
const result = JSON.parse(text);
|
||||
if (result.code == "mfa_required") {
|
||||
throw new ErrorMFARequired();
|
||||
}
|
||||
if (result.error) {
|
||||
errMsg = result.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const errMsg = result.error || 'Cannot reset password';
|
||||
switch (response.status) {
|
||||
case 400:
|
||||
throw new ErrorBadRequest(errMsg);
|
||||
|
@ -8,8 +8,34 @@
|
||||
</div>
|
||||
<div class="reset-area__content-area">
|
||||
<div class="reset-area__content-area__container" :class="{'success': isSuccessfulPasswordResetShown}">
|
||||
<template v-if="!isSuccessfulPasswordResetShown">
|
||||
<h1 class="reset-area__content-area__container__title">Reset Password</h1>
|
||||
<h1 v-if="!isSuccessfulPasswordResetShown" class="reset-area__content-area__container__title">Reset Password</h1>
|
||||
<template v-if="isSuccessfulPasswordResetShown">
|
||||
<KeyIcon />
|
||||
<h2 class="reset-area__content-area__container__title success">Success!</h2>
|
||||
<p class="reset-area__content-area__container__sub-title">
|
||||
You have successfully changed your password.
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="isMFARequired">
|
||||
<div class="info-box">
|
||||
<div class="info-box__header">
|
||||
<GreyWarningIcon />
|
||||
<h2 class="info-box__header__label">
|
||||
Two-Factor Authentication Required
|
||||
</h2>
|
||||
</div>
|
||||
<p class="info-box__message">
|
||||
You'll need the six-digit code from your authenticator app to continue.
|
||||
</p>
|
||||
</div>
|
||||
<div class="reset-area__content-area__container__input-wrapper">
|
||||
<ConfirmMFAInput ref="mfaInput" :on-input="onConfirmInput" :is-error="isMFAError" :is-recovery="isRecoveryCodeState" />
|
||||
</div>
|
||||
<span v-if="!isRecoveryCodeState" class="reset-area__content-area__container__recovery" @click="setRecoveryCodeState">
|
||||
Or use recovery code
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="reset-area__content-area__container__message">Please enter your new password.</p>
|
||||
<div class="reset-area__content-area__container__input-wrapper password">
|
||||
<HeaderlessInput
|
||||
@ -35,15 +61,11 @@
|
||||
@setData="setRepeatedPassword"
|
||||
/>
|
||||
</div>
|
||||
<p class="reset-area__content-area__container__button" @click.prevent="onResetClick">Reset Password</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<KeyIcon />
|
||||
<h2 class="reset-area__content-area__container__title success">Success!</h2>
|
||||
<p class="reset-area__content-area__container__sub-title">
|
||||
You have successfully changed your password.
|
||||
</p>
|
||||
</template>
|
||||
<p v-if="!isSuccessfulPasswordResetShown" class="reset-area__content-area__container__button" @click.prevent="onResetClick">Reset Password</p>
|
||||
<span v-if="isMFARequired && !isSuccessfulPasswordResetShown" class="reset-area__content-area__container__cancel" :class="{ disabled: isLoading }" @click.prevent="onMFACancelClick">
|
||||
Cancel
|
||||
</span>
|
||||
</div>
|
||||
<router-link :to="loginPath" class="reset-area__content-area__login-link">
|
||||
Back to Login
|
||||
@ -55,13 +77,16 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import ConfirmMFAInput from '@/components/account/mfa/ConfirmMFAInput.vue';
|
||||
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
|
||||
import PasswordStrength from '@/components/common/PasswordStrength.vue';
|
||||
|
||||
import GreyWarningIcon from '@/../static/images/common/greyWarning.svg';
|
||||
import LogoIcon from '@/../static/images/logo.svg';
|
||||
import KeyIcon from '@/../static/images/resetPassword/success.svg';
|
||||
|
||||
import { AuthHttpApi } from '@/api/auth';
|
||||
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
|
||||
import { RouteConfig } from '@/router';
|
||||
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
||||
import { Validator } from '@/utils/validation';
|
||||
@ -73,6 +98,8 @@ import { Validator } from '@/utils/validation';
|
||||
HeaderlessInput,
|
||||
PasswordStrength,
|
||||
KeyIcon,
|
||||
ConfirmMFAInput,
|
||||
GreyWarningIcon,
|
||||
},
|
||||
})
|
||||
|
||||
@ -80,10 +107,15 @@ export default class ResetPassword extends Vue {
|
||||
private token = '';
|
||||
private password = '';
|
||||
private repeatedPassword = '';
|
||||
private passcode = '';
|
||||
private recoveryCode = '';
|
||||
|
||||
private passwordError = '';
|
||||
private repeatedPasswordError = '';
|
||||
private isLoading = false;
|
||||
private isMFARequired = false;
|
||||
private isMFAError = false;
|
||||
private isRecoveryCodeState = false;
|
||||
|
||||
private readonly auth: AuthHttpApi = new AuthHttpApi();
|
||||
|
||||
@ -91,6 +123,10 @@ export default class ResetPassword extends Vue {
|
||||
|
||||
public readonly loginPath: string = RouteConfig.Login.path;
|
||||
|
||||
public $refs!: {
|
||||
mfaInput: ConfirmMFAInput;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lifecycle hook on component destroy.
|
||||
* Sets view to default state.
|
||||
@ -138,10 +174,23 @@ export default class ResetPassword extends Vue {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.auth.resetPassword(this.token, this.password);
|
||||
await this.auth.resetPassword(this.token, this.password, this.passcode.trim(), this.recoveryCode.trim());
|
||||
await this.auth.logout();
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
|
||||
} catch (error) {
|
||||
this.isLoading = false;
|
||||
|
||||
if (error instanceof ErrorMFARequired) {
|
||||
if (this.isMFARequired) this.isMFAError = true;
|
||||
this.isMFARequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMFARequired) {
|
||||
this.isMFAError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$notify.error(error.message);
|
||||
}
|
||||
|
||||
@ -203,6 +252,36 @@ export default class ResetPassword extends Vue {
|
||||
this.repeatedPassword = value.trim();
|
||||
this.repeatedPasswordError = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets page to recovery code state.
|
||||
*/
|
||||
public setRecoveryCodeState(): void {
|
||||
this.isMFAError = false;
|
||||
this.passcode = '';
|
||||
this.$refs.mfaInput.clearInput();
|
||||
this.isRecoveryCodeState = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels MFA passcode input state.
|
||||
*/
|
||||
public onMFACancelClick(): void {
|
||||
this.isMFARequired = false;
|
||||
this.isRecoveryCodeState = false;
|
||||
this.isMFAError = false;
|
||||
this.passcode = '';
|
||||
this.recoveryCode = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets confirmation passcode value from input.
|
||||
*/
|
||||
public onConfirmInput(value: string): void {
|
||||
this.isMFAError = false;
|
||||
|
||||
this.isRecoveryCodeState ? this.recoveryCode = value : this.passcode = value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -294,6 +373,26 @@ export default class ResetPassword extends Vue {
|
||||
background-color: #0059d0;
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
align-self: center;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
color: #0068dc;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__recovery {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
color: #0068dc;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__login-link {
|
||||
@ -307,6 +406,38 @@ export default class ResetPassword extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: #f7f8fb;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-top: 25px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.error {
|
||||
background-color: #fff9f7;
|
||||
border: 1px solid #f84b00;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 16px;
|
||||
color: #1b2533;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 16px;
|
||||
color: #1b2533;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
|
||||
.reset-area {
|
||||
|
Loading…
Reference in New Issue
Block a user