support login with 2fa

This commit is contained in:
MaysWind
2020-10-21 00:30:08 +08:00
parent 6896e1966e
commit 4e36558b43
9 changed files with 164 additions and 23 deletions
+15 -3
View File
@@ -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)
+5 -5
View File
@@ -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()
+2 -2
View File
@@ -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")
) )
+6
View File
@@ -0,0 +1,6 @@
package models
type AuthResponse struct {
Token string `json:"token"`
Need2FA bool `json:"need2FA"`
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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': '退出登录'
}; };
+96 -7
View File
@@ -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>