fix: passkey security
This commit is contained in:
@@ -189,13 +189,8 @@ func PasskeyStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_used_at": credential.LastUsedAt,
|
"last_used_at": credential.LastUsedAt,
|
||||||
"backup_eligible": credential.BackupEligible,
|
|
||||||
"backup_state": credential.BackupState,
|
|
||||||
}
|
|
||||||
if credential != nil {
|
|
||||||
data["credential_aaguid"] = fmt.Sprintf("%x", credential.AAGUID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -278,14 +273,14 @@ func PasskeyLoginFinish(c *gin.Context) {
|
|||||||
return nil, errors.New("该用户已被禁用")
|
return nil, errors.New("该用户已被禁用")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户句柄(如果提供的话)
|
|
||||||
if len(userHandle) > 0 {
|
if len(userHandle) > 0 {
|
||||||
if userID, parseErr := strconv.Atoi(string(userHandle)); parseErr == nil {
|
userID, parseErr := strconv.Atoi(string(userHandle))
|
||||||
if userID != user.Id {
|
if parseErr != nil {
|
||||||
return nil, errors.New("用户句柄与凭证不匹配")
|
// 记录异常但继续验证,因为某些客户端可能使用非数字格式
|
||||||
}
|
common.SysLog(fmt.Sprintf("PasskeyLogin: userHandle parse error for credential, length: %d", len(userHandle)))
|
||||||
|
} else if userID != user.Id {
|
||||||
|
return nil, errors.New("用户句柄与凭证不匹配")
|
||||||
}
|
}
|
||||||
// 如果解析失败,不做严格验证,因为某些情况下userHandle可能为空或格式不同
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return passkeysvc.NewWebAuthnUser(user, credential), nil
|
return passkeysvc.NewWebAuthnUser(user, credential), nil
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
adminRoute.POST("/manage", controller.ManageUser)
|
adminRoute.POST("/manage", controller.ManageUser)
|
||||||
adminRoute.PUT("/", controller.UpdateUser)
|
adminRoute.PUT("/", controller.UpdateUser)
|
||||||
adminRoute.DELETE("/:id", controller.DeleteUser)
|
adminRoute.DELETE("/:id", controller.DeleteUser)
|
||||||
adminRoute.DELETE("/:id/passkey", controller.AdminResetPasskey)
|
adminRoute.DELETE("/:id/reset_passkey", controller.AdminResetPasskey)
|
||||||
|
|
||||||
// Admin 2FA routes
|
// Admin 2FA routes
|
||||||
adminRoute.GET("/2fa/stats", controller.Admin2FAStats)
|
adminRoute.GET("/2fa/stats", controller.Admin2FAStats)
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import {
|
|||||||
Progress,
|
Progress,
|
||||||
Popover,
|
Popover,
|
||||||
Typography,
|
Typography,
|
||||||
|
Dropdown,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
|
import { IconMore } from '@douyinfe/semi-icons';
|
||||||
import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
|
import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,6 +215,28 @@ const renderOperations = (
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moreMenu = [
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
name: t('重置 Passkey'),
|
||||||
|
onClick: () => showResetPasskeyModal(record),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
name: t('重置 2FA'),
|
||||||
|
onClick: () => showResetTwoFAModal(record),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
name: t('注销'),
|
||||||
|
type: 'danger',
|
||||||
|
onClick: () => showDeleteModal(record),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
{record.status === 1 ? (
|
{record.status === 1 ? (
|
||||||
@@ -255,27 +279,17 @@ const renderOperations = (
|
|||||||
>
|
>
|
||||||
{t('降级')}
|
{t('降级')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Dropdown
|
||||||
type='warning'
|
menu={moreMenu}
|
||||||
size='small'
|
trigger='click'
|
||||||
onClick={() => showResetPasskeyModal(record)}
|
position='bottomRight'
|
||||||
>
|
>
|
||||||
{t('重置 Passkey')}
|
<Button
|
||||||
</Button>
|
type='tertiary'
|
||||||
<Button
|
size='small'
|
||||||
type='warning'
|
icon={<IconMore />}
|
||||||
size='small'
|
/>
|
||||||
onClick={() => showResetTwoFAModal(record)}
|
</Dropdown>
|
||||||
>
|
|
||||||
{t('重置 2FA')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='danger'
|
|
||||||
size='small'
|
|
||||||
onClick={() => showDeleteModal(record)}
|
|
||||||
>
|
|
||||||
{t('注销')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const searchUsers = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await API.delete(`/api/user/${user.id}/passkey`);
|
const res = await API.delete(`/api/user/${user.id}/reset_passkey`);
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
showSuccess(t('Passkey 已重置'));
|
showSuccess(t('Passkey 已重置'));
|
||||||
|
|||||||
Reference in New Issue
Block a user