Skip to content
Self-Hosting

配置认证钩子

Set up auth hooks for self-hosted Supabase with Docker.

本指南涵盖了在使用 Docker Compose 运行的自托管 Supabase 实例上启用身份验证钩子所需的服务器端配置。身份验证钩子允许你在认证流程的特定环节运行自定义逻辑——例如,向 JWT 添加声明,通过自定义提供商发送短信,或限制注册。

🌐 This guide covers the server-side configuration required to enable auth hooks on a self-hosted Supabase instance running with Docker Compose. Auth hooks let you run custom logic at specific points in the authentication flow - for example, adding claims to JWTs, sending SMS through a custom provider, or restricting signups.

在你开始之前 #

🌐 Before you begin

你需要:

🌐 You need:

  • 一个可用的自托管 Supabase 安装。请参阅 使用 Docker 自托管。
  • 关于 Postgres 函数钩子:可以访问数据库服务来创建函数。
  • 对于 HTTP 端点钩子:一个可访问的 HTTPS 端点或本地 Edge 函数。

hooks 是怎么运作的 #

🌐 How hooks work

有关钩子实现的详细信息(输入/输出模式、SQL 和 HTTP 示例),请参阅 Auth Hooks。

🌐 For hook implementation details (input/output schemas, SQL and HTTP examples), see Auth Hooks.

Supabase Auth 可以在身份验证流程的特定生命周期事件中调用 hook。每个 hook 都可以通过以下环境变量进行配置:

🌐 Supabase Auth can call a hook at specific lifecycle events during the auth flow. Each hook can be configured with the following environment variables:

  • GOTRUE_HOOK_{HOOK_NAME}_ENABLED:启用钩子(true/false)
  • GOTRUE_HOOK_{HOOK_NAME}_URI:钩子端点
  • GOTRUE_HOOK_{HOOK_NAME}_SECRETS:Webhook 签名密钥(用于 HTTP 钩子)
钩子钩子名称描述
自定义访问令牌CUSTOM_ACCESS_TOKEN在 JWT 签发前添加声明
发送短信SEND_SMS使用自定义提供商替换内置短信发送
发送邮件SEND_EMAIL使用自定义提供商替换内置邮件发送
用户创建前BEFORE_USER_CREATED在创建用户前进行检查或阻止注册
多因素验证MFA_VERIFICATION_ATTEMPT验证 MFA 尝试(限流、防暴力攻击)
密码验证PASSWORD_VERIFICATION_ATTEMPT跟踪并限制密码尝试失败次数

URI 方案 #

🌐 URI schemes

Hooks 支持两种 URI 方案:

🌐 Hooks support two URI schemes:

方案格式
pg-functions://pg-functions://postgres/<schema>/<function_name>
http:// 或 https://https://example.com/hook

一步步:Postgres 函数钩子 #

🌐 Step-by-step: Postgres function hook

这个例子演示了如何使用 Postgres 函数启用 自定义访问令牌 钩子,该函数会向 JWT 添加一个 user_role 声明。

🌐 This example enables the Custom Access Token hook using a Postgres function that adds a user_role claim to the JWT.

步骤 1:创建 Postgres 函数 #

🌐 Step 1: Create the Postgres function

你可以在 Supabase 仪表板的 SQL 编辑器中执行以下 SQL,或者通过像 psql 这样的 Postgres 客户端连接到你的数据库来执行。

🌐 You can execute the following SQL from the Supabase Dashboard SQL Editor, or by connecting to your database using a Postgres client such as psql.

这个例子是从 user_roles 表中读取角色,所以要先创建那个表。如果表不存在,钩子就会报错,每次登录都会失败。

🌐 This example reads roles from a user_roles table, so create that table first. If the table is missing, the hook errors and every sign-in fails.

user_roles.sql
create table if not exists public.user_roles (
user_id uuid not null references auth.users
on delete cascade,
role text not null,
primary key (user_id)
);

然后创建这个钩子函数:

🌐 Then create the hook function:

custom_access_token_hook.sql
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
claims jsonb;
user_role text;
begin
claims := event->'claims';
-- Example: look up a custom role from a user_roles table
select role into user_role
from public.user_roles
where user_id = (event->>'user_id')::uuid;
if user_role is not null then
claims := jsonb_set(
claims, '{user_role}', to_jsonb(user_role)
);
end if;
-- Return the modified claims
return jsonb_build_object('claims', claims);
end;
$$;
-- Grant execute permission to supabase_auth_admin
grant execute on function public.custom_access_token_hook
to supabase_auth_admin;
-- Grant schema access to supabase_auth_admin (usually already granted by default)
grant usage on schema public to supabase_auth_admin;
-- Revoke from public and other roles
revoke execute on function public.custom_access_token_hook
from authenticated, anon, public;

步骤 2:更新 docker-compose.yml#

🌐 Step 2: Update docker-compose.yml

更新 auth 服务的 environment: 块:

🌐 Update the auth service environment: block:

docker-compose.yml
services:
auth:
environment:
# ... existing variables ...
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: 'true' # 👈 enabling the hook is required
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: 'pg-functions://postgres/public/custom_access_token_hook'

步骤 3:重新启动认证服务 #

🌐 Step 3: Relaunch the auth service

sh run.sh recreate auth

第4步:验证自定义声明 #

🌐 Step 4: Verify the custom claim

给用户分配一个角色,这样钩子就有东西可以添加。用 auth.users 中的真实用户 ID 替换 UUID:

🌐 Give a user a role so the hook has something to add. Replace the UUID with a real user ID from auth.users:

insert into public.user_roles (user_id, role)
values ('00000000-0000-0000-0000-000000000000', 'admin');

以该用户身份登录并解码 JWT。如果钩子运行成功,user_role 声明就会出现。没有在 user_roles 中找到匹配行的用户仍然可以登录,但不会有该声明。如果有问题,查看认证日志:

🌐 Sign in as that user and decode the JWT. If the hook ran successfully, the user_role claim is present. A user with no matching row in user_roles still signs in, but without the claim. If something doesn't work, check the auth logs:

docker compose logs auth --tail 20

一步步:HTTP 端点钩子 #

🌐 Step-by-step: HTTP endpoint hook

这个例子使用 Edge Function 启用了 发送短信 钩子。

🌐 This example enables the Send SMS hook using an Edge Function.

第1步:创建边缘函数 #

🌐 Step 1: Create the Edge Function

创建 volumes/functions/send_sms/index.ts:

🌐 Create volumes/functions/send_sms/index.ts:

volumes/functions/send_sms/index.ts
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
// Note: this example assumes a single secret. If you use multiple secrets (e.g. "v1,whsec_new|v1,whsec_old"), split on '|'
// and try each secret in turn until wh.verify() succeeds.
const hookSecret = Deno.env.get('SEND_SMS_HOOK_SECRET')?.replace('v1,whsec_', '')
Deno.serve(async (req) => {
if (req.method !== 'POST') {
return new Response('not allowed', { status: 400 })
}
if (!hookSecret) {
console.error('SEND_SMS_HOOK_SECRET environment variable not provided')
return new Response('{}', { status: 500 })
}
// Verify the webhook signature
const payload = await req.text()
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(hookSecret)
const { user, sms } = wh.verify(payload, headers)
// Send SMS using your provider
// ... your sms sending logic here ...
return new Response(JSON.stringify({}), {
headers: { 'Content-Type': 'application/json' },
})
})

第2步:生成一个Webhook密钥 #

🌐 Step 2: Generate a webhook secret

使用以下命令生成一个秘密:

🌐 Generate a secret using the following command:

echo "v1,whsec_$(openssl rand -base64 32)"

复制这个命令的输出,例如:v1,whsec_abc123...。

🌐 Copy the output of this command, for example: v1,whsec_abc123....

步骤3:更新 .env#

🌐 Step 3: Update .env file

将以下环境变量添加到你的 .env 文件中:

🌐 Add the following environment variables to your .env file:

.env
SEND_SMS_HOOK_URI=http://host.docker.internal:8000/functions/v1/send_sms
SEND_SMS_HOOK_SECRET=YOUR_GENERATED_SECRET_HERE # Paste the secret generated in the last step here

步骤4:更新 docker-compose.yml#

🌐 Step 4: Update docker-compose.yml

将以下环境变量添加到 auth 和 functions 服务中。为了简洁,本示例只显示更新的字段。

🌐 Add the following environment variables to the auth and functions services. For brevity, only the updated fields are shown.

docker-compose.yml
services:
auth:
environment:
# ... existing variables ...
GOTRUE_HOOK_SEND_SMS_ENABLED: 'true'
GOTRUE_HOOK_SEND_SMS_URI: ${SEND_SMS_HOOK_URI}
GOTRUE_HOOK_SEND_SMS_SECRETS: ${SEND_SMS_HOOK_SECRET}
extra_hosts: # 👈 required so the container can resolve host.docker.internal
- 'host.docker.internal:host-gateway'
functions:
environment:
# ... existing variables ...
SEND_SMS_HOOK_SECRET: ${SEND_SMS_HOOK_SECRET}

步骤5:重新启动认证和函数服务 #

🌐 Step 5: Relaunch auth and functions services

sh run.sh recreate auth functions

第6步:确认钩子是否触发 #

🌐 Step 6: Verify the hook fires

触发一次短信验证事件,并确认钩子成功执行。

🌐 Trigger an SMS authentication event and confirm that the hook executes successfully.

如果有什么不工作,检查一下认证和功能日志:

🌐 If something doesn't work, check the auth and functions logs:

docker compose logs auth --tail 20
docker compose logs functions --tail 20

Webhook 密钥 #

🌐 Webhook secrets

HTTP 钩子使用 标准 Webhooks 规范来进行负载签名。

🌐 HTTP hooks use the Standard Webhooks specification for payload signing.

生成一个秘密 #

🌐 Generating a secret

echo "v1,whsec_$(openssl rand -base64 32)"

秘密格式 #

🌐 Secret format

  • 对称:v1,whsec_[base64]{32-88 characters}

密钥轮换 #

🌐 Key rotation

使用 | 分隔多个密钥,以便在不中断服务的情况下轮换密钥。例如:

🌐 Separate multiple secrets with | to rotate keys without downtime. For example:

.env
SEND_EMAIL_HOOK_SECRET=v1,whsec_new-secret|v1,whsec_old-secret
docker-compose.yml
services:
auth:
environment:
# ... existing variables ...
GOTRUE_HOOK_SEND_EMAIL_ENABLED: 'true'
GOTRUE_HOOK_SEND_EMAIL_URI: 'https://example.com'
GOTRUE_HOOK_SEND_EMAIL_SECRETS: ${SEND_EMAIL_HOOK_SECRET}

Auth 服务会用所有配置的密钥对每个请求进行签名,这样接收方可以用任意一个进行验证。一旦所有客户端都接受了新密钥,就可以删除旧的了。

🌐 The Auth service signs each request with all configured secrets, so receivers can verify against either. Once all clients accept the new secret, remove the old one.

故障排除 #

🌐 Troubleshooting

钩子没有触发 #

🌐 Hook not firing

  • 检查 docker-compose.yml 中的 GOTRUE_HOOK_{HOOK_NAME}_ENABLED 是否设置为 "true"(作为字符串)
  • 确认变量到达容器:sh run.sh printenv auth | grep GOTRUE_HOOK
  • 记住:.env 变量不会到达容器,除非通过 docker-compose.yml 传递

pg-functions://#

🌐 pg-functions:// URI errors

URI 格式必须正好是 pg-functions://postgres/<schema>/<function_name>:

  • 按惯例使用 postgres 作为主机。主机部分不进行验证。
  • 模式和函数名必须是有效的 Postgres 标识符
  • 该功能必须存在并授予给 supabase_auth_admin

HTTP 钩子返回错误 #

🌐 HTTP hook returns errors

查看认证日志以获取详细信息:

🌐 Check auth logs for details:

docker compose logs auth --tail 20

常见原因:

🌐 Common causes:

  • 从认证容器无法访问该端点
  • http:// 只允许 localhost、127.0.0.1、::1 和 host.docker.internal 使用

Webhook 密钥格式不匹配 #

🌐 Webhook secret format mismatch

密钥必须符合标准 Webhooks 格式:

🌐 Secrets must match the Standard Webhooks format:

  • 对称:v1,whsec_[base64](前缀后 32-88 个 base64 字符)
  • 秘密字符串中不要有空格或换行
  • 使用 echo "v1,whsec_$(openssl rand -base64 32)" 生成

Postgres 函数权限被拒 #

🌐 Permission denied on Postgres function

supabase_auth_admin 角色需要在这个函数上拥有 execute。默认情况下,每个角色都会从 public 角色继承 execute,但在第 1 步的设置里,这个权限从 public 被撤销了,所以你必须显式地把它再授予 supabase_auth_admin。否则,登录会失败并显示 500: Error running hook URI,认证日志中会出现权限拒绝错误:

🌐 The supabase_auth_admin role needs execute on the function. By default every role inherits execute from the public role, but the setup in Step 1 revokes it from public, so you must grant it back to supabase_auth_admin explicitly. Otherwise sign-in fails with 500: Error running hook URI, and the auth logs show a permission-denied error:

grant execute on function public.your_hook_function
to supabase_auth_admin;

该角色还需要在 schema 上有 usage。在默认安装中,它已经通过 public schema 上的内置授权拥有此权限,但如果你的数据库已经撤销了对 public 的使用权限,请显式授予它:

🌐 The role also needs usage on the schema. On a default install it already has this through the built-in grant on the public schema, but grant it explicitly if your database has revoked usage from public:

grant usage on schema public to supabase_auth_admin;

短信验证码有效期太短 #

🌐 SMS OTP expiry is too short

参考 OTP 设置文档

🌐 Refer to OTP Settings Docs