mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-20 01:34:24 +08:00
support login with 2fa
This commit is contained in:
@@ -74,7 +74,13 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
|
|||||||
c.SetTokenClaims(claims)
|
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)
|
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) {
|
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)
|
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)
|
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) {
|
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
@@ -144,7 +156,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !enableTwoFactor {
|
if !enableTwoFactor {
|
||||||
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
return nil, errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (in
|
|||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
|
||||||
|
|
||||||
if err == errs.ErrTwoFactorKeyIsNotEnabled {
|
if err == errs.ErrTwoFactorIsNotEnabled {
|
||||||
statusResp := &models.TwoFactorStatusResponse{
|
statusResp := &models.TwoFactorStatusResponse{
|
||||||
Enable: false,
|
Enable: false,
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
return nil, errs.ErrTwoFactorKeyAlreadyEnabled
|
return nil, errs.ErrTwoFactorAlreadyEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(uid)
|
||||||
@@ -123,7 +123,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
return nil, errs.ErrTwoFactorKeyAlreadyEnabled
|
return nil, errs.ErrTwoFactorAlreadyEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(uid)
|
user, err := a.users.GetUserById(uid)
|
||||||
@@ -212,7 +212,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !enableTwoFactor {
|
if !enableTwoFactor {
|
||||||
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
return nil, errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
|
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
|
||||||
@@ -244,7 +244,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !enableTwoFactor {
|
if !enableTwoFactor {
|
||||||
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
return nil, errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "net/http"
|
|||||||
var (
|
var (
|
||||||
ErrPasscodeInvalid = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 0, http.StatusUnauthorized, "passcode is invalid")
|
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")
|
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")
|
ErrTwoFactorIsNotEnabled = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 2, http.StatusBadRequest, "two factor is not enabled")
|
||||||
ErrTwoFactorKeyAlreadyEnabled = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 3, http.StatusBadRequest, "two factor key has already been 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")
|
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NORMAL_SUBCATEGORY_TWOFACTOR, 4, http.StatusUnauthorized, "two factor recovery code does not exist")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type AuthResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Need2FA bool `json:"need2FA"`
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(uid int64)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
return nil, errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
twoFactor.Secret, err = utils.DecryptSecret(twoFactor.Secret, s.CurrentConfig().SecretKey)
|
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{})
|
deletedRows, err := sess.Where("uid=?", uid).Delete(&models.TwoFactor{})
|
||||||
|
|
||||||
if deletedRows < 1 {
|
if deletedRows < 1 {
|
||||||
return errs.ErrTwoFactorKeyIsNotEnabled
|
return errs.ErrTwoFactorIsNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
+14
-2
@@ -21,7 +21,19 @@ axios.interceptors.response.use(response => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
authorize: (params) => {
|
authorize: ({ loginName, password }) => {
|
||||||
return axios.post('authorize.json', params);
|
return axios.post('authorize.json', {
|
||||||
|
loginName,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
},
|
||||||
|
authorize2FA: ({ passcode, token }) => {
|
||||||
|
return axios.post('2fa/authorize.json', {
|
||||||
|
passcode
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-1
@@ -15,7 +15,12 @@ export default {
|
|||||||
'email already exists': 'Email already exists',
|
'email already exists': 'Email already exists',
|
||||||
'login name is invalid': 'Login name is invalid',
|
'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 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',
|
'Done': 'Done',
|
||||||
'Home': 'Home',
|
'Home': 'Home',
|
||||||
@@ -34,6 +39,12 @@ export default {
|
|||||||
'Please input username': 'Please input username',
|
'Please input username': 'Please input username',
|
||||||
'Please input password': 'Please input password',
|
'Please input password': 'Please input password',
|
||||||
'Unable to login': 'Unable to login',
|
'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',
|
'Language': 'Language',
|
||||||
'Logout': 'Logout'
|
'Logout': 'Logout'
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-1
@@ -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': '登录名或密码错误',
|
||||||
|
'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': '完成',
|
'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': '两步验证',
|
||||||
|
'Passcode': '验证码',
|
||||||
|
'Verify': '验证',
|
||||||
|
'Please input passcode': '请输入验证码',
|
||||||
|
'Unable to verify': '无法验证',
|
||||||
|
'Use a scratch code': '使用验证口令',
|
||||||
'Language': '语言',
|
'Language': '语言',
|
||||||
'Logout': '退出登录'
|
'Logout': '退出登录'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
:label="$t('Username')"
|
:label="$t('Username')"
|
||||||
:placeholder="$t('Username or Email')"
|
:placeholder="$t('Username or Email')"
|
||||||
:value="username"
|
:value="username"
|
||||||
@input="username = $event.target.value"
|
@input="username = $event.target.value; tempToken = ''"
|
||||||
></f7-list-input>
|
></f7-list-input>
|
||||||
<f7-list-input
|
<f7-list-input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
:label="$t('Password')"
|
:label="$t('Password')"
|
||||||
:placeholder="$t('Password')"
|
:placeholder="$t('Password')"
|
||||||
:value="password"
|
:value="password"
|
||||||
@input="password = $event.target.value"
|
@input="password = $event.target.value; tempToken = ''"
|
||||||
></f7-list-input>
|
></f7-list-input>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
<f7-list>
|
<f7-list>
|
||||||
@@ -41,6 +41,34 @@
|
|||||||
></f7-list-item>
|
></f7-list-item>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
</f7-popover>
|
</f7-popover>
|
||||||
|
|
||||||
|
<f7-sheet
|
||||||
|
id="2fa-auth-sheet"
|
||||||
|
style="height:auto; --f7-sheet-bg-color: #fff;"
|
||||||
|
backdrop
|
||||||
|
>
|
||||||
|
<div class="sheet-modal-swipe-step">
|
||||||
|
<div class="display-flex padding justify-content-space-between align-items-center">
|
||||||
|
<div style="font-size: 18px"><b>{{ $t('Two-Factor Authentication') }}</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="padding-horizontal padding-bottom">
|
||||||
|
<f7-list no-hairlines class="twofa-auth-form">
|
||||||
|
<f7-list-input
|
||||||
|
type="password"
|
||||||
|
outline
|
||||||
|
clear-button
|
||||||
|
:placeholder="$t('Passcode')"
|
||||||
|
:value="passcode"
|
||||||
|
@input="passcode = $event.target.value"
|
||||||
|
></f7-list-input>
|
||||||
|
</f7-list>
|
||||||
|
<f7-button large fill :class="{ 'disabled': twoFAInputIsEmpty }" @click="verify">{{ $t('Verify') }}</f7-button>
|
||||||
|
<div class="margin-top text-align-center">
|
||||||
|
<f7-link href="/2fa-scratch-code">{{ $t('Use a scratch code') }}</f7-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</f7-sheet>
|
||||||
</f7-page>
|
</f7-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -52,6 +80,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
passcode: '',
|
||||||
|
tempToken: '',
|
||||||
allLanguages: self.$getAllLanguages()
|
allLanguages: self.$getAllLanguages()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -59,6 +89,9 @@ export default {
|
|||||||
inputIsEmpty() {
|
inputIsEmpty() {
|
||||||
return !this.username || !this.password;
|
return !this.username || !this.password;
|
||||||
},
|
},
|
||||||
|
twoFAInputIsEmpty() {
|
||||||
|
return !this.passcode;
|
||||||
|
},
|
||||||
currentLanguageName() {
|
currentLanguageName() {
|
||||||
const currentLocale = this.$i18n.locale;
|
const currentLocale = this.$i18n.locale;
|
||||||
let lang = this.$getLanguage(currentLocale);
|
let lang = this.$getLanguage(currentLocale);
|
||||||
@@ -86,11 +119,16 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.tempToken) {
|
||||||
|
app.sheet.open('#2fa-auth-sheet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let hasResponse = false;
|
let hasResponse = false;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!hasResponse) {
|
if (!hasResponse) {
|
||||||
self.$f7.preloader.show();
|
app.preloader.show();
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
@@ -102,12 +140,19 @@ export default {
|
|||||||
self.$f7.preloader.hide();
|
self.$f7.preloader.hide();
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
if (data && data.success && data.result && typeof(data.result) === 'string') {
|
if (!data || !data.success || !data.result || !data.result.token) {
|
||||||
self.$user.updateToken(data.result);
|
|
||||||
router.navigate('/');
|
|
||||||
} else {
|
|
||||||
app.dialog.alert(self.$i18n.t('Unable to login'));
|
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 => {
|
}).catch(error => {
|
||||||
hasResponse = true;
|
hasResponse = true;
|
||||||
self.$f7.preloader.hide();
|
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) {
|
changeLanguage(locale) {
|
||||||
this.$setLanguage(locale);
|
this.$setLanguage(locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.twofa-auth-form {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user