@ atlasauth/ 后端
用于节点、德诺、邦和边缘运行时间的服务器侧图集。 它有三样东西:
- 核实 阿特拉斯会话令牌、webhooks和后通道登录令牌
- 运行主机- 直接( OpenID Connect) 登录的服务器侧面
- 为后端 API 提供一个输入客户端, 使用您的密钥( W)
sk_...(中文(简体) ) - 给您一个平台 API 的打字客户端, 使用您的平台密钥( Name
psk_...(中文(简体) )
什么都不需要,不过 fetch。 。 。 .
npm install @atlasauth/backend
校验会话
import { AtlasBackend } from '@atlasauth/backend';
const atlas = new AtlasBackend({ issuer: 'https://clerk.example.com' });
const result = await atlas.verify(sessionToken);
if (result.ok) console.log(result.claims.sub);
JWKS是从发行商找到并被缓存的. 共享主机上的例子也通过 instanceId (或 publishableKey) (中文(简体) ).
用重定向( OpenID 连接) 签名
oidc 运行主机-重定向集成的依赖方侧:状态、非ce和PKCE、流程饼干、代码交换和ID-token检查(仅RS256, iss, (中文(简体) ), aud =你的客户身份, nonce, (中文(简体) ), exp) (中文(简体) ).
import { oidc, AtlasOidcError } from '@atlasauth/backend';
const config = {
issuer: 'https://bright-fox-42-fapi.atlasauth.net', // https:// + frontend_api_host
clientId: process.env.ATLAS_CLIENT_ID!,
redirectUri: 'https://app.example.com/auth/callback',
secret: process.env.OIDC_COOKIE_SECRET!, // 32+ random characters; seals the flow cookie
};
// Start: set the flow cookie, redirect.
const { url, cookie } = await oidc.start({ ...config, scope: 'openid profile email' });
// Callback: one call does every check.
const identity = await oidc.callback({
...config,
clientSecret: process.env.ATLAS_CLIENT_SECRET!,
query: request.url, // or URLSearchParams, or req.query
cookie: request.headers.get('cookie'),
});
// identity: { sub, namespacedSub, email, emailVerified, name, idToken, accessToken, refreshToken?, expiresAt, claims, clearCookie }
// Sign out: end your session, then end the Atlas one.
const logout = await oidc.logoutUrl({ ...config, postLogoutRedirectUri: 'https://app.example.com/', idTokenHint });
- 流饼干是
httpOnly, (中文(简体) ),SameSite=Lax, (中文(简体) ),Secure(a项除外)http://localhost),被瞄准回调路径,持续10分钟. 与secret它被加密(AES-GCM),所以不能被读取或被伪造. 无secret它只是编码。 总是通过一个生产. namespacedSub实值${issuer}#${sub}。如果您接受不止一个实例,则其上的关键用户,所以两个实例的用户从未碰撞过.clientAuth实值client_secret_basic(以秘密示弃),client_secret_post,或none(公共客户端,仅限PKCE).new AtlasBackend({ issuer }).oidc使用相同的方法issuer填报.- 你自己的密码交换了吗?
oidc.verifyIdToken(idToken, { issuer, clientId, nonce })运行只是象征性的支票.
失败投出 AtlasOidcError 拥有稳定 code数字 :
code | 含义 |
|---|---|
state_mismatch | 没有流饼干,不一样 state或者是超过10分钟的水流 再来一次. |
access_denied | Atlas 以 OAuth 重定向返回 error。准确值为 oauthError (单位:千美元)access_denied, (中文(简体) ), login_required(中文(简体) ). |
issuer_mismatch | 召回的电话 iss 参数名称为不同的服务器. |
invalid_request | 召回没有 code。 。 。 . |
token_exchange_failed | 符号端点拒绝代码或客户端( S)oauthError, (中文(简体) ), status) (中文(简体) ). |
invalid_token | ID 符号没有通过检查: 算法, 签名, iss, (中文(简体) ), aud编号 :azp, (中文(简体) ), nonce, (中文(简体) ), exp编号 :iat 或者说 at_hash。 。 。 . |
discovery_failed | 发现文件或签名密钥无法被取出或与发行人不符. |
reauthentication_required | start 请求 maxAge 和标识符的 auth_time 较旧(或缺失). |
subject_mismatch | start 请求 expectedSub (一个步骤)和一个不同的用户报名. |
每一个错误来自 callback 携带 clearCookie: 随错误页一起发送.
start 还要 uiLocales, (中文(简体) ), maxAge (回调后执行) auth_time, (中文(简体) ), returnTo (同一地点路径,返回为 identity.returnTo和 expectedSub。该身份带有 sid (Atlas会议d)和 authTime。 。 。 .
@atlasauth/backend/rp: 请求 – 任何运行时间的响应处理器
同样的流量 (Request) => Response 处理器,加分级,会话保持动态和退出。 此入口只装入 Web 标准和 jose,所以它运行 在节点18+,德诺,邦和工人.
import { createRelyingParty } from '@atlasauth/backend/rp';
const rp = createRelyingParty({
issuer: process.env.ATLAS_ISSUER!,
clientId: process.env.ATLAS_CLIENT_ID!,
clientSecret: process.env.ATLAS_CLIENT_SECRET!,
redirectUri: 'https://app.example.com/auth/callback',
secret: process.env.SESSION_SECRET!, // 32+ characters
secretKey: process.env.ATLAS_SECRET_KEY, // for touchIfDue and server-side revoke
postLogoutRedirectUri: 'https://app.example.com/',
});
const startResponse = await rp.start(request); // 302 to /oauth2/authorize
const { user, tokens, response } = await rp.callback(request); // user.sub, user.sid, user.auth_time, ...
const stepUp = await rp.requireRecentAuth(request, 300, { sub: user.sub, authTime: user.auth_time }); // null or a 302
const touch = await rp.touchIfDue(user.sid!, lastTouchedAt); // 'skipped' | 'touched' | 'ended'
const signOut = await rp.signOut({ idToken: tokens.id_token, sid: user.sid });
或用一条线将三条路线都接通 rp.handle(request) (签名、召回和退出); null 对任何其他路径,给予 onSignedIn(user, tokens, request, result) 财务报告和审定财务报表 onSignOut(request) 在配置中. forwardParams: ['ref'] 将您登录的 URL 的额外查询参数传递到 /oauth2/authorize。 。 。 .
还出口 LogoutTokenVerifier, (中文(简体) ), fetchBackchannelLogout 财务报告和审定财务报表 verifyAtlasWebhook 编号 : verifyAtlasWebhookRequest (网易密码).
同一切入点单独公布于 @atlasauth/rp,其唯一依赖是 jose。 。 。 .
Next.js (App路由器) (英语)
// app/auth/login/route.ts
import { oidc } from '@atlasauth/backend';
import { config } from '@/lib/atlas';
export async function GET() {
const { url, cookie } = await oidc.start(config);
return new Response(null, { status: 302, headers: { location: url, 'set-cookie': cookie } });
}
// app/auth/callback/route.ts
import { oidc, AtlasOidcError } from '@atlasauth/backend';
import { config } from '@/lib/atlas';
export async function GET(request: Request) {
try {
const user = await oidc.callback({
...config,
clientSecret: process.env.ATLAS_CLIENT_SECRET!,
query: request.url,
cookie: request.headers.get('cookie'),
});
const headers = new Headers({ location: '/' });
headers.append('set-cookie', user.clearCookie);
headers.append('set-cookie', await createAppSession(user)); // your own session cookie
return new Response(null, { status: 302, headers });
} catch (error) {
if (!(error instanceof AtlasOidcError)) throw error;
return new Response(`Sign-in failed (${error.code})`, {
status: 400,
headers: error.clearCookie ? { 'set-cookie': error.clearCookie } : {},
});
}
}
快递
import express from 'express';
import { oidc, AtlasOidcError } from '@atlasauth/backend';
const app = express();
app.get('/auth/login', async (req, res) => {
const { url, cookie } = await oidc.start(config);
res.append('Set-Cookie', cookie).redirect(url);
});
app.get('/auth/callback', async (req, res) => {
try {
const user = await oidc.callback({
...config,
clientSecret: process.env.ATLAS_CLIENT_SECRET!,
query: req.originalUrl,
cookie: req.headers.cookie,
});
res.append('Set-Cookie', user.clearCookie);
await startYourSession(req, res, user.namespacedSub);
res.redirect('/');
} catch (error) {
if (!(error instanceof AtlasOidcError)) throw error;
if (error.clearCookie) res.append('Set-Cookie', error.clearCookie);
res.status(400).send(`Sign-in failed (${error.code})`);
}
});
测试离线
createTestIssuer() 薄荷会话令牌和带有丢出密钥的ID令牌及其 fetch 服务于发现, JWKS 和一个符号端点, 因此一个完全的重定向的登录在没有网络的单元测试中运行 .
import { createTestIssuer, oidc, TEST_TOKEN_TAMPERS } from '@atlasauth/backend';
const fx = await createTestIssuer({ clientSecret: 'test-secret' });
const config = { issuer: fx.issuer, clientId: fx.clientId, redirectUri: 'https://app.example.com/auth/callback', secret: '…32+ chars…', fetchImpl: fx.fetch };
const { url, cookie } = await oidc.start(config);
const { callbackUrl } = await fx.authorize(url, { sub: 'user_1', claims: { email: '[email protected]', email_verified: true } });
const user = await oidc.callback({ ...config, clientSecret: 'test-secret', query: callbackUrl, cookie });
// Negative cases: wrong_aud, wrong_iss, foreign_key, expired, wrong_nonce, alg_none.
for (const tamper of TEST_TOKEN_TAMPERS) {
const { url, cookie } = await oidc.start(config);
const { callbackUrl } = await fx.authorize(url, { tamper });
await expect(oidc.callback({ ...config, clientSecret: 'test-secret', query: callbackUrl, cookie })).rejects.toMatchObject({ code: 'invalid_token' });
}
fx.mintIdToken(claims, { aud, nonce, tamper }) 财务报告和审定财务报表 fx.mintSession(claims, { tamper }) 薄荷单币. @atlasauth/testing 再出口 createTestIssuer。 。 。 .
管理客户端( 密钥)
import { createAtlasClient, paginate } from '@atlasauth/backend';
const atlas = createAtlasClient({ secretKey: process.env.ATLAS_SECRET_KEY! });
const user = await atlas.users.create({ email_address: '[email protected]' });
for await (const u of paginate(atlas.users.list)) console.log(u.id);
每个后端API操作都有方法,按资源分组. 这些团体是:
| 区域 | 命名空间 |
|---|---|
| 用户和会话 | users, (中文(简体) ), emailAddresses, (中文(简体) ), sessions, (中文(简体) ), signInTokens, (中文(简体) ), actorTokens |
| 组织 | organizations, (中文(简体) ), invitations |
| 作用和授权 | roles, (中文(简体) ), permissions, (中文(简体) ), fga, (中文(简体) ), openfga |
| 访问和注册控制 | waitlist, (中文(简体) ), allowlist, (中文(简体) ), blocklist, (中文(简体) ), domains, (中文(简体) ), redirectUrls |
| 实例设置 | instance, (中文(简体) ), instanceSecurity, (中文(简体) ), design, (中文(简体) ), branding, (中文(简体) ), localizations |
| 模板和信件 | emailTemplates, (中文(简体) ), smsTemplates, (中文(简体) ), messaging, (中文(简体) ), notifications |
| 社会和企业注册 | oauthProviders, (中文(简体) ), connectors, (中文(简体) ), ssoConnections, (中文(简体) ), ssoOnboarding, (中文(简体) ), orgLdapConnections, (中文(简体) ), scimTokens, (中文(简体) ), scimProvisioning, (中文(简体) ), legacyAuthConnection |
| 证书和标志 | oauthClients, (中文(简体) ), resourceServers, (中文(简体) ), jwtTemplates, (中文(简体) ), apiKeys, (中文(简体) ), machines, (中文(简体) ), tokens, (中文(简体) ), sessionExchangeApps, (中文(简体) ), tickets |
| 网络选择和审计 | webhooks, (中文(简体) ), auditLogs, (中文(简体) ), auditEvents, (中文(简体) ), events, (中文(简体) ), usage, (中文(简体) ), configChangelog, (中文(简体) ), logStreams |
| 警卫 | attackProtection, (中文(简体) ), rateLimitPolicy, (中文(简体) ), riskBasedMfa, (中文(简体) ), botSignals, (中文(简体) ), captcha, (中文(简体) ), networkAcls, (中文(简体) ), managedWaf, (中文(简体) ), radiusClients |
| 账单和应享权利 | billing, (中文(简体) ), entitlementOverrides, (中文(简体) ), organizationSettingsSchema |
| 隐私 | dataSubjectRequests, (中文(简体) ), importExport |
| 代理人和整编人员 | actions, (中文(简体) ), pca, (中文(简体) ), mcp, (中文(简体) ), mcpConnector, (中文(简体) ), ltiPlatforms |
| 开发工具 | testMode, (中文(简体) ), diagnostics |
检查您的设置
const report = await atlas.instance.preflight();
for (const f of report.findings) console.log(f.severity, f.message, '→', f.fix);
atlas.doctor() (或 npx atlas-doctor 与 ATLAS_SECRET_KEY set)检查密钥,时钟 skew对Atlas,后端API,JWKS,服务状态和实例配置,并从未丢出:
const report = await atlas.doctor();
if (!report.ok) console.error(formatDoctorReport(report));
退缩和可观察性
429(任何方法)或503(GET、PUT、DELETE、或带有一等钥匙的书写)两次重试,等待 Retry-After 或与焦躁的指数后退。 给它 retry: { maxRetries, baseDelayMs, maxDelayMs } 或通过 retry: false; 当服务器要求等待的时间比 maxDelayMs,则 AtlasApiError 被抛出 retryAfterMs 设置.
createAtlasClient({ secretKey, otel: true }) 记录每个请求的客户间距+ atlas.client.requests, (中文(简体) ), atlas.client.retries 财务报告和审定财务报表 atlas.client.request.duration通过全球 @opentelemetry/api (可选的对等依赖; 未设置, 没有加载) 。 过 otel: { tracer, meter } 用你自己的东西.
设计登录页面
import { readFile } from 'node:fs/promises';
const logo = await atlas.design.assets.upload(await readFile('logo.png'), {
kind: 'logo',
contentType: 'image/png',
});
await atlas.design.update({
palette: { primary: '#4f46e5' },
logo: { assetId: logo.asset_id },
});
// Per-organization override; null removes a setting.
await atlas.design.organizations.update('org_123', { palette: { primary: null } });
上传发送带有内容类型的原始图像字节 。 A 类 Blob 带来自己的类型.
API 密钥支出上限
给终端用户 API 每个单位和期间的密钥封顶(request, (中文(简体) ), second, (中文(简体) ), minute, (中文(简体) ), day, (中文(简体) ), month 或者说 total· ; { unit: 'requests', amount: 600, period: 'minute' } 每笔电话费的限额是每笔电话费的分数。 超过上限的借项被拒绝而不记录任何情况;它与 allowed: false 而不是扔东西.
const key = await atlas.apiKeys.create({
subject_type: 'user',
subject_id: 'user_123',
limits: [
{ unit: 'requests', amount: 1000, period: 'day' },
{ unit: 'usd_cents', amount: 5000, period: 'month' },
],
});
const debit = await atlas.apiKeys.recordUsage(key.id, {
unit: 'usd_cents',
amount: 12,
idempotency_key: 'req_8f2c', // a retry returns the first outcome without counting again
});
if (!debit.allowed) {
// debit.code === 'LIMIT_EXCEEDED'; debit.exceeded lists the caps that refused it
}
const { usage } = await atlas.apiKeys.getUsage(key.id); // used / remaining / resets_at per cap
await atlas.apiKeys.update(key.id, { limits: null }); // remove the caps
apiKeys.verify(secret) 返回密钥 name, (中文(简体) ), scopes, (中文(简体) ), effective_scopes (通过实例范围的影响加以扩大), constraints, (中文(简体) ), limits 和当前 usage。 。 。 .
镜像 Atlas 数据输入您的数据库
createMirror 遵循更改种子(先是snapshot,然后是命令更改,在您的位置被修剪后自动重拍)并写入内存, Redis, Postgres 或您自己的 Drizzle 或 Prisma 表格(@atlasauth/backend/mirror/drizzle, (中文(简体) ), @atlasauth/backend/mirror/prisma) (中文(简体) ).
import { createMirror, memoryMirrorStore } from '@atlasauth/backend';
const mirror = createMirror({ client: atlas, resources: ['user', 'organization'], store: memoryMirrorStore() });
await mirror.sync(); // or mirror.start() to poll
本地 API 密钥镜像
createKeyMirror 保留您自己的表格权威: 薄荷和在阿特拉斯的导入, 对照sha256进行本地验证, 没有网络呼叫, 本地先取消, 并提取从其它处提取的撤销信息 .
import { createKeyMirror, memoryKeyMirrorStore } from '@atlasauth/backend';
const mirror = createKeyMirror({ store: memoryKeyMirrorStore(), apiKeys: atlas.apiKeys });
const { secret } = await mirror.mint({ subject_type: 'organization', subject_id: 'org_123', scopes: ['calls:write'] });
const result = await mirror.verify(presented, { requiredScopes: ['calls:write'] }); // local, no network call
await mirror.revoke(keyId); // local first, then Atlas
setInterval(() => void mirror.sync(), 15_000); // revocation feed
// await mirror.applyWebhook(event) for verified api_key.* webhooks
回来 KeyMirrorStore (单位:千美元)put, (中文(简体) ), get, (中文(简体) ), getByHash, (中文(简体) ), markRevoked, (中文(简体) ), getCursor, (中文(简体) ), setCursor)与自制的桌子.
会话保存
如果您的应用程序只在某些页面上加载了Atlas的前端SDK,则在其他页面上所花的时间可以算作是无活性. 从后端触摸会话以滑动其不活动窗口 :
const s = await atlas.sessions.touch(sessionId);
// s.idle_expires_at is the new inactivity deadline; s.extended is false when a touch in the last minute already did it
触摸从未延续过会话的绝对到期,也从未恢复过会话的结束( )409 SESSION_REVOKED, (中文(简体) ), SESSION_IDLE_EXPIRED 或者说 SESSION_EXPIRED) (中文(简体) ). 它每分钟写一次 所以每次请求都安全.
严格的请求机构
默认情况下, Atlas 忽略了一个实体字段, 端点不接受 。 它列出在 Atlas-Unknown-Fields 响应头.
const atlas = createAtlasClient({
secretKey,
strict: true, // refuse unknown fields with 422 UNKNOWN_FIELD instead
onUnknownFields: ({ method, path, fields }) => console.warn(method, path, 'ignored', fields),
});
// Override for some calls:
await atlas.withOptions({ strict: false }).users.update('user_123', body);
错误
每次失败的调用 AtlasApiError,已经 status, (中文(简体) ), code 和全文 errors 列表中选择一个选项. AtlasErrorCodes 名称 :
import { AtlasApiError, AtlasErrorCodes } from '@atlasauth/backend';
try {
await atlas.notifications.send(body);
} catch (e) {
if (e instanceof AtlasApiError && e.hasCode(AtlasErrorCodes.EMAIL_DELIVERY_NOT_CONFIGURED)) {
// set up an email provider first
}
}
平台客户端( 平台密钥)
import { createAtlasPlatformClient } from '@atlasauth/backend';
const platform = createAtlasPlatformClient({ platformKey: process.env.ATLAS_PLATFORM_KEY! });
const app = await platform.apps.create({ name: 'Acme' });
const signIn = await platform.atlasIdApps.create({
name: 'Acme',
redirect_uris: ['https://acme.example/callback'],
post_logout_redirect_uris: ['https://acme.example/'],
backchannel_logout_uri: 'https://acme.example/backchannel-logout',
branding: { theme: 'dark', accent_color: '#4f46e5' },
});
console.log(signIn.client_id, signIn.client_secret); // the secret is shown once
平台客户端有这些命名空间:
apps: 创建、更新、存档、传输、主张和删除应用程序、管理其实例、旋转和列出机密accountlessAppsatlasIdApps: 与 Atlas 应用签名,包括设计预览和秘密旋转designAssets
它接受同样的 strict, (中文(简体) ), onUnknownFields 财务报告和审定财务报表 withOptions 设置.
域名移动、 登出扇并管理电子邮件
// Move every URL from the old origin to the new one. dry_run first shows the diff.
const preview = await atlas.instance.moveOrigin({ from: 'https://old.example.com', to: 'https://example.com', dry_run: true });
for (const c of preview.changes) console.log(c.object, c.field, c.before, '->', c.after);
// Send the same logout_token your OIDC apps get to another service.
await atlas.logoutNotifyEndpoints.create({ url: 'https://api.example.com/atlas/logout' });
// Send email from your own domain through Atlas, then publish the returned DNS records.
const managed = await atlas.messaging.managedEmail.create({ from_address: 'Acme <[email protected]>' });
console.log(managed.records);
在平台客户端上, atlasIdApps.findUsers(id, email) 回答一个应用程序的隐私请求,
atlasIdApps.revokeGrant(id, grantId) 撤销一个人的许可 atlasIdPublisher 索赔和核实
在同意屏幕上显示的出版商名称.
后通道登录
当用户在Atlas系统中签名时, Atlas POSTs a logout_token 给您的 backchannel_logout_uri。在本地会话结束前进行校验 :
import { LogoutTokenVerifier, memoryLogoutReplayStore } from '@atlasauth/backend';
const logoutTokens = new LogoutTokenVerifier({
issuer: 'https://atlasauth.net', // or your instance's issuer
clientId: process.env.ATLAS_CLIENT_ID!,
replayStore: memoryLogoutReplayStore(), // use a shared store across processes
});
// Framework-agnostic handler: (form body) -> { status, headers }
export async function handleBackchannelLogout(form: URLSearchParams) {
const headers = { 'Cache-Control': 'no-store' };
const token = form.get('logout_token');
if (!token) return { status: 400, headers };
const result = await logoutTokens.verify(token);
if (!result.ok) {
// retryable: our side failed for now (the issuer's keys could not be fetched).
// 503 + Retry-After makes Atlas send it again; 400 is final and never retried.
return result.retryable
? { status: 503, headers: { ...headers, 'Retry-After': '30' } }
: { status: 400, headers };
}
const { sid, sub } = result.claims;
if (sid) await sessions.deleteByAtlasSid(sid); // your session store
else if (sub) await sessions.deleteAllForUser(sub);
return { status: 200, headers };
}
核查员检查如下:
- rS256 签名
iss,该aud包含您的客户端 idiat,它必须是存在的, 不在未来和不太老exp如果有的话jti- 后通道登录事件
- 那个
sid或者说sub当前 - 没有
nonce - 那个
typ实值logout+jwt,当标题有一个
对于由您自己签名的标志, new AtlasBackend({ issuer }).verifyLogoutToken(token, { clientId }) 重用实例缓存的密钥 .
要拒绝在多个服务器上重放, 请执行 LogoutReplayStore.remember(jti, expiresAtMs) 在一个共享的商店,例如 Redis SET NX PX。 。 。 .
阿特拉斯重审一个得到的交付 5xx, (中文(简体) ), 408, (中文(简体) ), 429 5秒5分30分2小时5小时10小时后 任何其他 4xx 最终决定。 阿特拉斯系统从未遵循过方向:a 3xx 答复记录为 backchannel_redirect_not_followed因此,在域移动更新后 backchannel_logout_uri 到最后的URL。 回答自己的短暂失败( 数据库已下调) 503 我也一样.
要检查路线终点到终点, oauthClients.testBackchannelLogout(id) 发送一个签名的测试令牌( s)sid 开始 sid_test_,额外索赔 atlas_test: true并返回所回答的路线; oauthClients.listBackchannelDeliveries(id) 列出每次尝试.
网络呼号
verifyWebhook 请检查Atlas发送到您的 Webhook 端点的事件的签名 。 您管理端点 atlas.webhooks.endpoints: 创建,更新,揭示或旋转一个秘密,发送测试事件,并重报过去交付.
这个 atlas-signature 信头可以携带一个以上的签名,以空格分隔(v1,<sig> v1,<sig>) (中文(简体) ). 虽然一个端点秘密会旋转,但阿特拉斯可能会用旧的和新的秘密在每次交货上签名. verifyWebhook 当任何一种签名符合您的秘密时, 接受发送.
网络加密上的标准 Webhooks
您的应用程序通过 Atlas 发布的事件遵循标准 Webhooks . verifyStandardWebhookWeb({ body, headers, secret }) (和 verifyStandardWebhookRequest(request, { secret }))仅用Web Crypto验证它们,因此它们运行在Workers,Deno,Bun以及浏览器和节点上. signAtlasWebhook 财务报告和审定财务报表 signStandardWebhook 从 @atlasauth/rp/testing 制作正确的签名 交付给您的接受者测试.
类型事件
verifyWebhook 返回一个 AtlasWebhookEvent,一个工会结束 type,所以一个 switch 缩进 data数字 :
const { verified, event } = verifyWebhook({ body: rawBody, headers, secret });
if (!verified || !event) return res.sendStatus(400);
switch (event.type) {
case 'user.created':
await createUser(event.data.id, event.data.primary_email_address);
break;
case 'organization_membership.updated':
console.log(event.data.role, event.data.role_source); // RoleSource | null
break;
default:
// an event type newer than this package version
}
ATLAS_WEBHOOK_EVENT_TYPES 列出每一种类型. RoleSource 实值 "manual", (中文(简体) ), "scim", (中文(简体) ), "directory_sync", (中文(简体) ), "sso:<connection id>" 或者说 "ldap:<connection id>"。 。 。 .
将用户映射到数据库
syncLocalUser 为已签名的Atlas用户查找或创建行。 校验会话后拨打:
const { user, created, linked } = await syncLocalUser(auth.claims, {
findBySub: (sub) => db.user.findUnique({ where: { atlasSub: sub } }),
findByEmail: (email) => db.user.findUnique({ where: { email } }),
subOf: (row) => row.atlasSub,
isLocalEmailVerified: (row) => row.emailVerified,
link: (row, sub) => db.user.update({ where: { id: row.id }, data: { atlasSub: sub } }),
create: ({ sub, email, email_verified, external_id }) =>
db.user.create({ data: { atlasSub: sub, email, emailVerified: email_verified, externalId: external_id } }),
});
- 发现一行
sub先说. - 已有行只在符号上注明时通过电子邮件链接
email_verified: true和isLocalEmailVerified(row)这是真的。 如果将一个地址联系起来,没有人证明,那么登记地址的任何人都会先记账. - 已经绑定到不同 Atlas 用户的行永远不会重新连接 :
AtlasLinkConflictError被抛出. - 在 Atlas 子上设置一个独特的索引 。 当两个先要求比赛,失败者
create失败了syncLocalUser返回胜利者的行.isDuplicateError决定何为失败( 默认: Postgres)23505,或包含"重复"或"独特"的信息.
信物携带 email, (中文(简体) ), email_verified 财务报告和审定财务报表 external_id 当实例在会话符中包含电子邮件请求时.
需要外交部
if (!mfaSatisfied(auth.claims)) return res.status(403).json({ error: 'mfa_required' });
mfaSatisfied 信物上写着的是真的 mfa_satisfied: true,或者当 mfa 没错, acr 实值 urn:atlas:acr:mfa 或者说 amr 包含 mfa,或会议通过企业SSO(sso_connection_id) (中文(简体) ). usedMultiFactor 财务报告和审定财务报表 authenticatedWithin 带 SessionClaims 我也一样.
一个组织外务省的政策是单独的一条条: mfaOrgSatisfied(auth.claims, orgId) 当组织不需要外交部时,或标志上写着 org_mfa_ok. 保险单只扣下组织的债权,因此从未读过的申请不会被停止;从自己的表格中授权的应用程序必须调用 mfaOrgSatisfied 自己来 它只描述该标志的活跃组织;对于其他任何组织,用户属于,呼叫 client.organizations.mfaStatus(orgId, { sessionId })。 。 。 .
中国证监会
对于饼干认证表和 fetch 电话:
const secret = process.env.ATLAS_CSRF_SECRET ?? deriveCsrfSecret(process.env.ATLAS_SECRET_KEY!);
const token = csrfToken({ sid: auth.claims.sid, secret }); // set as the __atlas_csrf cookie, render into the page
// on a POST:
if (!(await verifyCsrf(request, { sid: auth.claims.sid, secret }))) return new Response(null, { status: 403 });
符号绑定在会话上(sid) (中文(简体) ). verifyCsrf 从 x-atlas-csrf 标题或一个 atlas_csrf 构成字段,它必须等于 __atlas_csrf 饼干 形式体是从克隆人读取的,所以你的处理器仍然可以读取. 和下一个Js, atlasProxy({ csrf: true }) 内 @atlasauth/nextjs 做这一切为你.
移交归属
随着转诊归属的开始,阿特拉斯系统在您转诊平台的每次互动注册之前都会报告 user.created 发送。 答案落在用户身上: referral (他们已回避他们,或已回避), referral_code 财务报告和审定财务报表 referral_link (用户自己的代码和链接,当他们是一个裁判).
await atlas.referrals.update({
enabled: true,
provider: 'webhook', // the Atlas referral protocol; or a built-in vendor adapter
endpoint_url: 'https://referrals.example.com/atlas',
signing_secret: process.env.REFERRAL_SIGNING_SECRET,
auto_enroll_users: true,
link_template: 'https://app.example.com/join?ref={code}',
security: { on_high_risk: 'hold' },
});
const test = await atlas.referrals.test(); // { ok, http_status, latency_ms, error? }
const failed = await atlas.referrals.listCalls({ status: 'failed', limit: 20 });
await atlas.referrals.retryCall(failed.data[0]!.id);
const user = await atlas.users.get('user_123');
user.referral?.referrer_user_id;
await atlas.users.update('user_123', { referral_code: 'ada-1' }); // 409 REFERRAL_CODE_TAKEN if held
await atlas.users.flagReferral('user_123', { reason: 'self-referral' });
浏览器抓取 : browser_sources 列出浏览器读取的脚本或 cookie 的推荐程序( 更新时不包含返回提供者的默认, 由 browser_sources_default),以及 consent_provider 选择决定(auto一个经理,或 app) (中文(简体) ).
无法满足供应商的需求 422 REFERRALS_NOT_CONFIGURED。当平台回复延迟时,阿特拉斯发送 user.referral_attributed 网络hook (UserReferralAttributedEvent) (中文(简体) ). 飞行前可报告 referrals_enabled_unverified 财务报告和审定财务报表 referrals_without_bot_protection。 。 。 .
接收推荐协议( 移交平台)
执行Atlas查询协议的查询平台与 verifyReferralRequest每一次通话都是 POST 它的JSON身体上写着它是什么行动op: signup, (中文(简体) ), erase, (中文(简体) ), export, (中文(简体) ), partner 或者说 signal。通过呼叫到达的路径:一个签名的机构,其 op 拒绝匹配终点(op_mismatch) (中文(简体) ).
import { verifyReferralRequest } from '@atlasauth/backend';
app.post('/atlas/:endpoint', express.raw({ type: '*/*' }), async (req, res) => {
const { verified, call } = verifyReferralRequest({
body: req.body,
headers: req.headers,
secret: process.env.REFERRAL_SIGNING_SECRET!,
path: req.path, // or expectedOp: 'signup' on a handler for one endpoint
});
if (!verified || !call) return res.sendStatus(401);
switch (call.op) {
case 'signup': // ReferralSignupPayload → answer ReferralSignupResponse
return res.json({ referral: await match(call.payload), partner: await enroll(call.payload) });
case 'export': // the connection test sends test: true
return call.test ? res.sendStatus(404) : res.json(await exportFor(call.payload.customerRef));
default: // erase, partner, signal
await handle(call);
return res.sendStatus(204);
}
});
一个电话的重试会保留它 atlas-id (单位:千美元)deliveryId) (中文(简体) ). 报名是独家的 customerRef:回答重试的方式相同.
双向同步
提供商与 inbound 能力可以将注册后所做的转录更改发回Atlas。 设置报告 inbound.url (交给平台), inbound.secret_set, (中文(简体) ), inbound.endpoint_id 财务报告和审定财务报表 inbound.last_event (单位:千美元){ at, type, outcome }) (中文(简体) ). 将平台事件保密 inbound_secret (只写字,16至512个字符) inbound_endpoint_id数字 :
await atlas.referrals.update({ inbound_secret: process.env.REFERRAL_INBOUND_SECRET, inbound_endpoint_id: 'we_123' });
const { inbound } = await atlas.referrals.get(); // inbound.last_event?.outcome: 'applied', 'stale', …
Atlas查询协议上的一个平台 referral.updated 事件为 inbound.url,如Atlas Webhook 与输入密件(或连接在未设置密件时是签名密件)一样签名. signReferralUpdatedEvent 构建它:
import { signReferralUpdatedEvent } from '@atlasauth/backend';
const e = signReferralUpdatedEvent({
inboundUrl: settings.inbound.url,
secret: process.env.REFERRAL_SIGNING_SECRET!,
event: { customerRef: 'user_123', referralId: 'ref_1', status: 'qualified' },
});
await fetch(e.request());