凸和金德
开始
本指南概述了针对 Convex 的 Kinde 特定设置,遵循与Convex 和 Clerk集成类似的流程,但重点介绍如何将 Kinde 与 Convex 集成。
它解答了 Kinde 开发者社区提出的许多问题,这些问题可以在这里找到:Kinde 社区 - 将 Convex 与 Kinde 集成
本教程提供了将 Kinde 身份验证与 Convex 集成的清晰、可操作的步骤,同时遵循最佳实践。
Kinde是一个身份验证平台,支持用户通过魔术链接、短信验证码或身份验证器应用程序等方式进行无密码登录。它还支持多因素身份验证 (MFA) 以增强安全性,支持基于 SAML 的企业级单点登录 (SSO),并为企业提供强大的用户管理工具。
如果您正在使用 Next.js,请参阅Convex 的 Next.js 设置指南。
开始
本指南假设您已使用 Convex 构建了一个可运行的 Next.js 应用。如果没有,请先按照Convex Next.js 快速入门进行操作。然后:
- 注册 Kinde
在kinde.com/register注册一个免费的 Kinde 帐户。
- 在 Kinde 创建企业
输入您的企业或应用程序的名称。
- 选择您的技术堆栈
选择用于构建此应用程序的技术堆栈或工具。
- 选择身份验证方法
选择您希望用户登录的方式。
- 将您的应用连接到 Kinde
将您的 Next.js 应用程序连接到 Kinde。
- 创建身份验证配置
KINDE_ISSUER_URL
从文件中复制您的内容.env.local
。进入convex
文件夹并创建一个新文件,auth.config.ts
其中包含用于验证访问令牌的服务器端配置。
粘贴KINDE_ISSUER_URL并将其设置applicationID
为"convex"
(值和"aud"
声明字段)。
const authConfig = {
providers: [
{
domain: process.env.KINDE_ISSUER_URL, // Example: https://barswype.kinde.com
applicationID: "convex",
},
]
};
export default authConfig;
- 设置 Convex 和 Kinde Webhook
在 Kinde 仪表板中,转到设置> Webhook > 单击添加 Webhook > 命名 webhook 并粘贴您的 Convex 端点 URL,例如https://<your-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("Invalid request", { 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(
`No user found to update with 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(
`No user found to delete with kindeId ${event.data.user.id}.`
);
}
break;
default:
console.warn(`Unhandled event type: ${event.type}`);
}
return new Response(null, { status: 200 });
});
// ===== JWT Validation =====
async function validateKindeRequest(request: Request): Promise<KindeEvent | null> {
try {
if (request.headers.get("content-type") !== "application/jwt") {
console.error("Invalid Content-Type. Expected application/jwt");
return null;
}
const token = await request.text(); // JWT is sent as raw text in the body.
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);
// Ensure payload contains the expected properties
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("Payload does not match the expected structure");
return null;
}
} catch (error) {
console.error("JWT verification failed", error);
return null;
}
}
http.route({
path: "/kinde",
method: "POST",
handler: handleKindeWebhook,
});
export default http;
有关在 Kinde 和 Convex 之间设置 webhook 的详细指南,请参阅此帖子。
- 部署您的更改
运行npx convex dev
以自动将您的配置同步到您的后端。
npx convex dev
- 安装 Kinde
在新的终端窗口中,安装 Kinde Next.js 库
npm install @kinde-oss/kinde-auth-nextjs
- 复制您的 Kinde 环境变量
在 Kinde 仪表板上,单击应用程序上的查看详细信息。
向下滚动并复制您的客户端 ID和客户端密钥
- 设置 Kinde 身份验证路由处理程序
在 Next.js 项目中创建以下文件app/api/auth/[kindeAuth]/route.ts
。在文件中route.ts
复制并粘贴以下代码:
import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export const GET = handleAuth();
这将处理 Next.js 应用中的 Kinde Auth 端点。
重要! Kinde SDK 依赖于此文件,该文件位于上述指定的位置。
- 为 Convex 和 Kinde 集成配置新的提供程序
providers
在根目录中创建一个文件夹并添加一个新文件ConvexKindeProvider.tsx
。此提供程序将把 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;
将您的配置导入ConvexKindeProvider.tsx
到您的主layout.tsx
文件。
import type { Metadata } from "next";
import "./globals.css";
import ConvexKindeProvider from "@/providers/ConvexKindeProvider";
export const metadata: Metadata = {
title: "Create Next App",
description: "Kinde and Convex Demo",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ConvexKindeProvider>
<html lang="en">
<body>
{children}
</body>
</html>
</ConvexKindeProvider>
);
};
- 根据身份验证状态显示 UI
"convex/react"
您可以使用和提供的组件来控制用户登录或注销时显示的 UI "@kinde-oss/kinde-auth-nextjs"
。
首先创建一个允许用户登录和注销的 shell。
因为DisplayContent
组件是的子组件Authenticated
,所以它和它的任何子组件内的身份验证都得到保证,并且凸查询可以需要它。
"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">Sign in</LoginLink>
<RegisterLink postLoginRedirectURL="/welcome">Sign up</RegisterLink>
</Unauthenticated>
<Authenticated>
{isAuthenticated && user ? (
<div>
<p>Name: {user.given_name} {user.family_name}</p>
<p>Email: {user.email}</p>
<p>Phone: {user.phone_number}</p>
</div>
) : null}
<DisplayContent />
<LogoutLink>Log out</LogoutLink>
</Authenticated>
</main>
);
}
function DisplayContent() {
const { user } = useKindeBrowserClient();
const files = useQuery(api.files.getForCurrentUser, {
kindeId: user?.id,
});
return <div>Authenticated content: {files?.length}</div>;
}
export default App;
- 在凸函数中使用身份验证状态
如果客户端通过身份验证,则可以通过 访问 Kinde 发送的 JWT 中存储的信息ctx.auth.getUserIdentity
。
如果客户端未经过身份验证,ctx.auth.getUserIdentity
将返回null
。
确保调用此查询的组件是Authenticated
from"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("Not authenticated");
}
const files = await ctx.db
.query("files")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.collect();
if (!files) {
throw new Error("No files found for this user");
}
return files;
},
});
登录和注销流程
现在您已完成所有设置,您可以使用该LoginLink
组件为您的应用创建登录流程。
如果您希望为您的应用配置自定义登录/注册表单,请参阅此帖子。
import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LoginLink>Sign in</LoginLink>
要启用注销流程,您可以使用该LogoutLink
组件让用户无缝注销您的应用程序。
import {LogoutLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LogoutLink>Log out</LogoutLink>
登录和注销视图
当您需要检查用户是否登录时,请使用useConvexAuth()
钩子而不是 Kinde 的钩子。钩子确保浏览器已获取向 Convex 后端发出经过身份验证的请求所需的身份验证令牌,并且 Convex 后端已对其进行了验证:useKindeBrowserClient
useConvexAuth
import { useConvexAuth } from "convex/react";
function App() {
const { isLoading, isAuthenticated } = useConvexAuth();
return (
<div className="App">
{isAuthenticated ? "Logged in" : "Logged out or still loading"}
</div>
);
}
函数中的用户信息
请参阅函数中的身份验证,了解如何在查询、变异和操作中访问有关经过身份验证的用户的信息。
请参阅在 Convex 数据库中存储用户,了解如何在 Convex 数据库中存储用户信息。
Next.js 中的用户信息
useKindeBrowserClient
您可以从 Kinde或钩子中访问已验证用户的信息,例如其姓名和电子邮件地址getKindeServerSession
。请参阅用户信息对象以获取可用字段的列表:
"use client";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
export default function Hero() {
const { user } = useKindeBrowserClient();
return <span>Logged in as {user?.given_name} {user?.family_name}</span>;
};
配置开发和生产实例
要在 Convex 开发和生产部署之间配置不同的 Kinde 实例,您可以使用在 Convex 仪表板上配置的环境变量。
配置后端
Kinde 的默认配置适用于生产环境。如需使用自定义域名而非已颁发的<your_app>.kinde.com
域名,请参阅本指南。
开发配置
在 Convex仪表板上打开您的开发部署的设置并从那里添加所有变量.env.local
:
生产配置
类似地,在凸仪表板上,在左侧菜单中切换到您的生产部署,并从那里设置变量.env.local
。
现在通过运行切换到新配置npx convex deploy
。
npx convex deploy
部署你的 Next.js 应用
根据您的托管平台,在生产环境中设置环境变量。请参阅托管。
调试身份验证
如果用户成功完成 Kinde 注册或登录流程,并在保存到您的 Convex 数据库并重定向回您的页面后useConvexAuth
显示isAuthenticated: false
,则可能是您的后端配置不正确。
auth.config.ts
目录中的文件包含convex/
已配置的身份验证提供程序列表。添加新的提供程序后,必须运行npx convex dev
或npx convex deploy
才能将配置同步到后端。
有关更详细的调试步骤,请参阅调试身份验证。
引擎盖下
身份验证流程如下:
- 用户点击注册或登录按钮。
- 用户被重定向到托管的 Kinde 页面,他们可以通过您在 Kinde 中配置的任何方法注册或登录。
- 成功注册或登录后,他们的详细信息将通过 webhook 发送并安全地存储在 Convex 中,之后他们会立即重定向回您的页面,或者您通过 Kinde
postLoginRedirectURL
prop 配置的其他页面。 - 现在
KindeProvider
知道用户已经通过身份验证。 - 并从 Kinde 获取身份验证令牌
useKindeAuth
。AuthTokenFetcher
- 然后,react
useEffect
hook 将此令牌设置为setAuth
Convex 的一个实例。 - 然后
ConvexProvider
将此令牌传递到您的 Convex 后端进行验证。 - 您的 Convex 后端从 Kinde 检索域、clientId 和 redirectUri 以检查令牌的签名是否有效。
- 收到身份验证成功通知
ConvexProvider
,现在整个应用程序都知道用户已通过 Convex 进行身份验证。useConvexAuth
返回isAuthenticated: true
并且Authenticated
组件呈现其子项。
文件中的配置ConvexKindeProvider.tsx
负责在需要时重新获取令牌,以确保用户与后端保持身份验证。