发布于 2025-12-10 2 阅读
0

Convex 和 Kinde 开始使用

凸和金德

开始

本指南概述了针对 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.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;
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,用 包装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;
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 dev或npx convex deploy才能将配置同步到后端。

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

引擎盖下

身份验证流程如下:

  1. 用户点击注册或登录按钮。
  2. 用户被重定向到托管的 Kinde 页面,他们可以通过您在 Kinde 中配置的任何方法注册或登录。
  3. 成功注册或登录后,他们的详细信息将通过 webhook 发送并安全地存储在 Convex 中,之后他们会立即重定向回您的页面,或者您通过 Kinde postLoginRedirectURLprop 配置的其他页面。
  4. 现在KindeProvider知道用户已经通过身份验证。
  5. 并从 Kinde 获取身份验证令牌useKindeAuth。AuthTokenFetcher
  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