本指南概述了针对 Kinde 的特定设置,流程类似Convex & Clerk集成,但本指南的重点是讲解如何将 Kinde 集成到 Convex 中。
它解答了许多开发者的问题,这些问题是由Kinde开发者社区关心的。详情可在这里查看:Kinde 社区 - 将 Convex 与 Kinde 集成
该教程清晰地列出了如何使用Kinde认证与Convex集成的实际步骤,并按照最佳实践。
Kinde 是一个认证平台,支持无密码登录方式,如魔法链接(Magic Link)、短信验证码(SMS)或身份验证器应用程序(Authenticator App)。它还支持多因素认证(MFA)以增强安全性,支持基于 SAML 的企业级单点登录(SSO),并提供强大的用户管理工具,帮助企业更好地管理用户。
示例: 使用Kinde的凸形认证(点击这里:Convex Authentication with Kinde)
如果你在使用 Next.js,可以参考 Convex 的 Next.js 设置指南。
开始吧此指南假设你已经集成了 Convex 的可正常运行的 Next.js 应用。如果没有,请先参考 Convex Next.js 快速入门。然后:
- 注册 Kinde 账号
在kinde.com/register 注册一个免费的 Kinde 账户。
- 在Kindle创办一个企业
输入您公司或应用的名称:
- 选择你的技术组合
选择你用来构建此应用程序的技术栈和工具。
- 选择认证方式
选择你想要用户如何签入。
- 将您的应用连接到Kinde
让你的 Next.js 应用连接到 Kinde。
- 创建认证设置
从你的 .env.local
文件中复制 KINDE_ISSUER_URL
这个字段。进入 convex
文件夹后,然后创建一个新的 auth.config.ts
文件,用于设置服务器端验证访问令牌的配置信息。
粘贴 _KINDE_ISSUERURL 并将 applicationID
设置为 "convex"
(无需修改 "aud"
Claims 字段的值)。
const authConfig = {
providers: [
{
domain: process.env.KINDE_ISSUER_URL, // 例如:https://barswype.kinde.com
applicationID: "应用ID",
},
]
};
export default authConfig;
点击这里进入全屏模式 点击这里退出全屏模式
- 设置 Convex & Kinde Webhook
在 Kinde 仪表盘中,进入 设置 > Webhooks > 点击 添加 Webhook > 命名为 Webhook 并粘贴您的 Convex 端点 URL,例如 https://<您的-convex-app>.convex.site/kinde
。
选择要触发的事件,比如“user.created
”和“user.deleted
”。
现在回到你的代码部分。打开你的 convex/
文件夹,然后新建一个名为 http.ts
的文件,接着复制并粘贴这段代码。
import { httpRouter } from "convex/server";
import { internal } from "./_generated/api";
import { httpAction } from "./_generated/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
type KindeEventData = {
user: {
id: string;
email: string;
first_name?: string;
last_name?: string | null;
is_password_reset_requested: boolean;
is_suspended: boolean;
organizations: {
code: string;
permissions: string | null;
roles: string | null;
}[];
phone?: string | null;
username?: string | null;
image_url?: string | null;
};
};
type KindeEvent = {
type: string;
data: KindeEventData;
};
const http = httpRouter();
const handleKindeWebhook = httpAction(async (ctx, request) => {
const event = await validateKindeRequest(request);
if (!event) {
return new Response("无效请求", { status: 400 });
}
switch (event.type) {
case "user.created":
await ctx.runMutation(internal.users.createUserKinde, {
kindeId: event.data.user.id,
email: event.data.user.email,
username: event.data.user.first_name || ""
});
break;
{/**
case "user.updated":
const existingUserOnUpdate = await ctx.runQuery(
internal.users.getUserKinde,
{ kindeId: event.data.user.id }
);
if (existingUserOnUpdate) {
await ctx.runMutation(internal.users.updateUserKinde, {
kindeId: event.data.user.id,
email: event.data.user.email,
username: event.data.user.first_name || ""
});
} else {
console.warn(
`未找到需要更新的用户,其 kindeId 为 ${event.data.user.id}.`
);
}
break;
*/}
case "user.deleted":
const userToDelete = await ctx.runQuery(internal.users.getUserKinde, {
kindeId: event.data.user.id,
});
if (userToDelete) {
await ctx.runMutation(internal.users.deleteUserKinde, {
kindeId: event.data.user.id,
});
} else {
console.warn(
`未找到需要删除的用户,其 kindeId 为 ${event.data.user.id}.`
);
}
break;
default:
console.warn(`未处理的事件类型:${event.type}`);
}
return new Response(null, { status: 200 });
});
// ===== JWT 验证部分 =====
async function validateKindeRequest(request: Request): Promise<KindeEvent | null> {
try {
if (request.headers.get("content-type") !== "application/jwt") {
console.error("无效的 Content-Type。期望 application/jwt");
return null;
}
const token = await request.text(); // JWT 作为纯文本发送在请求体中。
const JWKS_URL = `${process.env.KINDE_ISSUER_URL}/.well-known/jwks.json`;
const JWKS = createRemoteJWKSet(new URL(JWKS_URL));
const { payload } = await jwtVerify(token, JWKS);
// 确保负载包含预期属性
if (
typeof payload === "object" &&
payload !== null &&
"type" in payload &&
"data" in payload
) {
return {
type: payload.type as string,
data: payload.data as KindeEventData,
};
} else {
console.error("负载结构不符合预期要求");
return null;
}
} catch (error) {
console.error("JWT 认证失败", error);
return null;
}
}
http.route({
path: "/kinde",
method: "POST",
handler: handleKindeWebhook,
});
export default http;
点击全屏按钮进入全屏 点击退出按钮退出全屏
请参阅这篇帖子以获取有关在 Kinde 和 Convex 之间设置 webhook 的详细指南:参阅这篇帖子。
- 应用你的更改
运行命令 npx convex dev
以让您的配置自动同步到后端。
在终端中运行此命令: npx convex dev
进入全屏 退出全屏
- 安装 Kindle 电子书阅读器
在新的终端窗口里,打开并安装 Kinde Next.js 库(或npm包)。
npm install @kinde-oss/kinde-auth-nextjs
要安装这个包,请在终端中输入以上命令。@kinde-oss/kinde-auth-nextjs 是一个用于 Next.js 的 Kinde 认证插件。
点击全屏模式 点一下退出全屏
- 复制一下你的 Kindle 设置.
在 Kinde 仪表盘上,点击你应用的 查看详细信息。
往下滚动,然后复制你的 Client ID 和 Client secret
- 设置 Kinde 身份验证路由处理器
创建一个文件 app/api/auth/[kindeAuth]/route.ts
在你的 Next.js 项目里。在文件 route.ts
中复制以下代码:
import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
// 这里引入了身份验证处理函数,并设置了GET请求的处理方式
export const GET = handleAuth();
全屏: 进入 全屏退出
这将处理你 Next.js 应用中的 Kinde 认证接口。
重要提示! Kinde SDK 依赖于该文件位于上述指定位置如下。
- 为 Convex 和 Kinde 集成配置一个新的提供商
在你的项目根目录下创建一个 providers
文件夹,并添加一个新的文件 ConvexKindeProvider.tsx
。这个 provider 会集成 Convex 和 Kinde,并包裹整个应用。
在 ConvexKindeProvider.tsx
文件中,将 ConvexProvider
包裹在 KindeProvider
中,并使用 useKindeAuth
获取认证令牌,然后把它传给 Convex。
将 domain
、clientId
和 redirectUri
传递给 KindeProvider
。
"use client";
import { ReactNode, useEffect } from "react";
import { KindeProvider, useKindeAuth } from "@kinde-oss/kinde-auth-nextjs";
import { ConvexProvider, ConvexReactClient, AuthTokenFetcher } from "convex/react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL as string);
const ConvexKindeProvider = ({ children }: { children: ReactNode }) => {
const { getToken } = useKindeAuth();
useEffect(() => {
const fetchToken: AuthTokenFetcher = async () => {
const token = await getToken();
return token || null;
};
if (typeof getToken === "function") {
convex.setAuth(fetchToken);
}
}, [getToken]);
return (
<KindeProvider
domain={process.env.NEXT_PUBLIC_KINDE_DOMAIN as string}
clientId={process.env.NEXT_PUBLIC_KINDE_CLIENT_ID as string}
redirectUri={process.env.NEXT_PUBLIC_KINDE_REDIRECT_URI as string}
>
<ConvexProvider client={convex}>{children}</ConvexProvider>
</KindeProvider>
);
};
export default ConvexKindeProvider;
全屏模式 (按 esc 退出)
将配置好的 ConvexKindeProvider.tsx
导入到主 layout.tsx
文件中。
import type { Metadata } from "next";
import "./globals.css";
import ConvexKindeProvider from "@/providers/ConvexKindeProvider";
export const metadata: Metadata = {
title: "创建 Next 应用",
description: "Kinde 和 Convex 演示",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ConvexKindeProvider>
<html lang="en">
<body>
{children}
</body>
</html>
</ConvexKindeProvider>
);
};
全屏;退出全屏
- 基于认证状态显示界面
你可以使用来自"convex/react"
和"@kinde-oss/kinde-auth-nextjs"
的组件来控制显示哪个界面,这取决于用户是否已登录。
要开始,请创建一个允许用户登录和登出的shell。
因为 DisplayContent
组件是 Authenticated
的子组件,因此在其及其子组件内部,认证都是有保证的,Convex 查询也可以如此依赖。
"use client";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
import {
RegisterLink,
LoginLink,
LogoutLink,
} from "@kinde-oss/kinde-auth-nextjs/components";
import { Authenticated, Unauthenticated, useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
function App() {
const { isAuthenticated, getUser } = useKindeBrowserClient();
const user = getUser();
return (
<main>
<Unauthenticated>
<LoginLink postLoginRedirectURL="/dashboard">点击登录</LoginLink>
<RegisterLink postLoginRedirectURL="/welcome">点击注册</RegisterLink>
</Unauthenticated>
<Authenticated>
{isAuthenticated && user ? (
<div>
<p>名: {user.given_name} {user.family_name}</p>
<p>邮箱: {user.email}</p>
<p>手机号码: {user.phone_number}</p>
</div>
) : null}
<DisplayContent />
<LogoutLink>点击登出</LogoutLink>
</Authenticated>
</main>
);
}
function DisplayContent() {
const { user } = useKindeBrowserClient();
const files = useQuery(api.files.getForCurrentUser, {
kindeId: user?.id,
});
return <div>认证内容: {files?.length}</div>;
}
export default App;
全屏 退出全屏
- 在你的 Convex 函数中使用身份验证状态
如果用户通过身份验证,你可以通过 ctx.auth.getUserIdentity
获取 Kinde 发送的 JWT 中的用户信息。
如果用户未通过身份验证,ctx.auth.getUserIdentity
将返回 null
。
确保调用此查询的组件是Authenticated
的子组件,该Authenticated
来自"convex/react"
,否则会在页面加载时抛出异常。
import { query } from "./_generated/server";
export const getForCurrentUser = query({
args: { kindeId: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
throw new Error("未通过身份验证");
}
const files = await ctx.db
.query("files")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.collect();
if (!files) {
throw new Error("未找到此用户的任何文件");
}
return files;
},
});
全屏 退出全屏
登录和退出流程现在你设置好了,你可以使用LoginLink
组件来实现你的应用的登录功能。
如果您更喜欢为您的应用自定义登录或注册表单,参阅这篇帖子。
import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LoginLink>登录</LoginLink>
全屏模式(进入/退出)
你可以使用 LogoutLink
组件,让用户轻松地注销。
import {LogoutLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LogoutLink>登出链接</LogoutLink>
切换到全屏 / 退出全屏
登录和未登录时的视图使用useConvexAuth()
钩子而不是Kinde的useKindeBrowserClient
钩子来检查用户是否已登录与否。useConvexAuth
钩子确保浏览器获取了用于向Convex后端发送身份验证请求的认证令牌,并且该令牌已经被Convex后端验证。
import { useConvexAuth } from "convex/react";
function App() {
const { isLoading, isAuthenticated } = useConvexAuth();
return (
<div className="App">
{isAuthenticated ? "已登录状态" : "未登录或正在加载中"}
</div>
);
}
全屏 开启 全屏 关闭
功能中的用户信息请参阅 函数中的认证 以了解如何访问已验证用户的信息,在查询、突变和操作中。
参考在Convex的数据库中存储用户,了解如何在数据库中存储用户信息。
Next.js 中的用户信息管理您可以从 Kinde 的 useKindeBrowserClient
或 getKindeServerSession
钩子函数获取经过身份验证的用户的名字和邮箱地址等信息。有关可用字段的列表,请参阅用户信息对象。
"use client";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
export default function Hero() {
const { user } = useKindeBrowserClient();
return <span>您是 {user?.given_name}, {user?.family_name}</span>;
};
全屏 退出全屏
配置开发和生产环境让你的 Kinde 实例在 Convex 开发和生产部署之间有所不同,可以通过在 Convex 仪表盘上配置环境变量来实现这一点。
配置后端系统
Kinde 的默认配置设置为生产环境。若要使用自定义域名,请参阅此指南[在此处](https://docs.kinde.com/build/domains/pointing-your-domain/#:~:text=转到设置 > 环境 > 自定义域名,&text=在弹出窗口中,输入您的域名 <your_app>.kinde.com,然后点击保存)。
开发设置
打开你开发部署的设置,在Convex 仪表板中将你 .env.local
文件中的所有变量添加进去。
生产环境的设置
同样,在突出的仪表板上,在左侧菜单中切换到生产部署,并在你的 .env.local
文件里设置相应的变量。
现在运行命令 npx convex deploy
来切换到新的配置。
npx convex 部署命令
进入全屏,退出全屏
发布你的 Next.js 应用
根据您所使用的托管平台,在生产环境中设置相应的环境变量。参见托管平台以获取更多信息。
调试身份验证如果用户成功完成了 Kinde 的注册或登录过程,并且在被保存到您的 Convex 数据库之后,页面会重定向回用户,但 useConvexAuth
却返回 isAuthenticated: false
,那么您的后端可能没有正确配置。
你的 convex/
目录下的 auth.config.ts
文件列出了已配置的身份验证提供程序。在添加新的提供程序后,你必须运行 npx convex dev
或 npx convex deploy
来将配置同步到后端服务器。
对于更详细的调试方法,请参阅身份验证调试。
技术细节实际上,认证流程内部是这样的。
- 用户点注册或登录按钮。
- 用户会被引导到 Kinde 的页面,在那里他们可以通过您设定的方法注册或登录。
- 接着,他们的信息将通过 webhook 发送到 Convex 并被安全存储,然后用户会被立即重定向回您的页面或您通过 Kinde 的
postLoginRedirectURL
属性设置的其他页面。 KindeProvider
现在知道了用户已通过身份验证。useKindeAuth
和AuthTokenFetcher
从 Kinde 获取一个授权令牌。- 然后 react 的
useEffect
钩子将其设置为 Convex 的setAuth
实例。 ConvexProvider
将其传递给 Convex 后端验证。- 您的 Convex 后端会从 Kinde 获取 domain、clientId 和 redirectUri,以验证令牌签名的有效。
ConvexProvider
收到身份验证成功的通知,现在您的整个应用程序都知道用户已通过 Convex 进行身份验证。useConvexAuth
返回isAuthenticated: true
,并且Authenticated
组件会渲染其子组件。
ConvexKindeProvider.tsx
文件中的设置负责在需要时刷新令牌,以确保用户始终与后端保持认证状态。