Nhost nhost-js Auth 模块详解:Nhost 认证服务的 TypeScript 客户端实战指南
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
本篇基于 Nhost 仓库中的官方参考文档 auth.md,系统讲解@nhost/nhost-js的 Auth 模块:如何导入并创建认证客户端、55 个 API 方法的完整能力图谱(邮箱/密码、匿名、OTP、Passkey、OAuth2/OIDC、PAT 等)、FetchError错误处理机制,以及底层的 PKCE 工具与请求中间件链实现。读完本文,你可以直接在项目里落地 Nhost 的注册、登录、会话刷新、二次认证与令牌交换等完整认证流程。
模块定位与两种导入方式
Auth 模块是与 Nhost Auth 服务交互的核心模块,封装了用户注册/登录、会话管理、多因素认证(MFA)、WebAuthn 安全密钥、个人访问令牌(PAT)以及完整的 OAuth2/OpenID Connect 标准端点。源码位于 client.ts(约 5500 行的类型化 API 客户端),模块入口 index.ts 通过export * from './client'和export * from './pkce'同时导出 API 客户端与 PKCE 工具函数。
有两种使用方式:
- 通过主 Nhost 客户端间接使用(推荐):
nhost.auth属性会携带会话刷新、令牌挂载等中间件,适合绝大多数场景; - 直接导入 auth 子模块:适合只需要调用认证 API、不依赖完整 SDK 组合的特定用例。
包 package.json 中通过exports字段声明了独立的子路径入口(./auth、./fetch、./session等),支持 ESM(import)与 CJS(require)双格式,运行时要求 Node.js >= 22:
// 方式一:通过子模块直接导入 import { createAPIClient } from "@nhost/nhost-js/auth"; // 方式二:通过主客户端使用(文档示例写法) import { createClient } from "@nhost/nhost-js";创建客户端与首次注册调用
典型用法是通过createClient传入项目的subdomain与region,SDK 会据此构造 auth 服务的 baseURL(源码中由generateServiceUrl('auth', subdomain, region, authUrl)完成,见 nhost.ts),然后调用nhost.auth.signUpEmailPassword完成邮箱密码注册:
import { createClient } from "@nhost/nhost-js"; const nhost = createClient({ subdomain, region, }); await nhost.auth.signUpEmailPassword({ email, password, });若需要绕过主客户端,auth 子模块还导出低层工厂函数createAPIClient,可指定任意 baseURL 并注入自定义中间件链:
function createAPIClient( baseURL: string, chainFunctions?: ChainFunction[], ): Client;chainFunctions默认为空数组;每个ChainFunction的签名为(next: FetchFunction) => FetchFunction,用于拦截和改写请求/响应(日志、重试、自定义请求头等)。
错误处理:FetchError 与自动消息提取
这是文档中非常实用的一节,必须掌握。SDK 在大多数操作中,只要请求返回状态码 >= 300 或请求彻底失败(如网络错误),都会抛出FetchError<ErrorResponse>类型的错误:
import { createClient } from "@nhost/nhost-js"; import { FetchError } from "@nhost/nhost-js/fetch"; const nhost = createClient({ subdomain, region, }); try { await nhost.auth.signInEmailPassword({ email, password, }); } catch (err) { if (!(err instanceof FetchError)) { throw err; // Re-throw if it's not a FetchError } console.log("Error:", err); // Error: { // body: { // error: 'invalid-email-password', // message: 'Incorrect email or password', // status: 401 // }, // status: 401, // headers: { // 'content-length': '88', // 'content-type': 'application/json', // date: 'Mon, 12 May 2025 08:08:28 GMT' // } // } // error handling... }FetchError的三个关键成员在 fetch.ts 中定义:
| 成员 | 类型 | 说明 |
|---|---|---|
body | T(此处为ErrorResponse) | 原始响应体 |
status | number | HTTP 状态码 |
headers | Headers | 响应头 |
由于FetchError extends Error,err.message已被自动提取为人类可读文案。提取逻辑见 extractMessage:它依次尝试message字段、字符串型error字段、嵌套的error.message对象,以及 GraphQL 风格的errors[].message数组(多条以逗号拼接),全部不匹配时回退为"An unexpected error occurred"。因此只关心文案时,按标准Error处理即可:
try { await nhost.auth.signInEmailPassword({ email, password, }); } catch (err) { if (!(err instanceof Error)) { throw err; // Re-throw if it's not an Error } console.log("Error:", err.message); // Error: Incorrect email or password }对应的ErrorResponse结构包含error(错误码字符串)、message(描述)与status(状态码)三个字段,例如上例中的invalid-email-password/ 401。
Client 方法全景:按能力域梳理
文档中Client接口共列出 55+ 个方法,全部返回Promise<FetchResponse<T>>(少数重定向类方法返回 URL 字符串)。FetchResponse<T>统一携带body、status、headers三个字段(fetch.ts)。以下按能力域完整梳理,签名与返回值均以文档为准。
账号登录(Sign-in)
| 方法 | 签名(简化) | 说明 |
|---|---|---|
signInEmailPassword | (body: SignInEmailPasswordRequest, options?) => Promise<FetchResponse<SignInEmailPasswordResponse>> | 邮箱 + 密码登录;启用 TOTP MFA 时返回 MFA challenge 而非直接会话 |
signInAnonymous | (body?: SignInAnonymousRequest, options?) => Promise<FetchResponse<SessionPayload>> | 匿名登录,总是创建新用户;不受AUTH_DISABLE_AUTO_SIGNUP门控,而是由AUTH_DISABLE_SIGNUP与AUTH_ANONYMOUS_USERS_ENABLED控制 |
signInIdToken | (body: SignInIdTokenRequest, options?) => Promise<FetchResponse<SessionPayload>> | 使用 Apple/Google ID token 登录;用户不存在且未设置AUTH_DISABLE_AUTO_SIGNUP时自动注册 |
signInOTPEmail | (body: SignInOTPEmailRequest, options?) => Promise<FetchResponse<"OK">> | 发起邮箱 OTP 登录,向邮箱发送一次性密码 |
signInPasswordlessEmail | (body: SignInPasswordlessEmailRequest, options?) => Promise<FetchResponse<"OK">> | 魔法链接(magic link)登录 |
signInPasswordlessSms | (body: SignInPasswordlessSmsRequest, options?) => Promise<FetchResponse<"OK">> | 手机号免密登录,发送 SMS OTP |
signInPAT | (body: SignInPATRequest, options?) => Promise<...> | 用 Personal Access Token 登录(自动化系统) |
signInProviderURL | 返回string | 构造 OAuth2 第三方登录的授权跳转 URL |
signInWebauthn | (body: SignInWebauthnRequest, options?) => Promise<...> | 发起 Passkey/WebAuthn 登录,返回 challenge |
注意signInIdToken、signInOTPEmail、signInPasswordlessEmail等方法文档中均明确写道:当服务端启用AUTH_DISABLE_AUTO_SIGNUP后,未注册用户必须先走对应的/signup/...端点注册,这是配置 Auth 服务时需要了解的重要行为分叉。
账号注册(Sign-up)
| 方法 | 说明 |
|---|---|
signUpEmailPassword(body: SignUpEmailPasswordRequest, options?) | 邮箱密码注册,返回SessionPayload |
signUpIdToken(body: SignUpIdTokenRequest, options?) | Apple/Google ID token 注册 |
signUpOTPEmail(body: SignUpOTPEmailRequest, options?) | 邮箱 OTP 注册 |
signUpPasswordlessEmail(body: SignUpPasswordlessEmailRequest, options?) | 魔法链接注册 |
signUpPasswordlessSms(body: SignUpPasswordlessSmsRequest, options?) | 手机号免密注册 |
signUpProviderURL(params, options?) | 构造第三方登录的注册跳转 URL |
signUpWebauthn(body: SignUpWebauthnRequest, options?) | 发起 Passkey 注册,返回PublicKeyCredentialCreationOptions |
会话、令牌与验证
| 方法 | 说明 |
|---|---|
refreshToken(body: RefreshTokenRequest, options?) | 用刷新令牌换新 JWT;旧刷新令牌被吊销并颁发新的(轮换机制),返回Session |
signOut(body: SignOutRequest, options?) | 登出/吊销会话 |
tokenExchange(body: TokenExchangeRequest, options?) | PKCE 授权码换会话:code(重定向获得的授权码)+codeVerifier(43–128 字符) |
verifyToken(body: VerifyTokenRequest, options?) | 校验访问令牌有效性 |
verifyTicketURL(params?) | 构造验证链接(如邮箱验证、密码重置)的跳转 URL |
getJWKs(options?) | 获取 JWKS 公钥集,用于客户端验证 JWT 签名 |
verifySignInMfaTotp/verifySignInOTPEmail/verifySignInPasswordlessSms/verifySignInWebauthn/verifySignUpWebauthn/verifyElevateWebauthn/verifyAddSecurityKey/verifyChangeUserMfa/verifyChangeUserPhoneNumber | 各验证流程的"第二步":提交 TOTP 码、邮箱 OTP、SMS OTP、WebAuthn 断言等完成验证 |
用户信息变更(需 elevated 权限)
| 方法 | 说明 |
|---|---|
getUser(options?) | 获取当前用户资料(角色、元数据、账号状态),返回User |
changeUserEmail(body: UserEmailChangeRequest, options?) | 修改邮箱,向新邮箱发验证邮件 |
changeUserPassword(body: UserPasswordRequest, options?) | 修改密码;该操作会原子地吊销包括当前请求在内的一切会话,客户端必须视为已登出并重新登录 |
changeUserPhoneNumber(body: UserPhoneNumberChangeRequest, options?) | 修改手机号,向新号码发 SMS OTP,验证通过前原号码不变 |
changeUserMfa(options?) | 生成 TOTP 密钥(返回TotpGenerateResponse:QR 码图片 URL + 密钥明文),用于开启/切换 MFA |
deanonymizeUser(body: UserDeanonymizeRequest, options?) | 匿名用户转正:补充邮箱(可选密码)凭据 |
deanonymizeUserSms(body: UserDeanonymizeSmsRequest, options?) | 匿名用户转正:补充手机号,后续通过/signin/passwordless/sms/otp完成验证 |
sendPasswordResetEmail(body: UserPasswordResetRequest, options?) | 发送密码重置邮件 |
sendVerificationEmail(body: UserEmailSendVerificationEmailRequest, options?) | 发送邮箱验证链接 |
linkIdToken(body: LinkIdTokenRequest, options?) | 用 ID token 把当前账号与外部 OAuth 提供方账号绑定 |
请求体中的约束值得注意(来源 client.ts):密码字段password/newPassword长度为 3–50 字符(见UserPasswordRequest);UserDeanonymizeRequest中signInMethod取值为'email-password' | 'passwordless';多个验证类请求(UserEmailChangeRequest、SignUpWebauthnVerifyRequest、UserDeanonymizeRequest等)都支持可选的codeChallenge字段——"PKCE code challenge (S256),提供且需要邮箱验证时,验证重定向中返回授权码而不是刷新令牌",模式为^[A-Za-z0-9_-]{43}$。
WebAuthn / 安全密钥
| 方法 | 说明 |
|---|---|
addSecurityKey(options?) | 初始化添加 WebAuthn 安全密钥,返回PublicKeyCredentialCreationOptions(challenge),需 elevated 权限 |
verifyAddSecurityKey(body, options?) | 提交CredentialCreationResponse完成密钥绑定 |
elevateWebauthn(options?) | 为已登录用户生成提权 challenge(PublicKeyCredentialRequestOptions) |
verifyElevateWebauthn(body, options?) | 完成提权 |
相关类型(如AuthenticatorAssertionResponse的authenticatorData/clientDataJSON/signature均为 Base64url 编码,AuthenticatorAttestationResponse额外含attestationObject、publicKeyAlgorithm、transports等)用于对接浏览器的navigator.credentialsAPI。
PAT(Personal Access Token)
createPAT(body: CreatePATRequest, options?): Promise<FetchResponse<CreatePATResponse>>;生成可用于自动化系统的长期令牌,可替代常规认证流程,需 elevated 权限。配合signInPAT使用。
OAuth2 / OpenID Connect 标准端点
Auth 服务自身也是一套完整的 OAuth2 授权服务器,SDK 完整暴露了 RFC 标准端点:
| 方法 | RFC / 说明 |
|---|---|
oauth2AuthorizeURL(params?)/oauth2AuthorizePostURL(body, options?) | 授权端点(GET/POST),返回跳转 URL 字符串 |
oauth2Token(body: OAuth2TokenRequest, options?) | 令牌端点:支持authorization_code与refresh_token两种 grant |
oauth2Introspect(body, options?) | RFC 7662 令牌自省 |
oauth2Revoke(body, options?) | RFC 7009 令牌吊销 |
oauth2UserinfoGet/oauth2UserinfoPost | UserInfo 端点(GET/POST) |
oauth2Jwks(options?) | OAuth2/OIDC 签名公钥 JWKS |
getOpenIDConfiguration(options?)/getOAuthAuthorizationServer(options?) | OpenID Provider Metadata 与授权服务器元数据(RFC 8414),两者内容相同 |
oauth2LoginGet(params?)/oauth2LoginPost(body, options?) | 供同意(consent)UI 调用的内部端点:获取授权请求详情 / 完成登录并回调授权码 |
OAuth2AuthorizeParams包含标准授权参数,OAuth2TokenRequest的grant_type为'authorization_code' | 'refresh_token'枚举(见文档类型定义节)。
第三方 Provider 令牌
| 方法 | 说明 |
|---|---|
getProviderTokens(provider: SignInProvider, options?) | OAuth 回调后立即拉取提供方令牌(access token / refresh token / 过期信息);会话在数据库中被清除,必须紧接着回调调用,用户需自行安全存储(如 localStorage) |
refreshProviderToken(provider, body, options?) | 用 refresh token 刷新提供方访问令牌,避免用户重复认证 |
linkIdToken(body, options?) | 见用户信息变更小节 |
服务诊断
healthCheckGet()(返回"OK")、healthCheckHead()(返回void)、getVersion()(返回GetVersionResponse200)用于探活与版本探测。
此外还有pushChainFunction(chainFunction: ChainFunction): void——向该客户端的 fetch 中间件链追加一个自定义中间件(见下文中间件链一节)。
核心数据结构速览
文档"Interfaces"一节定义了 60 余个类型,这里摘取最关键的几组(完整定义见 auth.md 与 client.ts):
会话三件套
Session:刷新令牌接口返回,包含新访问令牌、刷新令牌与用户信息;SessionPayload:多数登录/注册接口返回;SignInEmailPasswordResponse:邮箱密码登录专用返回,可能带 MFA challenge(MFAChallengePayload)。
用户模型User(client.ts#L1382-L1457):
export interface User { avatarUrl: string; // 头像 URL createdAt: string; // date-time defaultRole: string; // 默认授权角色,如 "user" displayName: string; email?: string; // email 格式 emailVerified: boolean; id: string; // UUID isAnonymous: boolean; locale: string; // 2-3 字符语言码 metadata: Record<string, unknown> | null; phoneNumber?: string; phoneNumberVerified: boolean; roles: string[]; // 如 ["user","customer"] activeMfaType?: string; // 当前启用的 MFA 类型 }MFA 相关:TotpGenerateResponse含imageUrl(QR 码 data URL,形如"data:image/png;base64,...")与totpSecret(用于手动输入密钥);UserMfaRequest的activeMfaType为'totp' | ''——传空字符串即关闭 MFA。
OAuth/OIDC 类型:OAuth2DiscoveryResponse、OAuth2TokenRequest/Response、OAuth2IntrospectRequest/Response(含token_type_hint)、OAuth2RevokeRequest、OAuth2UserinfoResponse、ProviderSession等;WebAuthn 类型:PublicKeyCredentialCreationOptions/RequestOptions、AuthenticatorSelection(authenticatorAttachment、residentKey、userVerification等)、AuthenticatorAttestationResponse、AuthenticatorAssertionResponse;以及JWK/JWKSet、ErrorResponse、OKResponse(字面量'OK')等。
PKCE 工具:generatePKCEPair 与实现细节
auth 子模块还导出三个 PKCE(Proof Key for Code Exchange,RFC 7636)工具函数,用于邮箱验证等"重定向中携带授权码"的场景:
function generateCodeVerifier(): string; // 43 个 base64url 字符的随机 verifier function generateCodeChallenge(verifier: string): Promise<string>; // 由 verifier 派生 S256 challenge function generatePKCEPair(): Promise<{ verifier: string; challenge: string }>;实现见 pkce.ts:generateCodeVerifier用crypto.getRandomValues生成 32 字节随机数后转为 base64url(替换+//并去掉尾部=),正好得到 43 字符,与codeChallenge字段的正则^[A-Za-z0-9_-]{43}$对齐;generateCodeChallenge用crypto.subtle.digest('SHA-256', ...)对 verifier 做 S256 摘要再 base64url 编码。文件头注释说明其依赖 Web Crypto API,因此适用于浏览器、Node.js >= 19、Bun 与 Deno 等运行时。相关行为有测试覆盖:pkce.test.ts。
典型用法:注册时调用generatePKCEPair(),把challenge传给signUpEmailPassword/sendVerificationEmail等的codeChallenge字段,verifier保存在本地;验证重定向回来后携带授权码code与verifier调用tokenExchange换取会话。
中间件链与主客户端的会话管理
文档中Client的方法表里有pushChainFunction(),它背后是 createEnhancedFetch 实现的洋葱模型中间件:中间件数组通过reduceRight反向包裹原生fetch,按声明顺序执行,每个中间件都能在调用next前后分别拦截请求与响应。
主客户端工厂在 nhost.ts 中定义了三组开箱即用的配置函数,它们会把中间件同时挂到auth、storage、graphql、functions四个客户端上:
withClientSideSessionMiddleware(L50-L69):客户端场景默认使用,链式为sessionRefreshMiddleware(令牌临近过期自动刷新)→updateSessionFromResponseMiddleware(响应带来新令牌时更新会话存储)→attachAccessTokenMiddleware(为所有服务请求挂载 Authorization 头)。createClient()会自动注入这一配置;withServerSideSessionMiddleware:服务端场景,刻意去掉自动刷新中间件以避免并发请求下的竞态条件;createServerClient()必须显式传入storage(如基于 cookie 的实现);withAdminSession(adminSession)/withChainFunctions(fns):前者用 admin secret 为 storage/graphql/functions 客户端附加特权会话(源码注释明确警告切勿用于客户端代码);后者向全部客户端追加自定义中间件。
会话本身由sessionStorage持久化,主客户端暴露getUserSession()、refreshSession(marginSeconds = 60)(提前 N 秒刷新,传 0 强制刷新)与clearSession();浏览器下默认使用 localStorage,其他环境降级为内存存储,也可通过storage: new CookieStorage({...})等自定义后端替换(见 session/ 模块)。
文档与源码的对应关系
这份参考文档并非手写维护的散文,而是与源码严格同步的生成物:模块级说明直接引用了 docstrings.test.ts 中的测试代码片段({@includeCode ...}指令),auth.md里的 Usage 与 Error handling 示例即取自该测试文件;每个方法的签名、参数表与返回类型则由 OpenAPI 代码生成流程产出(gen.sh 驱动,devDependencies 含openapi3-ts)。因此当你发现文档与行为不一致时,优先核对 client.ts 的 JSDoc 与__tests__目录下的测试,它们是第一手依据。
实践要点小结
- 常规应用用
createClient({ subdomain, region })即可获得带自动会话管理的nhost.auth;只需认证 API 时可用@nhost/nhost-js/auth的createAPIClient(baseURL, chainFunctions); - 统一用
instanceof FetchError分支错误处理:需要状态码/原始响应体看err.status/err.body,只需文案直接用err.message; - 注意
changeUserPassword会吊销全部会话,成功后必须重新登录; - 邮箱验证等重定向流程建议配合
generatePKCEPair()+codeChallenge+tokenExchange,避免刷新令牌直接暴露在重定向 URL 中; - 涉及自动注册的行为受服务端环境变量影响:
AUTH_DISABLE_AUTO_SIGNUP、AUTH_DISABLE_SIGNUP、AUTH_ANONYMOUS_USERS_ENABLED,部署前应在 Auth 服务配置中确认其取值; - 包要求 Node.js >= 22(package.json
engines字段),PKCE 工具依赖 Web Crypto,Node 侧需 >= 19 的globalThis.crypto。
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考