跳转至

Authany 与 Supabase

用户身份验证是任何现代 Web 应用的基础组成部分,但要把它做对却出了名地困难。这正是 Authany 的用武之地。Authany 开箱即用地处理用户注册、登录流程、会话管理,以及生物识别登录、双重验证、社交登录等高级安全功能。

将 Authany 与 Supabase 的 Postgres 数据库以及强大的行级安全(Row-Level Security,RLS)搭配使用,可以为您构建面向单个用户的应用打下安全、可扩展的基础。

在本指南中,您将学习如何使用 React.js,把 Authany 和 Supabase 集成起来,构建一个基础的 CRUD 应用。

Simple instrument tracking app where users can manage their personal collection after logging in with Authany

一个简单的乐器追踪应用,用户使用 Authany 登录后即可管理自己的收藏

我们将介绍如何使用 Authany 对用户进行身份验证、通过 Supabase Edge Function 交换令牌、基于 Authany 签发的 JWT 应用 RLS 策略,并将这一切整合进使用 Vite 搭建的现代 React 前端中。

┌─────────────┐          ┌──────────────┐          ┌──────────────┐
│   Browser   │          │   Authgear   │          │   Supabase   │
│  (React App)│◄────────►│  (Auth IdP)  │          │  (Database)  │
└──────┬──────┘          └──────────────┘          └──────┬───────┘
       │                                                  │
       │ 1. Login with Authgear                           │
       │ 2. Get Authgear JWT (access token)               │
       │ 3. Call Supabase with Authgear JWT (request data)│
       └──────────────────────────────────────────────────►
       │                                                   │
       │ 4. Exchange JWT (Edge Function)                   │
       │ 5. Return Supabase JWT                            │
       │◄──────────────────────────────────────────────────
       │                                                   │
       │ 6. Access data with Supabase JWT (enforce RLS)    │
       └──────────────────────────────────────────────────►

完整示例代码

完整的示例应用源代码已发布在 GitHub 上,您可以将其作为自己项目的起点:https://github.com/authgear/authgear-example-supabase

准备工作

第 1 步:搭建 Authany 项目

首先,配置 Authany 来为您的应用处理身份验证。

  1. 创建一个 Authany 应用: 登录 Authany 管理台,创建一个新应用,应用类型选择单页应用。
  2. 配置 OAuth 重定向: 在 Authany 应用设置中,设置重定向 URI:
  3. 重定向 URI:http://localhost:5173/auth-redirect —— 登录成功后 Authany 会将浏览器重定向到这里(我们的 React 应用会处理这个路由)。
  4. 登出后重定向 URI: http://localhost:5173/ —— 用户登出后跳转到的地址。
  5. 记下您的 Authany 凭据: 创建完成后,记下以下信息:
  6. 您应用的 Authany 端点(形如 https://<your-project>.authgear.cloud)。
  7. 您 Authany 应用的客户端 ID。

在 React 应用和 Supabase 函数中初始化 Authany 时会用到这些值。

第 2 步:搭建 Supabase 项目

接下来,准备您的 Supabase 后端。

  1. 创建一个 Supabase 项目: 登录 Supabase 控制台,创建一个新项目(选择组织、项目名称、数据库密码和地区)。
  2. 获取项目密钥: 在您的 Supabase 项目中,进入Project Settings → API → API keys。复制您项目的 Publishable Key 和 URL(例如 https://xyzcompany.supabase.co)。
  3. 获取 JWT Secret: 仍在 Project settings 中,进入 JWT Keys,找到您项目的 Legacy JWT Secret。这是 Supabase 用来为 RLS 签名和验证 JWT 的密钥。复制这个 JWT secret 值——我们会在 Edge Function 的配置中用到它。

    从 JWT Keys 页面复制 Legacy JWT Secret

  4. (可选) 安装 Supabase CLI: 如果您打算用 CLI 部署 Edge Function,请按照 Supabase 的说明进行安装。您也可以通过控制台界面部署该函数。

第 3 步:创建一个用于交换 JWT 的 Edge Function

现在我们来搭建关键的一环:一个 Supabase Edge Function,用于接收 Authany 的 JWT,并返回一个用 Supabase 密钥签名的新 JWT。

  1. 进入 "Edge Functions" -> "Secrets",添加以下两个密钥:
  2. AUTHGEAR_ENDPOINT = 您的 Authany 应用端点(例如 https://myapp.authanyid.com)。
  3. SB_JWT_SECRET = 您的 Supabase JWT secret(来自上面的第 2 步)。
  4. 部署该函数
  5. 在 Supabase 网页控制台中进入 "Edge Functions" -> "Functions"。复制粘贴以下代码并在此部署,将函数命名为 exchange-jwt。
  6. 您也可以使用 Supabase CLI,从本地机器构建并部署该 Edge Function 到您的 Supabase 项目。
// supabase/functions/exchange-jwt/index.ts

import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import jwt from "npm:jsonwebtoken";
import jwks from "npm:jwks-rsa";
import * as jose from "https://deno.land/x/jose@v4.14.4/index.ts";

// CORS headers for cross-origin requests
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "Authorization, Content-Type",
  "Access-Control-Allow-Methods": "GET",
  "Access-Control-Max-Age": "3600"
};

// Read environment variables
const authgearEndpoint = Deno.env.get("AUTHGEAR_ENDPOINT");
const supabaseJwtSecret = Deno.env.get("SB_JWT_SECRET");
if (!authgearEndpoint || !supabaseJwtSecret) throw new Error("Missing env vars");

// Get JWKS URI from Authgear's OpenID configuration
async function getJwksUri(): Promise<string> {
  const configUrl = `${authgearEndpoint}/.well-known/openid-configuration`;
  const response = await fetch(configUrl);
  const config = await response.json();
  return config.jwks_uri;
}

// Extract Bearer token from Authorization header
function extractToken(req: Request): string | null {
  const authHeader = req.headers.get("Authorization");
  if (!authHeader) return null;

  const parts = authHeader.split(" ");
  if (parts.length !== 2 || parts[0] !== "Bearer") return null;

  return parts[1];
}

// Verify JWT token with Authgear's public key
async function verifyToken(token: string): Promise<any> {
  // Decode token to get key ID
  const decoded = jwt.decode(token, { complete: true }) as any;
  if (!decoded?.header?.kid) {
    throw new Error("Invalid token: missing key ID");
  }

  // Get JWKS URI and create JWKS client
  const jwksUri = await getJwksUri();
  const jwksClient = jwks({ jwksUri });

  // Get the signing key
  const key = await jwksClient.getSigningKey(decoded.header.kid);
  const signingKey = key.getPublicKey();

  // Verify the token
  const options = {
    algorithms: ["RS256"],
    issuer: authgearEndpoint,
  };

  return jwt.verify(token, signingKey, options);
}

// Sign a new JWT with Supabase secret
async function signSupabaseJwt(payload: any): Promise<string> {
  payload.role = "authenticated"; // Required by Supabase

  // Add or modify any other claims you need for RLS policies
  // payload.some_claim = "some claim";

  // Sign with Supabase JWT secret
  const supabaseSecret = new TextEncoder().encode(supabaseJwtSecret);

  const supabaseJwt = await new jose.SignJWT(payload)
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer("supabase")
    .setIssuedAt(payload.iat)
    .setExpirationTime(payload.exp || "")
    .sign(supabaseSecret);

  return supabaseJwt;
}

// Main function handler
Deno.serve(async (req: Request) => {
  // Handle CORS preflight requests
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  try {
    // Extract token from request
    const token = extractToken(req);
    if (!token) {
      return new Response(
        JSON.stringify({ error: "Missing or invalid Authorization header" }),
        { 
          headers: { ...corsHeaders, "Content-Type": "application/json" },
          status: 401 
        }
      );
    }
    // Verify the token
    const verified = await verifyToken(token);
    // Sign a new JWT with Supabase secret
    const supabaseJwt = await signSupabaseJwt(verified);
    return new Response(
      JSON.stringify({ supabaseJwt }),
      {
        headers: { ...corsHeaders, "Content-Type": "application/json" },
        status: 200,
      }
    );
  } catch (error) {
    console.error("Token verification failed:", error.message);
    return new Response(
      JSON.stringify({ error: "Token verification failed" }),
      {
        headers: { ...corsHeaders, "Content-Type": "application/json" },
        status: 401,
      }
    );
  }
});

部署完成后,您的函数可以通过以下地址访问:

${SUPABASE_URL}/functions/v1/exchange-jwt

第 4 步:创建数据库表和 RLS 策略

接下来,我们在 Supabase 中搭建数据库表和 RLS 规则。我们将创建一个表(instruments)来存储用户拥有的物品,并使用 RLS 确保每个用户只能操作自己的行。

在您的 Supabase 项目中打开 SQL 编辑器,运行以下 SQL 命令:

-- 1. Helper function to get the current Authgear user ID from the JWT
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS TEXT AS $$
  SELECT auth.jwt() ->> 'sub';
$$ LANGUAGE SQL STABLE;

我们创建了一个名为 current_user_id() 的 SQL 函数,用于从当前请求的 JWT 中返回 sub 声明(subject)。Supabase 的 Postgres 提供了一个 auth.jwt() 函数,用于暴露请求方 JWT 中的声明;auth.jwt() ->> 'sub' 会以文本形式提取 sub 字段。

-- 2. Instruments table: each row has a user_id to identify the owner
CREATE TABLE instruments (
  id BIGSERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
  name TEXT NOT NULL,
  user_id TEXT NOT NULL   -- store Authgear user’s ID (sub)
);

-- 3. Enable Row Level Security on the table
ALTER TABLE instruments ENABLE ROW LEVEL SECURITY;
  • instruments 表有一个 user_id 列,用于为每件乐器存储对应 Authany 用户的 ID。
  • 我们在该表上启用了 RLS,这意味着默认情况下,除非有策略允许,否则任何行都无法被访问。
-- 4. RLS Policies:
-- Allow each authenticated user to SELECT rows where user_id = their own ID
CREATE POLICY "Users can read their own instruments"
ON instruments
FOR SELECT
TO authenticated
USING (user_id = current_user_id());

-- Allow INSERTs only if setting user_id to their own ID
CREATE POLICY "Users can insert their own instruments"
ON instruments
FOR INSERT
TO authenticated
WITH CHECK (user_id = current_user_id());

-- Allow UPDATE on rows they own
CREATE POLICY "Users can update their own instruments"
ON instruments
FOR UPDATE
TO authenticated
USING (user_id = current_user_id())
WITH CHECK (user_id = current_user_id());

-- Allow DELETE on rows they own
CREATE POLICY "Users can delete their own instruments"
ON instruments
FOR DELETE
TO authenticated
USING (user_id = current_user_id());

然后我们定义了四条策略,分别对应四种 CRUD 操作(SELECT、INSERT、UPDATE、DELETE)。每条策略都仅限于 authenticated 角色,并要求该行的 user_id 与来自 JWT 的用户 ID 相匹配。

第 5 步:使用 Supabase 客户端构建 React 应用

接下来是前端部分。我们将使用 Vite 创建一个 React 应用,集成 Authany 的 SDK 来做身份验证,并配置 Supabase 客户端,让它自动使用我们的令牌交换流程。

依赖项

  1. 使用 npm create vite@latest my-app -- --template react 搭建一个 React 应用的脚手架
  2. 按照这里的指南搭建 SPA:React,搭建一个带有 Authany 身份验证的基础 React 应用
  3. 安装 Supabase 库

    npm install @supabase/supabase-js
    
    4. 在 .env 中配置环境变量,让 Supabase 的 URL 和 Publishable key 可用。您的 .env.local 应该形如:

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-supabase-anon-key

VITE_AUTHGEAR_ENDPOINT=https://your-app.authanyid.com
VITE_AUTHGEAR_CLIENT_ID=your-authgear-client-id
VITE_AUTHGEAR_REDIRECT_URL=http://localhost:5173/auth-redirect
VITE_AUTHGEAR_LOGOUT_REDIRECT_URL=http://localhost:5173/

配置 Supabase 客户端

现在我们可以设置 Supabase JS 客户端,让它使用我们的 Authany 令牌。新建一个文件 my-app/src/lib/supabase.js,添加以下内容:

// my-app/src/lib/supabase.js
import { createClient } from '@supabase/supabase-js';
import authgear from '@authgear/web';

const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY;

// Optional: caches to avoid redundant token exchanges
let cachedSupabaseJwt = null;
let cachedAuthgearToken = null;

// Function to exchange Authgear token for Supabase token
async function exchangeToken(authgearToken) {
  // Return cached token if the Authgear token hasn’t changed
  if (cachedAuthgearToken === authgearToken && cachedSupabaseJwt) {
    return cachedSupabaseJwt;
  }
  // Call the Supabase Edge Function
  const res = await fetch(`${SUPABASE_URL}/functions/v1/exchange-jwt`, {
    headers: { Authorization: `Bearer ${authgearToken}` }
  });
  if (!res.ok) {
    throw new Error(`Token exchange failed with status ${res.status}`);
  }
  const { supabaseJwt } = await res.json();
  // Update cache and return the new JWT
  cachedAuthgearToken = authgearToken;
  cachedSupabaseJwt = supabaseJwt;
  return supabaseJwt;
}

// Create Supabase client with custom auth settings
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    autoRefreshToken: false,   // Don't use Supabase’s token refresh
    persistSession: false,     // Don't store Supabase session (Authgear will handle session)
  },
  accessToken: async () => {
    // This function runs before every Supabase request that needs auth
    await authgear.refreshAccessTokenIfNeeded();
    const authgearToken = authgear.accessToken;
    if (!authgearToken) return null;
    // Exchange Authgear token for a Supabase token:contentReference[oaicite:12]{index=12}:contentReference[oaicite:13]{index=13}
    return exchangeToken(authgearToken);
  },
});

在这个配置中:

  • 我们禁用了 Supabase 客户端中的 autoRefreshToken 和 persistSession,这样 Authany 就是会话状态的唯一权威来源。
  • 我们提供了一个异步的 accessToken 函数。每当 Supabase 客户端需要用到 JWT 进行已验证身份的请求时,都会调用这个函数。在我们的例子中,就是每次对 instruments 表的查询。
  • 在 accessToken() 中,我们首先通过调用 authgear.refreshAccessTokenIfNeeded() 来确保 Authany 的令牌是最新的。
  • 然后我们获取 authgear.accessToken——这是来自 Authany SDK 的当前 JWT。
  • 如果令牌存在,我们会调用 exchangeToken() 辅助函数,该函数会把 Authany 令牌传给 Edge Function 的端点,并接收返回的 Supabase JWT。

构建 CRUD 函数

登录后,使用 Supabase 客户端来获取和操作数据。例如,要加载当前用户的乐器列表,可以这样调用:

const { data, error } = await supabase.from('instruments').select('*');

要添加一件新乐器,可以调用

supabase.from('instruments').insert({
    name: "<new intruments>",
    user_id: userInfo.sub,
});

类似地,要更新或删除,也可以在 Supabase 客户端中调用带有相应过滤条件的 .update(...) 或 .delete(...)

6. 运行并测试应用

现在可以测试整个系统了。启动开发服务器:

npm run dev

在浏览器中打开 http://localhost:5173,测试集成效果。

到此就完成了

您已经成功将 Authany 用于身份验证、Supabase 用于安全的按用户数据访问(借助行级安全)集成到了一起。

有了这样的配置,您就可以充分利用 Authany 强大的身份验证功能,例如社交登录、双重验证和安全的会话管理,同时借助 Supabase 可扩展的数据库和内置 RLS,让用户数据保持隔离和受保护。这种令牌交换方式让 Authany(或任何其他基于 JWT 的身份提供方)与 Supabase 搭配使用变得非常简单。