密码重置(未登录恢复)
auth.reset 命名空间提供未登录用户的三段式密码恢复:
auth.reset.sendCode({ email })— 创建 ory recovery flow + 发邮件auth.reset.verifyCode({ code })— 提交验证码把 recovery flow 推到 privileged session(recovery code 一次性、成功后作废)auth.reset.submitNewPassword({ password })— 在已提权的 session 内走 settings flow 改密码
已登录用户改密码请用 auth.settings
本 namespace 设计给「忘记密码 / 未登录」场景。已登录用户走 auth.reset 会导致原 session 失效、用户被登出。已登录场景请用 auth.settings 已登录改密码(3 段流程、session 保持)。
底层依次走 ory createBrowserRecoveryFlow → updateRecoveryFlow(method: 'code') → createBrowserSettingsFlow → updateSettingsFlow(method: 'password'),flow 链本身不变,只是拆到了 verifyCode(前两步 recovery 提权)与 submitNewPassword(后两步 settings 改密)两个方法里,提权状态靠浏览器 session cookie 携带。SDK 内部维护「当前活跃的 recovery 会话」串起前两步。
为什么拆三段
原来 code + password 一次性提交,验证码错和密码错(如「新密码不能与旧密码相同」)都从同一次调用抛出、都是 FLOW 类型,UI 无法区分,密码错会被错误地显示在验证码输入步。拆成 verifyCode(验证码错在此抛)+ submitNewPassword(密码错在此抛)后,两类错误各自在对应步骤抛出,UI 可分屏展示、互不串屏——与 auth.settings 已登录改密码的三段式(sendVerifyCode / verifyCode / updatePassword)是同一套「两屏 UX」模式。
最短链路
import { TripoAuth, TripoAuthError } from "@tripo3d/auth";
const auth = new TripoAuth({
basePath: "https://auth.tripo3d.ai",
});
// 1. 触发 ory 下发恢复邮件
await auth.reset.sendCode({ email: "user@example.com" });
// 2. 用户拿到邮件里的验证码,提交完成提权
await auth.reset.verifyCode({ code: "123456" });
// 3. 提权 session 内提交新密码
await auth.reset.submitNewPassword({
password: "new-strong-password",
});
// 4. 成功后 SDK 不写 signal cookie;业务侧通常引导用户去登录页重新登录设计要点
1. sendCode → verifyCode → submitNewPassword 是有状态序列
sendCode 创建 recovery flow 后把 { flow, csrf_token } 写入实例级 RecoverySessionHolder;verifyCode 复用同一份 flow id 提交验证码 + csrf 完成提权,成功后清掉 recovery flow ref(recovery code 一次性、防重放)并置位内部 privileged 标志;submitNewPassword 在已提权的 session 内新建 settings flow 改密。
[sendCode]
ory createBrowserRecoveryFlow → flow.id + ui.nodes
ory updateRecoveryFlow(method='code', email) → 邮件下发
holder.set({ flow, csrf_token })
[verifyCode]
holder.get() ─► cached
ory updateRecoveryFlow(method='code', code, csrf) → privileged session
holder 清 recovery flow ref(code 一次性、防重放)+ 置位 privileged 标志
[submitNewPassword]
ory createBrowserSettingsFlow → settings.id + 新 csrf (settings flow 有自己一份 csrf)
ory updateSettingsFlow(method='password', password, csrf) → 密码更新
holder.clear()要点:
- 三个方法必须在同一个
TripoAuth实例上顺序调用,跨实例 holder / 提权标志不通 - 多实例 / SSR 并发场景下 holder 互不串扰
verifyCode/submitNewPassword都不再要求传 email——recovery flow 已经绑定 identity,邮箱在 sendCode 阶段就已定型
2. 重置成功会写登录信号 cookie(直接登录)
密码重置成功后 SDK 会调 writeSignal({ provider: 'password' })。原因:
- recovery 验证码校验通过时 ory 已把当前会话认证为登录态(此后再走
submitPassword会撞 "you are already logged in") - 因此重置密码即完成一次登录,与
submitPassword/submitCode/ oidc 成功后一致写信号 - 容器据信号收尾:dialog
useConsumeSignal关窗 +emitLogin;独立登录页useReadSignal跳returnTo——用户改完密码直接进入登录态,无需重新登录
await auth.reset.submitNewPassword({ password });
// 无需再手动 redirect 到登录页:容器会因登录信号自动收尾进入登录态
toast.success("密码修改成功");3. 邮箱变更要重新 sendCode
业务 UI 改了邮箱之后必须重新 sendCode——SDK 不会替业务侧检测;旧 flow 拿来 verifyCode 会被 ory 拒绝。建议 UI 上一旦邮箱输入框值变更,就清掉「已发码」状态:
import { ref, watch } from "vue";
const email = ref("");
const codeSent = ref(false);
watch(email, () => {
codeSent.value = false;
});4. 缺前置调用(乱序)
三段有严格前后依赖,跳步调用抛 VALIDATION:
// 没先 sendCode 就 verifyCode
await auth.reset.verifyCode({ code });
// TripoAuthError(name: 'VALIDATION', message: 'reset.verifyCode requires a prior reset.sendCode call in the same session')
// 没先 verifyCode 就 submitNewPassword
await auth.reset.submitNewPassword({ password });
// TripoAuthError(name: 'VALIDATION', message: 'reset.submitNewPassword requires a prior reset.verifyCode call in the same session')业务 UI 应通过分屏(验证码步 → 密码步)+ disable 提交按钮卡住这些错误路径。
重试与重发
验证码错
验证码无效 / 过期在 verifyCode 抛 TripoAuthError(name: 'FLOW')。此时 holder 的 pending 状态已清,需要重新 sendCode 下发新验证码后再 verifyCode:
try {
await auth.reset.verifyCode({ code: input });
} catch (error) {
if (error instanceof TripoAuthError && error.name === "FLOW") {
toast.error(error.message);
// 重发码(sendCode)后重试 verifyCode
}
}密码强度错 / 新旧相同
ory 在 settings flow 阶段校验密码(如「password too short」「password breached」「新密码不能与旧密码相同」);submitNewPassword 抛 FLOW,业务侧直接 toast error.message。此时提权标志保留、不清 holder——用户换一个合规密码原地再调一次 submitNewPassword({ password }) 即可,无需重新 verifyCode 验码。仍建议 UI 层用 zod / yup 做基本强度校验,把 ory 拒绝当兜底。
提权过期
verifyCode 与 submitNewPassword 之间停留过久,ory 会判定提权已过期(回 session_refresh_required)。此时 submitNewPassword 抛 TripoAuthError(name: 'PRIVILEGED_SESSION_EXPIRED'),SDK 清掉内部提权标志。业务应把 UI 切回验证码步,引导用户重新 sendCode + verifyCode:
try {
await auth.reset.submitNewPassword({ password });
} catch (error) {
if (
error instanceof TripoAuthError &&
error.name === "PRIVILEGED_SESSION_EXPIRED"
) {
toast.warning("验证已过期,请重新发送验证码");
backToVerifyStep();
}
}验证码过期 / flow 过期
Ory recovery flow 默认 5 分钟过期。过期后 verifyCode 抛 TripoAuthError(name: 'FLOW'),业务需要重新 sendCode:
async function onResend() {
await auth.reset.sendCode({ email });
startCountdown(60);
}端到端示例(Vue)
分两屏:第一屏收邮箱 + 验证码,verifyCode 成功后进入第二屏收新密码 submitNewPassword。错误分流:验证码错留在验证码步,PRIVILEGED_SESSION_EXPIRED 退回验证码步,其它密码错留在密码步原地重试。
<script setup lang="ts">
import { ref } from "vue";
import { TripoAuthError } from "@tripo3d/auth";
import { auth } from "~/plugins/auth";
type Step = "email" | "password";
const step = ref<Step>("email");
const email = ref("");
const code = ref("");
const password = ref("");
const codeSent = ref(false);
const cooldown = ref(0);
const verifying = ref(false);
const submitting = ref(false);
async function onSendCode() {
try {
await auth.reset.sendCode({ email: email.value });
codeSent.value = true;
startCooldown(60);
} catch (err) {
handleAuthError(err);
}
}
// 第一屏:提交验证码完成提权,进入密码步
async function onVerifyCode() {
if (!codeSent.value) return;
verifying.value = true;
try {
await auth.reset.verifyCode({ code: code.value });
step.value = "password";
} catch (err) {
// 验证码错 / 过期留在验证码步;引导重发码
handleAuthError(err);
} finally {
verifying.value = false;
}
}
// 第二屏:提权 session 内提交新密码
async function onSubmit() {
if (password.value.length < 8) {
toast.error("密码至少 8 位");
return;
}
submitting.value = true;
try {
await auth.reset.submitNewPassword({ password: password.value });
toast.success("密码修改成功,请用新密码登录");
router.push("/login");
} catch (err) {
if (
err instanceof TripoAuthError &&
err.name === "PRIVILEGED_SESSION_EXPIRED"
) {
// 提权过期:退回验证码步重发码
toast.warning("验证已过期,请重新发送验证码");
step.value = "email";
return;
}
// 其它(密码强度错 / 新旧相同)留在密码步,原地改密码重试即可
handleAuthError(err);
} finally {
submitting.value = false;
}
}
function handleAuthError(err: unknown) {
if (err instanceof TripoAuthError) {
toast.error(err.message);
return;
}
toast.error("网络异常,请稍后重试");
}
function startCooldown(seconds: number) {
cooldown.value = seconds;
const t = setInterval(() => {
cooldown.value -= 1;
if (cooldown.value <= 0) clearInterval(t);
}, 1000);
}
</script>
<template>
<!-- 第一屏:邮箱 + 验证码 -->
<form v-if="step === 'email'" @submit.prevent="onVerifyCode">
<input v-model="email" type="email" placeholder="邮箱" required />
<div>
<input
v-model="code"
type="text"
inputmode="numeric"
placeholder="验证码"
maxlength="6"
/>
<button
type="button"
:disabled="cooldown > 0 || !email"
@click="onSendCode"
>
{{ cooldown > 0 ? `${cooldown}s` : "发送验证码" }}
</button>
</div>
<button type="submit" :disabled="!codeSent || verifying">下一步</button>
</form>
<!-- 第二屏:新密码 -->
<form v-else @submit.prevent="onSubmit">
<input
v-model="password"
type="password"
placeholder="新密码"
minlength="8"
required
/>
<button type="submit" :disabled="submitting">重置密码</button>
</form>
</template>API 参考
| 方法 | 签名 | 说明 |
|---|---|---|
auth.reset.sendCode | (args: { email }) => Promise<void> | 创建 ory recovery flow + 触发邮件下发;flow id + csrf 写入实例级 holder |
auth.reset.verifyCode | (args: { code }) => Promise<void> | 提交验证码到 recovery flow 完成提权 → 当前 session 被 ory 标记为 privileged;成功后清 recovery flow ref(一次性、防重放)+ 置位 privileged 标志 |
auth.reset.submitNewPassword | (args: { password }) => Promise<void> | 在已提权的 session 内走 settings flow 改密码;成功后清 holder 全部状态 |
常见错误
完整错误分类见 错误处理:
FLOW— 验证码错 / 过期(verifyCode)/ 新密码强度不达标或新旧相同(submitNewPassword)(业务直接 toasterror.message)NETWORK— ory 调用 fetch 抛错VALIDATION—verifyCode在没先sendCode、或submitNewPassword在没先verifyCode的情况下被调用PRIVILEGED_SESSION_EXPIRED—submitNewPassword时提权已过期(verifyCode与submitNewPassword间隔过久);引导用户回验证码步重发码INTERNAL— flow ui 节点取不到 csrf_token(理论上 ory 不应出现)
下一步
- 验证码流程 — 验证码登录入口,与 reset 共享一套 UI 心智
- 错误处理 —
TripoAuthError完整分类与 catch 模板 - Playground