fix(admin): remove operation-level email reauthentication
This commit is contained in:
@@ -39,10 +39,6 @@ export type AdminAccessResult =
|
||||
|
||||
export const ADMIN_MFA_PROOF_COOKIE = "jyotisha-admin.mfa";
|
||||
export const ADMIN_MFA_PROOF_TTL_MS = 10 * 60 * 1_000;
|
||||
export const HIGH_RISK_ADMIN_CHALLENGE_COOKIE = "jyotisha-admin.reauth-challenge";
|
||||
export const HIGH_RISK_ADMIN_CHALLENGE_TTL_MS = 5 * 60 * 1_000;
|
||||
export const HIGH_RISK_ADMIN_PROOF_COOKIE = "jyotisha-admin.reauth";
|
||||
export const HIGH_RISK_ADMIN_PROOF_TTL_MS = 5 * 60 * 1_000;
|
||||
|
||||
type AdminMfaProofContext = {
|
||||
userId: string;
|
||||
@@ -56,21 +52,10 @@ type AdminMfaProofClaims = AdminMfaProofContext & {
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
type HighRiskAdminProofContext = AdminMfaProofContext & {
|
||||
permission: AdminPermission;
|
||||
};
|
||||
|
||||
type HighRiskAdminProofClaims = HighRiskAdminProofContext & {
|
||||
version: 1;
|
||||
issuedAt: number;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
export type AdminMfaStatus = {
|
||||
required: boolean;
|
||||
enrolled: boolean;
|
||||
verified: boolean;
|
||||
highRiskWritesEnabled: boolean;
|
||||
};
|
||||
|
||||
export function authorizeAdminAccess(
|
||||
@@ -167,7 +152,6 @@ export function resolveAdminMfaStatus(
|
||||
required,
|
||||
enrolled,
|
||||
verified: currentSessionVerified,
|
||||
highRiskWritesEnabled: !required || currentSessionVerified,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,7 +172,7 @@ function signProof(
|
||||
|
||||
function encodeProof(
|
||||
purpose: string,
|
||||
claims: AdminMfaProofClaims | HighRiskAdminProofClaims,
|
||||
claims: AdminMfaProofClaims,
|
||||
proofSecret: string,
|
||||
sessionToken: string,
|
||||
): string {
|
||||
@@ -255,81 +239,3 @@ export function verifyAdminMfaProof(
|
||||
&& claims.issuedAt! <= now
|
||||
&& now < claims.expiresAt!;
|
||||
}
|
||||
|
||||
export function issueHighRiskAdminChallenge(
|
||||
context: HighRiskAdminProofContext,
|
||||
proofSecret: string,
|
||||
sessionToken: string,
|
||||
now = Date.now(),
|
||||
): string {
|
||||
return encodeProof("jyotisha-admin-reauth-challenge-v1", {
|
||||
version: 1,
|
||||
...context,
|
||||
issuedAt: now,
|
||||
expiresAt: now + HIGH_RISK_ADMIN_CHALLENGE_TTL_MS,
|
||||
} satisfies HighRiskAdminProofClaims, proofSecret, sessionToken);
|
||||
}
|
||||
|
||||
export function verifyHighRiskAdminChallenge(
|
||||
proof: string | undefined,
|
||||
context: HighRiskAdminProofContext,
|
||||
proofSecret: string,
|
||||
sessionToken: string,
|
||||
now = Date.now(),
|
||||
): boolean {
|
||||
const claims = decodeProof<HighRiskAdminProofClaims>(
|
||||
proof,
|
||||
"jyotisha-admin-reauth-challenge-v1",
|
||||
proofSecret,
|
||||
sessionToken,
|
||||
);
|
||||
return claims?.version === 1
|
||||
&& claims.userId === context.userId
|
||||
&& claims.sessionId === context.sessionId
|
||||
&& claims.permission === context.permission
|
||||
&& claims.origin === context.origin
|
||||
&& Number.isSafeInteger(claims.issuedAt)
|
||||
&& Number.isSafeInteger(claims.expiresAt)
|
||||
&& claims.expiresAt! - claims.issuedAt! === HIGH_RISK_ADMIN_CHALLENGE_TTL_MS
|
||||
&& claims.issuedAt! <= now
|
||||
&& now < claims.expiresAt!;
|
||||
}
|
||||
|
||||
export function issueHighRiskAdminProof(
|
||||
context: HighRiskAdminProofContext,
|
||||
proofSecret: string,
|
||||
sessionToken: string,
|
||||
now = Date.now(),
|
||||
): string {
|
||||
return encodeProof("jyotisha-admin-reauth-v1", {
|
||||
version: 1,
|
||||
...context,
|
||||
issuedAt: now,
|
||||
expiresAt: now + HIGH_RISK_ADMIN_PROOF_TTL_MS,
|
||||
} satisfies HighRiskAdminProofClaims, proofSecret, sessionToken);
|
||||
}
|
||||
|
||||
export function verifyHighRiskAdminProof(
|
||||
proof: string | undefined,
|
||||
context: HighRiskAdminProofContext,
|
||||
proofSecret: string,
|
||||
sessionToken: string,
|
||||
now = Date.now(),
|
||||
): boolean {
|
||||
const claims = decodeProof<HighRiskAdminProofClaims>(
|
||||
proof,
|
||||
"jyotisha-admin-reauth-v1",
|
||||
proofSecret,
|
||||
sessionToken,
|
||||
);
|
||||
return claims?.version === 1
|
||||
&& claims.userId === context.userId
|
||||
&& claims.sessionId === context.sessionId
|
||||
&& claims.permission === context.permission
|
||||
&& claims.origin === context.origin
|
||||
&& Number.isSafeInteger(claims.issuedAt)
|
||||
&& Number.isSafeInteger(claims.expiresAt)
|
||||
&& claims.expiresAt! - claims.issuedAt! === HIGH_RISK_ADMIN_PROOF_TTL_MS
|
||||
&& claims.issuedAt! <= now
|
||||
&& now < claims.expiresAt!;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
} from "./auth";
|
||||
import {
|
||||
ADMIN_MFA_PROOF_COOKIE,
|
||||
HIGH_RISK_ADMIN_PROOF_COOKIE,
|
||||
isTrustedAdminMutationRequest,
|
||||
resolveAdminMfaStatus,
|
||||
verifyAdminMfaProof,
|
||||
verifyHighRiskAdminProof,
|
||||
type AdminMfaStatus,
|
||||
} from "./auth-policy";
|
||||
import { adminErrorResponse } from "./admin-error-response";
|
||||
@@ -86,30 +84,6 @@ export function readAdminMfaStatus(
|
||||
return resolveAdminMfaStatus(session.requiresMfa, enrolled, verified);
|
||||
}
|
||||
|
||||
export async function requireHighRiskAdminMutation(
|
||||
request: Request,
|
||||
permission: AdminPermission,
|
||||
): Promise<AdminSession> {
|
||||
const session = await requireAdminMutation(request, permission);
|
||||
|
||||
const origin = new URL(request.url).origin;
|
||||
const valid = verifyHighRiskAdminProof(
|
||||
requestCookie(request, HIGH_RISK_ADMIN_PROOF_COOKIE),
|
||||
{
|
||||
userId: session.user.id,
|
||||
sessionId: session.identitySession.id,
|
||||
permission,
|
||||
origin,
|
||||
},
|
||||
adminProofSigningSecret(),
|
||||
session.identitySession.token,
|
||||
);
|
||||
if (!valid) {
|
||||
throw new AdminAuthorizationError("请先使用邮箱验证码重新认证", 403);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
export function invalidQueryResponse(details?: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: "查询参数不正确", ...(details ? { details } : {}) },
|
||||
|
||||
Reference in New Issue
Block a user