配置认证钩子
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 钩子)
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 函数钩子在你的数据库内部运行,所以没有网络开销,也不需要管理密钥。
🌐 Postgres function hooks run inside your database, so there is no network overhead and no need to manage secrets.
http:// URI 只允许用于 localhost、127.0.0.1、::1 和 host.docker.internal 主机名。
一步步: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.
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:
create or replace function public.custom_access_token_hook(event jsonb)returns jsonblanguage plpgsqlsecurity definerset 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_admingrant 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 rolesrevoke 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:
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.
发送短信的钩子只有在身份验证发送一次性密码(OTP)时才会触发。确保手机验证已启用(GOTRUE_EXTERNAL_PHONE_ENABLED=true),并且自动手机确认已关闭(GOTRUE_SMS_AUTOCONFIRM=false)。当GOTRUE_SMS_AUTOCONFIRM开启时,注册无需OTP就会被确认,所以钩子根本不会运行。
🌐 The Send SMS hook only fires when Auth sends an OTP. Make sure phone auth is enabled (GOTRUE_EXTERNAL_PHONE_ENABLED=true) and automatic phone confirmation is off (GOTRUE_SMS_AUTOCONFIRM=false). When GOTRUE_SMS_AUTOCONFIRM is on, signups are confirmed without an OTP, so the hook never runs.
第1步:创建边缘函数 #
🌐 Step 1: Create the Edge Function
创建 volumes/functions/send_sms/index.ts:
🌐 Create 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:
SEND_SMS_HOOK_URI=http://host.docker.internal:8000/functions/v1/send_smsSEND_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.
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 20docker compose logs functions --tail 20Webhook 密钥 #
🌐 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:
SEND_EMAIL_HOOK_SECRET=v1,whsec_new-secret|v1,whsec_old-secretservices: 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.
Postgres 函数钩子(pg-functions:// URI)不需要秘密信息——它们直接在数据库中运行。
🌐 Postgres function hooks (pg-functions:// URIs) do not require secrets - they run directly inside the database.
故障排除 #
🌐 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