From 4e36558b43365f5a95dae6c5f9c492119812b899 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Wed, 21 Oct 2020 00:30:08 +0800 Subject: [PATCH] support login with 2fa --- pkg/api/authorizations.go | 18 +++- pkg/api/twofactor_authorizations.go | 10 +-- pkg/errs/twofactor_authorization.go | 4 +- pkg/models/auth_response.go | 6 ++ pkg/services/twofactor_authorizations.go | 4 +- src/lib/services.js | 16 +++- src/locales/en.js | 13 ++- src/locales/zh_Hans.js | 13 ++- src/views/mobile/Login.vue | 103 +++++++++++++++++++++-- 9 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 pkg/models/auth_response.go diff --git a/pkg/api/authorizations.go b/pkg/api/authorizations.go index f996c0df..8189716e 100644 --- a/pkg/api/authorizations.go +++ b/pkg/api/authorizations.go @@ -74,7 +74,13 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err c.SetTokenClaims(claims) log.InfofWithRequestId(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt) - return token, nil + + authResp := &models.AuthResponse{ + Token : token, + Need2FA: twoFactorEnable, + } + + return authResp, nil } func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interface{}, *errs.Error) { @@ -123,7 +129,13 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac c.SetTokenClaims(claims) log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt) - return token, nil + + authResp := &models.AuthResponse{ + Token : token, + Need2FA: false, + } + + return authResp, nil } func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (interface{}, *errs.Error) { @@ -144,7 +156,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont } if !enableTwoFactor { - return nil, errs.ErrTwoFactorKeyIsNotEnabled + return nil, errs.ErrTwoFactorIsNotEnabled } user, err := a.users.GetUserById(uid) diff --git a/pkg/api/twofactor_authorizations.go b/pkg/api/twofactor_authorizations.go index 1f813e31..1981dc59 100644 --- a/pkg/api/twofactor_authorizations.go +++ b/pkg/api/twofactor_authorizations.go @@ -33,7 +33,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (in uid := c.GetCurrentUid() twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid) - if err == errs.ErrTwoFactorKeyIsNotEnabled { + if err == errs.ErrTwoFactorIsNotEnabled { statusResp := &models.TwoFactorStatusResponse{ Enable: false, } @@ -64,7 +64,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte } if enabled { - return nil, errs.ErrTwoFactorKeyAlreadyEnabled + return nil, errs.ErrTwoFactorAlreadyEnabled } user, err := a.users.GetUserById(uid) @@ -123,7 +123,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte } if exists { - return nil, errs.ErrTwoFactorKeyAlreadyEnabled + return nil, errs.ErrTwoFactorAlreadyEnabled } user, err := a.users.GetUserById(uid) @@ -212,7 +212,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i } if !enableTwoFactor { - return nil, errs.ErrTwoFactorKeyIsNotEnabled + return nil, errs.ErrTwoFactorIsNotEnabled } err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid) @@ -244,7 +244,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c } if !enableTwoFactor { - return nil, errs.ErrTwoFactorKeyIsNotEnabled + return nil, errs.ErrTwoFactorIsNotEnabled } recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes() diff --git a/pkg/errs/twofactor_authorization.go b/pkg/errs/twofactor_authorization.go index 9e040a02..6511c44d 100644 --- a/pkg/errs/twofactor_authorization.go +++ b/pkg/errs/twofactor_authorization.go @@ -5,7 +5,7 @@ import "net/http" var ( ErrPasscodeInvalid = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 0, http.StatusUnauthorized, "passcode is invalid") ErrTwoFactorRecoveryCodeInvalid = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 1, http.StatusUnauthorized, "two factor recovery code is invalid") - ErrTwoFactorKeyIsNotEnabled = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 2, http.StatusBadRequest, "two factor key is not enabled") - ErrTwoFactorKeyAlreadyEnabled = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 3, http.StatusBadRequest, "two factor key has already been enabled") + ErrTwoFactorIsNotEnabled = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 2, http.StatusBadRequest, "two factor is not enabled") + ErrTwoFactorAlreadyEnabled = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 3, http.StatusBadRequest, "two factor has already been enabled") ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 4, http.StatusUnauthorized, "two factor recovery code does not exist") ) diff --git a/pkg/models/auth_response.go b/pkg/models/auth_response.go new file mode 100644 index 00000000..61382203 --- /dev/null +++ b/pkg/models/auth_response.go @@ -0,0 +1,6 @@ +package models + +type AuthResponse struct { + Token string `json:"token"` + Need2FA bool `json:"need2FA"` +} diff --git a/pkg/services/twofactor_authorizations.go b/pkg/services/twofactor_authorizations.go index eec74b3b..cf8fb5f9 100644 --- a/pkg/services/twofactor_authorizations.go +++ b/pkg/services/twofactor_authorizations.go @@ -53,7 +53,7 @@ func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(uid int64) if err != nil { return nil, err } else if !has { - return nil, errs.ErrTwoFactorKeyIsNotEnabled + return nil, errs.ErrTwoFactorIsNotEnabled } twoFactor.Secret, err = utils.DecryptSecret(twoFactor.Secret, s.CurrentConfig().SecretKey) @@ -109,7 +109,7 @@ func (s *TwoFactorAuthorizationService) DeleteTwoFactorSetting(uid int64) error deletedRows, err := sess.Where("uid=?", uid).Delete(&models.TwoFactor{}) if deletedRows < 1 { - return errs.ErrTwoFactorKeyIsNotEnabled + return errs.ErrTwoFactorIsNotEnabled } return err diff --git a/src/lib/services.js b/src/lib/services.js index e0cc7461..1228b39d 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -21,7 +21,19 @@ axios.interceptors.response.use(response => { }); export default { - authorize: (params) => { - return axios.post('authorize.json', params); + authorize: ({ loginName, password }) => { + return axios.post('authorize.json', { + loginName, + password + }); + }, + authorize2FA: ({ passcode, token }) => { + return axios.post('2fa/authorize.json', { + passcode + }, { + headers: { + Authorization: `Bearer ${token}` + } + }); } }; diff --git a/src/locales/en.js b/src/locales/en.js index 5e6015a4..f1fc78d0 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -15,7 +15,12 @@ export default { 'email already exists': 'Email already exists', 'login name is invalid': 'Login name is invalid', 'login name or password is invalid': 'Login name or password is invalid', - 'login name or password is wrong': 'Login name or password is wrong' + 'login name or password is wrong': 'Login name or password is wrong', + 'passcode is invalid': 'Passcode is invalid', + 'two factor recovery code is invalid': 'Two factor recovery code is invalid', + 'two factor is not enabled': 'Two factor is not enabled', + 'two factor has already been enabled': 'Two factor has already been enabled', + 'two factor recovery code does not exist': 'Two factor recovery code does not exist' }, 'Done': 'Done', 'Home': 'Home', @@ -34,6 +39,12 @@ export default { 'Please input username': 'Please input username', 'Please input password': 'Please input password', 'Unable to login': 'Unable to login', + 'Two-Factor Authentication': 'Two-Factor Authentication', + 'Passcode': 'Passcode', + 'Verify': 'Verify', + 'Please input passcode': 'Please input passcode', + 'Unable to verify': 'Unable to verify', + 'Use a scratch code': 'Use a scratch code', 'Language': 'Language', 'Logout': 'Logout' }; diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 80eae745..a1f00882 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -15,7 +15,12 @@ export default { 'email already exists': '邮箱已经存在', 'login name is invalid': '登录名无效', 'login name or password is invalid': '登录名或密码无效', - 'login name or password is wrong': '登录名或密码错误' + 'login name or password is wrong': '登录名或密码错误', + 'passcode is invalid': '验证码无效', + 'two factor recovery code is invalid': '两步验证恢复口令无效', + 'two factor is not enabled': '两步验证没有启用', + 'two factor has already been enabled': '两步验证已经启用', + 'two factor recovery code does not exist': '两步验证恢复口令不存在' }, 'Done': '完成', 'Home': '首页', @@ -34,6 +39,12 @@ export default { 'Please input username': '请输入用户名', 'Please input password': '请输入密码', 'Unable to login': '无法登录', + 'Two-Factor Authentication': '两步验证', + 'Passcode': '验证码', + 'Verify': '验证', + 'Please input passcode': '请输入验证码', + 'Unable to verify': '无法验证', + 'Use a scratch code': '使用验证口令', 'Language': '语言', 'Logout': '退出登录' }; diff --git a/src/views/mobile/Login.vue b/src/views/mobile/Login.vue index 5794e510..86de40b9 100644 --- a/src/views/mobile/Login.vue +++ b/src/views/mobile/Login.vue @@ -8,7 +8,7 @@ :label="$t('Username')" :placeholder="$t('Username or Email')" :value="username" - @input="username = $event.target.value" + @input="username = $event.target.value; tempToken = ''" > @@ -41,6 +41,34 @@ > + + +
+
+
{{ $t('Two-Factor Authentication') }}
+
+
+ + + + {{ $t('Verify') }} +
+ {{ $t('Use a scratch code') }} +
+
+
+
@@ -52,6 +80,8 @@ export default { return { username: '', password: '', + passcode: '', + tempToken: '', allLanguages: self.$getAllLanguages() }; }, @@ -59,6 +89,9 @@ export default { inputIsEmpty() { return !this.username || !this.password; }, + twoFAInputIsEmpty() { + return !this.passcode; + }, currentLanguageName() { const currentLocale = this.$i18n.locale; let lang = this.$getLanguage(currentLocale); @@ -86,11 +119,16 @@ export default { return; } + if (self.tempToken) { + app.sheet.open('#2fa-auth-sheet'); + return; + } + let hasResponse = false; setTimeout(() => { if (!hasResponse) { - self.$f7.preloader.show(); + app.preloader.show(); } }, 200); @@ -102,12 +140,19 @@ export default { self.$f7.preloader.hide(); const data = response.data; - if (data && data.success && data.result && typeof(data.result) === 'string') { - self.$user.updateToken(data.result); - router.navigate('/'); - } else { + if (!data || !data.success || !data.result || !data.result.token) { app.dialog.alert(self.$i18n.t('Unable to login')); + return; } + + if (data.result.need2FA) { + self.tempToken = data.result.token; + app.sheet.open('#2fa-auth-sheet'); + return; + } + + self.$user.updateToken(data.result.token); + router.navigate('/'); }).catch(error => { hasResponse = true; self.$f7.preloader.hide(); @@ -119,9 +164,53 @@ export default { } }) }, + verify() { + const self = this; + const app = self.$f7; + const router = self.$f7router; + + if (!this.passcode) { + app.dialog.alert(self.$i18n.t('Please input passcode')); + return; + } + + app.preloader.show(); + + self.$services.authorize2FA({ + passcode: self.passcode, + token: self.tempToken + }).then(response => { + self.$f7.preloader.hide(); + const data = response.data; + + if (!data || !data.success || !data.result || !data.result.token) { + app.dialog.alert(self.$i18n.t('Unable to verify')); + return; + } + + self.$user.updateToken(data.result.token); + app.sheet.close('#2fa-auth-sheet'); + router.navigate('/'); + }).catch(error => { + self.$f7.preloader.hide(); + + if (error.response && error.response.data && error.response.data.errorMessage) { + app.dialog.alert(self.$i18n.t(`error.${error.response.data.errorMessage}`)); + } else { + app.dialog.alert(self.$i18n.t('Unable to verify')); + } + }) + }, changeLanguage(locale) { this.$setLanguage(locale); } } }; + +