Convex 和 Kinde 开始使用

2025-06-10

凸和金德

开始

本指南概述了针对 Convex 的 Kinde 特定设置,遵循与Convex 和 Clerk集成类似的流程,但重点介绍如何将 Kinde 与 Convex 集成。

它解答了 Kinde 开发者社区提出的许多问题,这些问题可以在这里找到:Kinde 社区 - 将 Convex 与 Kinde 集成

本教程提供了将 Kinde 身份验证与 Convex 集成的清晰、可操作的步骤,同时遵循最佳实践。

Kinde是一个身份验证平台,支持用户通过魔术链接、短信验证码或身份验证器应用程序等方式进行无密码登录。它还支持多因素身份验证 (MFA) 以增强安全性,支持基于 SAML 的企业级单点登录 (SSO),并为企业提供强大的用户管理工具。

示例: 使用 Kinde 的 Convex 身份验证

如果您正在使用 Next.js,请参阅Convex 的 Next.js 设置指南

开始

本指南假设您已使用 Convex 构建了一个可运行的 Next.js 应用。如果没有,请先按照Convex Next.js 快速入门进行操作。然后:

  • 注册 Kinde

在kinde.com/register注册一个免费的 Kinde 帐户

Kinde企业注册页面

  • 在 Kinde 创建企业

输入您的企业或应用程序的名称。

在 Kinde 上创建企业

  • 选择您的技术堆栈

选择用于构建此应用程序的技术堆栈或工具。

在 Kinde 上选择技术堆栈

  • 选择身份验证方法

选择您希望用户登录的方式。

选择身份验证方法

  • 将您的应用连接到 Kinde

将您的 Next.js 应用程序连接到 Kinde。

演示如何将 next.js 应用连接到 Kinde

  • 创建身份验证配置

KINDE_ISSUER_URL从文件中复制您的内容.env.local。进入convex文件夹并创建一个新文件,auth.config.ts其中包含用于验证访问令牌的服务器端配置。

复制您的 Kinde 发卡机构网址

粘贴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;
Enter fullscreen mode Exit fullscreen mode
  • 设置 Convex 和 Kinde Webhook

在 Kinde 仪表板中,转到设置> Webhook > 单击添加 Webhook > 命名 webhook 并粘贴您的 Convex 端点 URL,例如https://<your-convex-app>.convex.site/kinde

选择要触发的事件,例如user.createduser.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;
Enter fullscreen mode Exit fullscreen mode

有关在 Kinde 和 Convex 之间设置 webhook 的详细指南,请参阅此帖子

  • 部署您的更改

运行npx convex dev以自动将您的配置同步到您的后端。

npx convex dev
Enter fullscreen mode Exit fullscreen mode
  • 安装 Kinde

在新的终端窗口中,安装 Kinde Next.js 库

npm install @kinde-oss/kinde-auth-nextjs
Enter fullscreen mode Exit fullscreen mode
  • 复制您的 Kinde 环境变量

在 Kinde 仪表板上,单击应用程序上的查看详细信息。

复制 Kinde 环境变量

向下滚动并复制您的客户端 ID客户端密钥

在 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();
Enter fullscreen mode Exit fullscreen mode

这将处理 Next.js 应用中的 Kinde Auth 端点。

重要! Kinde SDK 依赖于此文件,该文件位于上述指定的位置。

  • 为 Convex 和 Kinde 集成配置新的提供程序

providers在根目录中创建一个文件夹并添加一个新文件ConvexKindeProvider.tsx。此提供程序将把 Convex 与 Kinde 集成并包装您的整个应用程序。

在里面ConvexKindeProvider.tsx,用 包装ConvexProviderKindeProvider并使用useKindeAuth来获取身份验证令牌并将其传递给 Convex。

domainclientIdredirectUri作为 道具 粘贴到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;
Enter fullscreen mode Exit fullscreen mode

将您的配置导入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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 根据身份验证状态显示 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;
Enter fullscreen mode Exit fullscreen mode
  • 在凸函数中使用身份验证状态

如果客户端通过身份验证,则可以通过 访问 Kinde 发送的 JWT 中存储的信息ctx.auth.getUserIdentity

如果客户端未经过身份验证,ctx.auth.getUserIdentity将返回null

确保调用此查询的组件是Authenticatedfrom"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;
  },
});
Enter fullscreen mode Exit fullscreen mode

登录和注销流程

现在您已完成所有设置,您可以使用该LoginLink组件为您的应用创建登录流程。

如果您希望为您的应用配置自定义登录/注册表单,请参阅此帖子

import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";

<LoginLink>Sign in</LoginLink>
Enter fullscreen mode Exit fullscreen mode

要启用注销流程,您可以使用该LogoutLink组件让用户无缝注销您的应用程序。

import {LogoutLink} from "@kinde-oss/kinde-auth-nextjs/components";

<LogoutLink>Log out</LogoutLink>
Enter fullscreen mode Exit fullscreen mode

登录和注销视图

当您需要检查用户是否登录时,请使用useConvexAuth()钩子而不是 Kinde 的钩子。钩子确保浏览器已获取向 Convex 后端发出经过身份验证的请求所需的身份验证令牌,并且 Convex 后端已对其进行了验证:useKindeBrowserClientuseConvexAuth

import { useConvexAuth } from "convex/react";

function App() {
  const { isLoading, isAuthenticated } = useConvexAuth();

  return (
    <div className="App">
      {isAuthenticated ? "Logged in" : "Logged out or still loading"}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

函数中的用户信息

请参阅函数中的身份验证,了解如何在查询、变异和操作中访问有关经过身份验证的用户的信息。

请参阅在 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>;
};
Enter fullscreen mode Exit fullscreen mode

配置开发和生产实例

要在 Convex 开发和生产部署之间配置不同的 Kinde 实例,您可以使用在 Convex 仪表板上配置的环境变量。

配置后端

Kinde 的默认配置适用于生产环境。如需使用自定义域名而非已颁发的<your_app>.kinde.com域名,请参阅本指南

开发配置

在 Convex仪表板上打开您的开发部署的设置并从那里添加所有变量.env.local

带有环境变量的凸面仪表板

生产配置

类似地,在凸仪表板上,在左侧菜单中切换到您的生产部署,并从那里设置变量.env.local

现在通过运行切换到新配置npx convex deploy

npx convex deploy
Enter fullscreen mode Exit fullscreen mode

部署你的 Next.js 应用

根据您的托管平台,在生产环境中设置环境变量。请参阅托管

调试身份验证

如果用户成功完成 Kinde 注册或登录流程,并在保存到您的 Convex 数据库并重定向回您的页面后useConvexAuth显示isAuthenticated: false,则可能是您的后端配置不正确。

auth.config.ts目录中的文件包含convex/已配置的身份验证提供程序列表。添加新的提供程序后,必须运行npx convex devnpx convex deploy才能将配置同步到后端。

有关更详细的调试步骤,请参阅调试身份验证

引擎盖下

身份验证流程如下:

  1. 用户点击注册或登录按钮。
  2. 用户被重定向到托管的 Kinde 页面,他们可以通过您在 Kinde 中配置的任何方法注册或登录。
  3. 成功注册或登录后,他们的详细信息将通过 webhook 发送并安全地存储在 Convex 中,之后他们会立即重定向回您的页面,或者您通过 Kinde postLoginRedirectURLprop 配置的其他页面。
  4. 现在KindeProvider知道用户已经通过身份验证。
  5. 从 Kinde 获取身份验证令牌useKindeAuthAuthTokenFetcher
  6. 然后,react useEffecthook 将此令牌设置为setAuthConvex 的一个实例。
  7. 然后ConvexProvider将此令牌传递到您的 Convex 后端进行验证。
  8. 您的 Convex 后端从 Kinde 检索域、clientId 和 redirectUri 以检查令牌的签名是否有效。
  9. 收到身份验证成功通知ConvexProvider,现在整个应用程序都知道用户已通过 Convex 进行身份验证。useConvexAuth返回isAuthenticated: true并且Authenticated组件呈现其子项。

文件中的配置ConvexKindeProvider.tsx负责在需要时重新获取令牌,以确保用户与后端保持身份验证。

鏂囩珷鏉ユ簮锛�https://dev.to/sholajgede/convex-kinde-2pe1
PREV
NextRaise:利用人工智能代理 GenAI LIVE 简化初创企业的融资流程!| 2025 年 6 月 4 日
NEXT
构建由人工智能 CompetiAI 驱动的竞争情报工具