fix: require reauthentication for auth request approval

This commit is contained in:
shuaiplus
2026-06-24 01:23:34 +08:00
committed by Shuai
parent 0daad46591
commit 23b23f39b9
3 changed files with 47 additions and 9 deletions
+16
View File
@@ -14,6 +14,19 @@ function normalizeText(value: unknown, maxLength: number): string {
return String(value ?? '').trim().slice(0, maxLength); return String(value ?? '').trim().slice(0, maxLength);
} }
function isSerializedEncString(value: unknown): value is string {
const text = String(value || '').trim();
if (!text) return false;
const parts = text.split('.');
if (parts.length !== 2) return false;
const type = Number(parts[0]);
const bodyParts = parts[1].split('|');
if (type === 2) return bodyParts.length === 3 && bodyParts.every(Boolean);
if (type === 3 || type === 4) return bodyParts.length === 1 && !!bodyParts[0];
if (type === 5 || type === 6) return bodyParts.length === 2 && bodyParts.every(Boolean);
return false;
}
function getClientIp(request: Request): string | null { function getClientIp(request: Request): string | null {
return ( return (
request.headers.get('CF-Connecting-IP') || request.headers.get('CF-Connecting-IP') ||
@@ -251,6 +264,9 @@ export async function handleUpdateAuthRequest(request: Request, env: Env, userId
if (approved && !key) { if (approved && !key) {
return errorResponse('Encrypted key is required to approve the request.', 400); return errorResponse('Encrypted key is required to approve the request.', 400);
} }
if (approved && !isSerializedEncString(key)) {
return errorResponse('Encrypted key is not a valid encrypted string.', 400);
}
const updated = await storage.updateAuthRequestResponse(id, userId, { const updated = await storage.updateAuthRequestResponse(id, userId, {
approved, approved,
+4 -1
View File
@@ -337,6 +337,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
} }
let validatedAuthRequestId: string | null = null; let validatedAuthRequestId: string | null = null;
let authRequestLoginKey: string | null = null;
let valid = false; let valid = false;
const normalizedAuthRequestId = String(authRequestId || '').trim(); const normalizedAuthRequestId = String(authRequestId || '').trim();
if (normalizedAuthRequestId) { if (normalizedAuthRequestId) {
@@ -349,10 +350,12 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
authRequest.responseDate && authRequest.responseDate &&
!authRequest.authenticationDate && !authRequest.authenticationDate &&
!isAuthRequestExpired(authRequest) && !isAuthRequestExpired(authRequest) &&
!!authRequest.key &&
constantTimeEquals(authRequest.accessCode, passwordHash) constantTimeEquals(authRequest.accessCode, passwordHash)
); );
if (valid) { if (valid) {
validatedAuthRequestId = authRequest!.id; validatedAuthRequestId = authRequest!.id;
authRequestLoginKey = authRequest!.key;
} }
} else { } else {
valid = await auth.verifyPassword(passwordHash, user.masterPasswordHash, user.email); valid = await auth.verifyPassword(passwordHash, user.masterPasswordHash, user.email);
@@ -493,7 +496,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
token_type: 'Bearer', token_type: 'Bearer',
...(shouldUseWebSession(request) ? { web_session: true } : { refresh_token: refreshToken }), ...(shouldUseWebSession(request) ? { web_session: true } : { refresh_token: refreshToken }),
...(trustedTwoFactorTokenToReturn ? { TwoFactorToken: trustedTwoFactorTokenToReturn } : {}), ...(trustedTwoFactorTokenToReturn ? { TwoFactorToken: trustedTwoFactorTokenToReturn } : {}),
Key: user.key, Key: authRequestLoginKey || user.key,
PrivateKey: user.privateKey, PrivateKey: user.privateKey,
AccountKeys: accountKeys, AccountKeys: accountKeys,
accountKeys: accountKeys, accountKeys: accountKeys,
+27 -8
View File
@@ -238,6 +238,7 @@ export default function App() {
const [disableTotpPassword, setDisableTotpPassword] = useState(''); const [disableTotpPassword, setDisableTotpPassword] = useState('');
const [disableTotpSubmitting, setDisableTotpSubmitting] = useState(false); const [disableTotpSubmitting, setDisableTotpSubmitting] = useState(false);
const [authRequestDialogDismissedId, setAuthRequestDialogDismissedId] = useState<string | null>(null); const [authRequestDialogDismissedId, setAuthRequestDialogDismissedId] = useState<string | null>(null);
const [authRequestDialogSelectedId, setAuthRequestDialogSelectedId] = useState<string | null>(null);
const [authRequestSubmittingId, setAuthRequestSubmittingId] = useState<string | null>(null); const [authRequestSubmittingId, setAuthRequestSubmittingId] = useState<string | null>(null);
const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' }); const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' });
const [themePreference, setThemePreference] = useState<ThemePreference>(() => readThemePreference()); const [themePreference, setThemePreference] = useState<ThemePreference>(() => readThemePreference());
@@ -1119,7 +1120,20 @@ export default function App() {
}); });
const pendingAuthRequests = (pendingAuthRequestsQuery.data || []).filter(isPendingAuthRequest); const pendingAuthRequests = (pendingAuthRequestsQuery.data || []).filter(isPendingAuthRequest);
const latestPendingAuthRequest = pendingAuthRequests[0] || null; const latestPendingAuthRequest = pendingAuthRequests[0] || null;
const authRequestDialogOpen = !!latestPendingAuthRequest && latestPendingAuthRequest.id !== authRequestDialogDismissedId; const selectedPendingAuthRequest = authRequestDialogSelectedId
? pendingAuthRequests.find((request) => request.id === authRequestDialogSelectedId) || null
: null;
const authRequestDialogRequest = selectedPendingAuthRequest || (
latestPendingAuthRequest && latestPendingAuthRequest.id !== authRequestDialogDismissedId
? latestPendingAuthRequest
: null
);
const authRequestDialogOpen = !!authRequestDialogRequest;
async function beginApproveAuthRequest(authRequest: AuthRequest): Promise<void> {
setAuthRequestDialogSelectedId(authRequest.id);
setAuthRequestDialogDismissedId(null);
}
async function approveAuthRequest(authRequest: AuthRequest): Promise<void> { async function approveAuthRequest(authRequest: AuthRequest): Promise<void> {
if (!session) throw new Error(t('txt_vault_key_unavailable')); if (!session) throw new Error(t('txt_vault_key_unavailable'));
@@ -1133,6 +1147,7 @@ export default function App() {
requestApproved: true, requestApproved: true,
}); });
setAuthRequestDialogDismissedId(null); setAuthRequestDialogDismissedId(null);
setAuthRequestDialogSelectedId(null);
pushToast('success', t('txt_auth_request_approved')); pushToast('success', t('txt_auth_request_approved'));
await pendingAuthRequestsQuery.refetch(); await pendingAuthRequestsQuery.refetch();
} finally { } finally {
@@ -1148,6 +1163,7 @@ export default function App() {
requestApproved: false, requestApproved: false,
}); });
setAuthRequestDialogDismissedId(null); setAuthRequestDialogDismissedId(null);
setAuthRequestDialogSelectedId(null);
pushToast('success', t('txt_auth_request_denied')); pushToast('success', t('txt_auth_request_denied'));
await pendingAuthRequestsQuery.refetch(); await pendingAuthRequestsQuery.refetch();
} finally { } finally {
@@ -2002,7 +2018,7 @@ export default function App() {
onRefreshPendingAuthRequests: async () => { onRefreshPendingAuthRequests: async () => {
await pendingAuthRequestsQuery.refetch(); await pendingAuthRequestsQuery.refetch();
}, },
onApproveAuthRequest: approveAuthRequest, onApproveAuthRequest: beginApproveAuthRequest,
onDenyAuthRequest: denyAuthRequest, onDenyAuthRequest: denyAuthRequest,
onLockTimeoutChange: setLockTimeoutMinutes, onLockTimeoutChange: setLockTimeoutMinutes,
onSessionTimeoutActionChange: setSessionTimeoutAction, onSessionTimeoutActionChange: setSessionTimeoutAction,
@@ -2278,21 +2294,24 @@ export default function App() {
/> />
<AuthRequestApprovalDialog <AuthRequestApprovalDialog
open={authRequestDialogOpen} open={authRequestDialogOpen}
authRequest={latestPendingAuthRequest} authRequest={authRequestDialogRequest}
submitting={!!authRequestSubmittingId} submitting={!!authRequestSubmittingId}
onApprove={() => { onApprove={() => {
if (!latestPendingAuthRequest) return; if (!authRequestDialogRequest) return;
void approveAuthRequest(latestPendingAuthRequest).catch((error) => { void approveAuthRequest(authRequestDialogRequest).catch((error) => {
pushToast('error', error instanceof Error ? error.message : t('txt_auth_request_update_failed')); pushToast('error', error instanceof Error ? error.message : t('txt_auth_request_update_failed'));
}); });
}} }}
onDeny={() => { onDeny={() => {
if (!latestPendingAuthRequest) return; if (!authRequestDialogRequest) return;
void denyAuthRequest(latestPendingAuthRequest).catch((error) => { void denyAuthRequest(authRequestDialogRequest).catch((error) => {
pushToast('error', error instanceof Error ? error.message : t('txt_auth_request_update_failed')); pushToast('error', error instanceof Error ? error.message : t('txt_auth_request_update_failed'));
}); });
}} }}
onClose={() => setAuthRequestDialogDismissedId(latestPendingAuthRequest?.id || null)} onClose={() => {
setAuthRequestDialogSelectedId(null);
setAuthRequestDialogDismissedId(authRequestDialogRequest?.id || null);
}}
/> />
</> </>
); );