用于 Faable Auth 的异构 JavaScript 客户端 .
完整文档 faable.com/docs (英语)
特征
- OAuth 社交连接( Google, GitHub, .) 与 PKCE 和 暗流
- 用户名+密码登录
- 无密码:电子邮件魔法链接和OTP代码
- 以跨 Tab 同步方式通过
BroadcastChannel - 可插式存储适配器
localStorage,饼干,或自定义) - Next.js 服务器侧会话助手
安装
npm install @faable/auth-js
需要节点.js >=22.8 促进发展。 已发布的捆绑在任意
现代浏览器和在节点/SSR环境中.
快开始
import { createClient } from '@faable/auth-js'
export const auth = createClient({
domain: '<faableauth_domain>',
clientId: '<client_id>',
redirectUri: window.location.origin
})
// Trigger a social login
await auth.signInWithOauthConnection({ connection: 'google' })
配置
createClient(config) 接受:
| 选项 | 类型 | 说明 |
|---|---|---|
domain | string | 要求. 你的法奥思租户领地。 议定书是任择性的—— tenant.auth.faable.link 财务报告和审定财务报表 https://tenant.auth.faable.link 等同起来. |
clientId | string | 要求. 应用程序客户端ID. |
redirectUri | string | 默认回调 URL 。 倒计时 window.location.origin。 。 。 . |
scope | string | 空相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 默认为 openid profile email。 。 。 . |
storage | SupportedStorage | 自定义存储适配器 。 默认为 localStorage。 。 。 . |
storageKey | string | 存储密钥的前缀 。 最后键是 ${storageKey}-${clientId}。 。 。 . |
cookieOptions | CookieOptions | 设定时,将存储器切换到饼干适配器. |
lock | LockFunc | 自定义锁定原始同时刷新 . |
debug | boolean | 启用动词记录 . |
认证流量
OAuth / 社会联系
// The tenant's hosted login (signs in silently with a live SSO session)
await auth.signIn()
// Or pick a specific provider (by name or connection_id)
await auth.signInWithOauthConnection({
connection_id: 'conn_01HX…', // preferred when known; falls back to `connection` for legacy tenants
redirectTo: 'https://app.example.com/callback',
scopes: 'openid profile email',
queryParams: { prompt: 'select_account' }
})
在浏览器中,SDK默认使用PKCE流并交换 code (单位:千美元)
在调用页面上。 第一个呼叫 createClient 自动
当用户返回重定向目标时处理 URL.
在重定向成功路径上,返回的承诺 未解决 - 那个
浏览器已经导航了, 所以装入状态您会绑定到 await
直到页面卸下,而不是闪回闲置。 别
可在 await 在这条路上.
自行控制导航(例如自定义时间,或非重定向)
运行时间,通过 skipBrowserRedirect: true。然后,该呼吁与
授权 URL 并留给您导航 :
const { data, error } = await auth.signInWithOauthConnection({
connection: 'google',
skipBrowserRedirect: true
})
if (error) throw error
window.location.assign(data.url)
当回程出现错误时
被拒绝的登录或链接( 行动拒绝, 身份已经与另一个连接)
user...)返回到您的重定向目标 ?error=…和承诺
左边的电话和导航器一起死了 每页装入一次
getRedirectError()它带有浏览器对往返的记忆
失败: 连接, 是否为 linkOauthConnection,则
appState 这是从开始的.
await auth.linkOauthConnection({
connection_id: GOOGLE,
appState: { provider: 'google' }
})
// Back on the page:
const err = await auth.getRedirectError()
if (err?.error_code === 'identity_already_linked') {
toast(`That ${err.appState?.provider} account belongs to another user`)
}
每个客户端都有一个错误 storageKey,且仅限
一个呼叫者得到它: 第二个呼叫—— 另一个客户端实例, 或 React PrintMode
运行两次效果——获得 null。 。 。 .
用户名+密码
await auth.signInWithUsernamePassword({
username: '[email protected]',
password: '••••••••',
redirectTo: 'https://app.example.com/callback'
})
无密码(魔法链接或检察官办公室)
// Step 1 — request a code or link
await auth.signInWithPasswordless({
email: '[email protected]',
type: 'code' // or "link"
})
// Step 2 — complete the login with the OTP the user received
const { data, error } = await auth.signInWithOtp({
username: '[email protected]',
otp: '123456'
})
发送的代码会被记住直到它用完, 所以要求它的屏幕可以 重新装入后, 从另一个标签中取出, 或在应用中取出 浏览器登录在用户读取电子邮件时关闭打开 。 任意 签入或签出客户 忘了它.
const pending = await auth.getPendingPasswordless()
if (pending) showCodeStep(pending.email, pending.sent_at)
// "Change email"
await auth.clearPendingPasswordless()
signInWithPasswordless 还要 authParams (账户概况)
代码创建, resumeUrl (你的一页,代码电子邮件链接回,在一个
(a) 客户的注册籍)和 appState (按待决代码,
从未发送). resumePasswordless({ email, sent_at }) 记录此代码
浏览器没有要求, 例如其电子邮件链接到 resumeUrl。 。 。 .
密码重置
await auth.changePassword({ email: '[email protected]' })
账户:密码、配置、链接账户和会话
登录用户会话时, 不 client_secret 没有管理 API :
// "Set a password" or "Change password"?
const { data } = await auth.getCredentials()
// data: { has_password: false, connections: [{ id, name, type: 'google_oauth2' }] }
// Change it (the current password is required)…
await auth.setPassword({ password: next, current_password: current })
// …or set the first one. That needs a verified email and a recent sign-in
// (with the second factor, if the user has one); otherwise `reauth_required`.
const { error } = await auth.setPassword({ password: next })
if (error?.code === 'reauth_required') {
await auth.authorize({ queryParams: { max_age: '0' } })
}
// Name, company and country, and the app's own profile fields (in
// user_metadata["faable.com/fields"]; only keys declared in the client's or
// tenant's `profile_fields` are accepted). Keys left out are not touched.
await auth.updateProfile({
name: 'Ada Lovelace',
country_iso: 'GB',
fields: [{ key: 'profile', value: 'teacher' }]
})
// One field at a time (null removes it), and reading it back. The session's
// user is updated on the spot, so getSession() sees the new value too.
await auth.updateProfileField('profile', 'teacher')
const profile = await auth.getProfileField('profile') // 'teacher'
// Unlink a Google, GitHub or GitLab account (a connection id from
// getCredentials). Refused when it is the user's last way to sign in.
const { error: unlinkError } = await auth.unlinkIdentity(connection_id)
if (unlinkError?.code === 'last_sign_in_method') {
// Ask them to set a password or link another account first.
}
// Where the user is signed in, and "sign out everywhere else".
const { data: sessions } = await auth.listSessions()
// sessions: { results: [{ id, user_agent, ip, last_seen_at, … }], current_session_id }
const { data: ended } = await auth.revokeOtherSessions() // { revoked: 2 }
// Delete the account. Permanent: confirm first. Clears the local session and
// emits SIGNED_OUT.
await auth.deleteAccount()
设置或更改密码将用户从每个密码中标记出去 其他项目 第二届会议 并发给他们一封安全邮件.
签字
默认 signOut() 签名用户退出 仅此应用程序:它的新鲜度
auth 服务器上撤销了指针并清除了存储,而不离开
页 次 租户的SSO会话会留下,所以用户的其他应用软件都一样
房客(你的仪表板,你的文件... ) 让他们签到,然后签到
这里不问就回来.
为了结束SSO会议,通过 scope: 'global'在浏览器中
导航页面到认证服务器 /logout因为SSO饼干
生活在认证域和跨源 fetch 既不能发出,又不能发出
这个.
await auth.signOut() // this app only; other apps of the tenant stay signed in
await auth.signOut({ scope: 'global' }) // + the SSO session: redirects to /logout
await auth.signOut({ scope: 'global', returnTo: 'https://app.example.com/bye' })
await auth.signOut({ scope: 'local' }) // only this device's storage, server not told
await auth.signOut({ scope: 'others' }) // every OTHER device; this one stays signed in
await auth.signOut({ scope: 'global', redirect: false }) // legacy: best-effort fetch
范围: app (默认)撤销了此应用程序的刷新令牌并清除了它的存储.
global 结束 此浏览器 在 auth 服务器上进行会话, 所以每次刷新
从那时起,每个应用软件, 和浏览器,
导航到 /logout。 。 。 . local 只清除存储; 服务器未被告知.
others 电话 POST /me/sessions/revoke-others: 其他届会
用户端, 本地端留下, 否 SIGNED_OUT 火灾.
从 2. x 升级 :
signOut()曾经的意思scope: 'global'时 退出按钮应结束SSO会话, 通过{ scope: 'global' }。 。 。 .
returnTo 地图到 OIDC post_logout_redirect_uri 财务报告和审定财务报表 必须登记
作为客户端的登录 URL,或服务器响应 400。 。 。 .
在重定向路径上,返回的保证不能解析(浏览器是
导航)——在完成后不再重新启用UI await来驱动
自己导航, 以 getLogoutUrl数字 :
window.location.assign(
auth.getLogoutUrl({ returnTo: 'https://app.example.com' })
)
没有登录屏幕的令牌
getTokenSilently() 返回实时访问符或无法存在的原因
一个 它首先刷新(refresh_token 给予, 不导航; 当刷新时
拒绝——没有会话,或者会话被别处的签出取消,a
管理员,或后通道登录——它发送浏览器通过
/authorize?prompt=none 作为 顶级导航 (无iframe:第三方)
饼干阻断使一个不可靠。 认证服务器将用户签回
从它的SSO饼干 没有屏幕,降落在您身上 redirectUri,与
用户在 returnTo在这条道路上,承诺永远无法实现.
const { data, error } = await auth.getTokenSilently()
if (error) {
// `login_required`: the SSO session is gone too — show a login.
if (error.code === 'login_required') auth.authorize({ response_type: 'code' })
return
}
fetch('/api/me', { headers: { Authorization: `Bearer ${data.access_token}` } })
过 { redirect: false } 获取 AuthLoginRequiredError 代替
导航 页面负载本身是拒绝的默想返回
(单位:千美元)?error=login_required永远不再导航.
处理错误
客户有 一项统一适用的错误合同: 每个同步
方法解决方式 { data, error } 财务报告和审定财务报表 从不为预期投出
失败 (不良的资质,错误的OTP,缺失会话,网络错误. ). 打开
成功 error 实值 null; 失败时 data 实值 null 财务报告和审定财务报表 error 是一个
AuthError。总是检查 error 读取前 data数字 :
const { data, error } = await auth.signInWithOtp({ username, otp })
if (error) {
showError(error.message)
return
}
useSession(data.session)
唯一能抛出的东西是 createClient 本身,并且只为
配置错误( 缺失) domain 编号 : clientId)——你修补的编程错误
一次,不是一个运行时间条件 赶上.
这适用于 signInWithOauthConnection, (中文(简体) ), signInWithUsernamePassword, (中文(简体) ),
signup, (中文(简体) ), signupAndLogin, (中文(简体) ), signUp, (中文(简体) ), signInWithOtp, (中文(简体) ), signInWithPasswordless, (中文(简体) ),
changePassword, (中文(简体) ), changeEmail, (中文(简体) ), signOut, (中文(简体) ), getSession, (中文(简体) ), getClaims, (中文(简体) ),
setSession, (中文(简体) ), refreshSession, (中文(简体) ), initialize 财务报告和审定财务报表 handleRedirectCallback他们
返回类型(AuthResult<T>, (中文(简体) ), AuthResponse, (中文(简体) ), OAuthResponse)都是变体
形状相同.
更喜欢投球吗? 使用 unwrap
如果您的代码路径宁愿让错误传播( 服务器处理器, a)
try/catch将一切正常投放的包装纸 将呼叫包入
unwrap 而不是手写 if (error) throw error数字 :
import { unwrap } from '@faable/auth-js'
// returns data on success, throws the AuthError on failure
const { session } = unwrap(await auth.signInWithOtp({ username, otp }))
会话和状态变化
// Get the current session (refreshes if needed)
const {
data: { session }
} = await auth.getSession()
// Subscribe to auth events
const {
data: { subscription }
} = auth.onAuthStateChange((event, session) => {
// event: INITIAL_SESSION | SIGNED_IN | SIGNED_OUT | TOKEN_REFRESHED | PASSWORD_RECOVERY | USER_UPDATED
})
// Stop listening
subscription.unsubscribe()
// Force a refresh
await auth.refreshSession()
使用标签播放认证事件 BroadcastChannel,所以一个登录或
在一个分页中签出,则在使用同一分页的其他分页中反映 storageKey。 。 。 .
读取令状索赔
习惯声称你的房客在访问符上贴上连接
claims_mapping 或动作 api.accessToken.setCustomClaim ——已有
两种方式:
- 在用户上 :
/me返回它们为顶级属性,所以session.user['ciapol.com/station_id']刚刚工作. - 从符文中解码,没有请求:
// Typed by namespace; refreshes an expired session first, like getSession()
const { data } = await auth.getClaims<{ 'ciapol.com/station_id': string }>()
data.claims?.['ciapol.com/station_id'] // 'station_123456789'
data.claims?.scope // standard claims are typed too
// One claim, or null when signed out / absent
const station = await auth.getClaim<string>('ciapol.com/station_id')
令牌被解码,而不是签名验证:对UI和路由的使用要求 决定,绝不作为授权——这是资源服务器的工作. 自定义 权利主张在登录时被冻结,在刷新后幸存;在下一个时变化 注册.
角色和权限
用你的爱皮 token_dialect 设置为 access_token_authz,访问符
携带一个 permissions 索赔; 与 include_roles_in_access_token页:1 roles
索赔。 询问 Api 用 audience 当亚庇启用RBAC时,
请求作为范围所需的权限: 符号带有用户的
角色授予.
const auth = createClient({
domain: 'https://my-tenant.auth.faable.link',
clientId: '…',
audience: 'https://api.example.com',
scope: 'openid profile email invites:read invites:write'
})
if (await auth.hasPermission('invites:write')) showInviteButton()
const isStaff = await auth.hasRole('staff')
两者都要求 全部 你通过的价值。 喜欢 getClaims他们解码
符号而不验证: 显示 UI 的罚款, 但是您的 API 必须检查
符号本身( e)verifyAccessToken 财务报告和审定财务报表 requirePermissions 内
@faable/auth-sdk) (中文(简体) ). 纯洁的助手 permissionsOf, (中文(简体) ), hasPermissions 财务报告和审定财务报表
hasRoles 处理任何已解码的索赔.
存储适配器
权衡
刷新符号很敏感:任何读取它们的人都可以冒充用户 直到吊销该标志。 你选的仓库决定他们住在哪里:
localStorage(默认) – 简单并支持跨tab同步通过BroadcastChannel,但运行在同一源代码上的任何脚本都可以读取. A 类 单 XSS 允许攻击者去过滤刷新符. 可接受 低风险应用程序和原型;当表面有第三方时不推荐 脚本,由用户生成的HTML,或严格的遵从要求.- 饼干 —— SSR(服务器在每次请求中都读取)和
只有适配器允许您使用
Secure, (中文(简体) ),SameSite,以及Domain。请注意,这个库是从 JavaScript 写的 cookie,所以 无法标记HttpOnly; XSS 还可以读取, 但 cookie 制作 CSRF 和以某种方式可执行的同一地点政策localStorage没有. - 自定义适配器 - 用于内存存储(在重新装入时丢失、最安全的纸箱) 针对XSS),Web Workers,或针对平台的密钥链.
如果您的应用程序被曝光到不可信内容, 则选择 cookie Secure: true
财务报告和审定财务报表 SameSite: "Lax" (或 "Strict"),治疗XSS预防(CSP,出逃,
框架保障)作为一个硬性要求,无论您是哪一个适配器
选择.
本地服务器( 默认)
在浏览器中自动使用. 不需要配置 .
饼干
用于服务器必须从请求中读取会话的 SSR 设置 .
import { createClient } from '@faable/auth-js'
export const auth = createClient({
domain: '<faableauth_domain>',
clientId: '<client_id>',
storage: 'cookie'
})
就是这样。 适配器设置合理的默认值 : Path=/, (中文(简体) ), SameSite=Lax自动
Secure 和30天时间 Max-Age 所以用户在浏览器之间签名
重启 .
使用 cookieOptions 仅在您需要覆盖某样东西时——例如共享
跨子域的会话 :
createClient({
domain: '<faableauth_domain>',
clientId: '<client_id>',
storage: 'cookie',
cookieOptions: { domain: '.example.com' }
})
自定义适配器
提供执行对象 getItem, (中文(简体) ), setItem,以及 removeItem (同步)
),或者说Aync. 设定 isServer: true 如果数值可能来自不可信来源,则
作为请求饼干.
const memoryStorage = {
store: new Map<string, string>(),
getItem: (k: string) => memoryStorage.store.get(k) ?? null,
setItem: (k: string, v: string) => void memoryStorage.store.set(k, v),
removeItem: (k: string) => void memoryStorage.store.delete(k)
}
createClient({ domain, clientId, storage: memoryStorage })
Next.js / 服务器侧
@faable/auth-js/nextjs 运行登录 服务器: App 路由器处理器
页:1 login, (中文(简体) ), callback, (中文(简体) ), logout 财务报告和审定财务报表 backchannel-logout,一个 HttpOnly
无法读取的加密会话 cookie, id_token 财务报告和审定财务报表 logout_token
由租户JWKS核实jose页:1 getAccessToken() 那个
在服务器上刷新 。 需要节点 20+ 或边缘运行时间,以及 next 13+ (英语).
// lib/faable-auth.ts
import { createFaableAuth } from '@faable/auth-js/nextjs'
export const faableAuth = createFaableAuth({
domain: process.env.FAABLE_AUTH_DOMAIN!, // your-tenant.auth.faable.link
clientId: process.env.FAABLE_AUTH_CLIENT_ID!,
clientSecret: process.env.FAABLE_AUTH_CLIENT_SECRET, // confidential clients only
secret: process.env.FAABLE_AUTH_SECRET!, // ≥ 32 chars; encrypts the cookie
baseUrl: process.env.FAABLE_AUTH_BASE_URL! // https://app.example.com
})
// Every field also reads the FAABLE_AUTH_* environment variable of the same name.
// app/auth/[...faable]/route.ts
import { faableAuth } from '@/lib/faable-auth'
export const { GET, POST } = faableAuth.handlers
登记册 https://app.example.com/auth/callback 作为允许召回的 URL,
https://app.example.com/ (或你的电话) returnTo 页面)作为登录 URL,以及
https://app.example.com/auth/backchannel-logout 作为客户的
backchannel_logout_uri。然后链接到 /auth/login?returnTo=/dashboard 财务报告和审定财务报表
/auth/logout。 。 。 .
// app/dashboard/page.tsx — Server Component
import { redirect } from 'next/navigation'
import { faableAuth } from '@/lib/faable-auth'
export default async function Dashboard() {
const session = await faableAuth.getSession()
if (!session) redirect('/auth/login?returnTo=/dashboard')
return <h1>Hello {session.user.email}</h1>
}
// app/api/orders/route.ts — call an API on the user's behalf
export async function GET() {
const token = await faableAuth.getAccessToken() // refreshes when needed
if (!token) return new Response(null, { status: 401 })
return fetch('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${token.accessToken}` }
})
}
// middleware.ts — keep the session fresh, gate a section
import type { NextRequest } from 'next/server'
import { faableAuth } from '@/lib/faable-auth'
export const middleware = (req: NextRequest) =>
faableAuth.middleware(req, {
protect: req => req.nextUrl.pathname.startsWith('/dashboard')
})
export const config = { matcher: ['/((?!_next|favicon.ico).*)'] }
检查服务器上的权限 已核实 访问符 (JWKS,
iss, (中文(简体) ), aud, (中文(简体) ), exp),这样他们就可以打开页面和动作. 他们需要
authorizationParams.audience 设置到您的 Api 标识符( 并使用 RBAC 在
Api, 请求的权限 authorizationParams.scope:
// Server Component / Server Action
if (!(await faableAuth.hasPermission('invites:write'))) notFound()
// middleware.ts — anonymous → login, signed in without the permission → 403
faableAuth.middleware(req, {
permissions: req =>
req.nextUrl.pathname.startsWith('/admin') ? ['admin:read'] : undefined
})
当用户在同一 SSO 会话中从另一个应用程序中签名时, 认证服务器
职位a logout_token 改为 /auth/backchannel-logout援助者证实它
从那时起, getSession() 返回时 null 虽然这届会议
浏览器仍然发送饼干。 记录存在于 SessionStore 内
默认内存( 一个进程); 当应用程序运行时, 将自己的内存传递给 Redis
有几个例子.
遗产 : getSessionFromCookies (折旧)
浏览器 SDK 带有 storage: 'cookie' 写入服务器可以解析的饼干
与 getSessionFromCookies这是 没有 HttpOnly 未核实 - 任何
页面上的脚本,或伪造的请求,可以放入任意会话——所以
使用它只是为了方便, 从来没有作为授权。 新应用程序使用
@faable/auth-js/nextjs 条目.
文档
完整导游、API参考和仪表板设置的走行道访问 faable.com/docs (英语)。 。 。 .
许可证
麻省理工学院——见 自由主义。 。 。 .