Frame.io TypeScript SDK — 身份验证指南

本指南介绍如何使用 Frame.io TypeScript SDK (frameio) 通过 Frame.io API 进行身份验证。 Frame.io V4 API 使用 Adobe Identity Management Service (IMS),Adobe 的 OAuth 2.0 身份平台。 这是面向 TypeScript/JavaScript 开发者的独立参考。 下方的所有代码示例和流程仅适用于 frameio 包。


TypeScript SDK 中的身份验证类型

TypeScript SDK 支持四种 OAuth 身份验证类,以及直接使用令牌的方式:

方法用例是否需要用户交互?是否需要客户端密钥?
静态令牌快速脚本、测试,或者您已拥有令牌否否
服务器到服务器后端服务、定时任务、自动化否是
Web 应用程序服务器端应用程序(Express、Fastify、Next.js)是是
SPA (PKCE)无法存储密钥的浏览器应用程序是否
原生应用程序 (PKCE)使用自定义 URI 方案进行重定向的桌面/移动应用程序是否
服务器到服务器方式让您的应用程序以服务帐户身份运行,无需用户交互。此方式仅适用于通过 Adobe Admin Console 管理的 Frame.io V4 帐户。Web 应用程序和 SPA 方式让您的应用程序以特定用户的身份运行。两者底层都使用 Adobe IMS:用户授权您的应用程序后,SDK 会将生成的代码交换为令牌。TypeScript SDK 为您处理 IMS 的 /authorize/v2 和 /token/v3 流程。对于 Web 应用程序,您需要客户端密钥;对于 SPA,则改用 PKCE。原生应用程序遵循与 SPA 相同的 PKCE 流程,但使用 Adobe 为您的原生应用程序凭据分配的 adobe+<hash>://callback</hash> 重定向 URI。这让您的应用程序能够在授权完成后,于操作系统层面拦截该重定向。

服务帐户用户

使用服务器到服务器的身份验证时,您的应用程序充当服务帐户用户,这是一种可以代表服务执行操作的独特帐户类型。 这些内容在 Frame.io 中对其他用户可见:当服务帐户执行操作时,其名称会显示在 UI 中。 您可以通过 Adobe Admin Console 和 Developer Console 授予和撤销服务帐户访问权限。 服务帐户名称通过 Frame.io UI 进行管理。 默认情况下,您的第一个 S2S 连接名为 Service Account User,第二个名为 Service Account User 2,以此类推。


快速入门

前提条件

  1. Adobe Developer Console 提供的 凭据:
  • 客户端 ID — 所有 OAuth 流均需要 - 客户端密钥 — 服务器到服务器的流和 Web 应用程序流需要 - 重定向 URI — Web 应用程序、SPA 和原生应用程序流需要;必须注册在 Adobe 项目中
  1. 安装 SDK:
npm install frameio

选择方法

  • 无用户参与? 使用服务器到服务器 (ServerToServerAuth)。
  • 有用户参与且您可以存储密钥? 使用 Web 应用程序 (WebAppAuth)。
  • 有用户参与但您无法存储密钥? 对于浏览器应用程序,使用 SPA (SPAAuth);对于桌面/移动应用程序,使用 原生应用程序 (NativeAppAuth)。

访问令牌

如果您已有访问令牌(来自其他 OAuth 系统或之前的交换,例如通过我们的 API Explorer获得),那么您可以直接传递:

import { FrameioClient } from "frameio";
const client = new FrameioClient({ token: "YOUR_ACCESS_TOKEN" });

这是最简单的方法,但令牌最终会过期,SDK 不会为您刷新令牌。

旧版开发者令牌

对于尚未通过 Adobe Admin Console 进行管理的 V4 迁移帐户,您可以继续使用来自 Frame.io 开发者网站的旧版开发者令牌。 您必须包含 x-frameio-legacy-token-auth 标头并将其设置为 true:

import { FrameioClient } from "frameio";
const client = new FrameioClient({
token: "YOUR_LEGACY_DEVELOPER_TOKEN",
headers: { "x-frameio-legacy-token-auth": "true" },
});

旧版开发者令牌不会过期,但它们是一种过渡机制。 对于新的集成和生产工作负载,我们推荐使用下面的其中一个 OAuth 2.0 流。 有关详细信息,请参阅迁移指南。


服务器到服务器(客户端凭据)

将此用于需要 Frame.io 访问权限且无需用户交互的后端服务和脚本。 此流仅适用于通过 Adobe Admin Console 管理的 Frame.io V4 帐户。 您的应用程序以服务帐户用户的身份进行身份验证,无需人工干预。

import { FrameioClient, ServerToServerAuth } from "frameio";
const auth = new ServerToServerAuth({
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
});
const client = new FrameioClient({ token: () => auth.getToken() });

就是这样。 auth.getToken() 是 SDK 在每个请求中都会调用的异步函数。 如果当前令牌仍然有效,它就会立即返回。 如果即将过期,它会先获取一个新令牌,过程完全透明。

工作原理

您的客户端凭据(客户端 ID + 密钥)永不过期。 您只需出于安全维护目的手动轮换它们即可。 S2S 可为您提供实际上永久、不间断的 API 访问权限,无需手动干预。

底层原理:

  1. 对于第一次 API 调用,getToken() 使用 client_credentials 授权凭证从 Adobe IMS 请求新的访问令牌。
  2. 令牌会缓存在内存中。 单个访问令牌会过期(通常为 24 小时),但这会为您自动处理。
  3. 当缓存的令牌在刷新缓冲时间内时(默认:过期时间前 60 秒),SDK 会使用相同的客户端凭据自动获取新的令牌。
  4. 不涉及刷新令牌。 客户端凭据本身就是长期有效的密钥,始终可用于生成新的访问令牌。

显式身份验证

如果您想主动获取令牌(例如,在启动时因错误凭据而快速失败):

const auth = new ServerToServerAuth({ clientId: "...", clientSecret: "..." });
await auth.authenticate(); // throws AuthenticationError if credentials are invalid
const client = new FrameioClient({ token: () => auth.getToken() });

Web 应用程序(授权代码)

将此用于用户使用其 Adobe ID 登录的服务器端应用程序。 此流需要客户端密钥,必须安全地将其存储在您的服务器上。

1

将用户重定向到 Adobe IMS

import { WebAppAuth } from "frameio";
import crypto from "crypto";
const auth = new WebAppAuth({
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
redirectUri: "https://yourapp.com/callback",
});
// Generate a cryptographically random state value to prevent CSRF attacks
const state = crypto.randomBytes(32).toString("hex");
const authorizationUrl = auth.getAuthorizationUrl({ state });
// Store `state` in the user's session, then redirect them to `authorizationUrl`
2

处理回调

当 Adobe IMS 将用户重定向回您的 redirectUri 时,提取 code 和 state 参数。 验证状态是否与您存储的内容匹配,然后将代码交换为令牌:

// In your callback handler (e.g. an Express route):
await auth.exchangeCode(req.query.code as string);

这会将授权代码交换为访问令牌和刷新令牌,并将两者都存储在内部。

3

使用客户端

import { FrameioClient } from "frameio";
const client = new FrameioClient({ token: () => auth.getToken() });

就是这样。 从现在开始,getToken() 会自动管理令牌生命周期。 当访问令牌即将过期时,SDK 会使用刷新令牌获取新令牌。 无需用户交互。

完整的表达式示例

import crypto from "crypto";
import express from "express";
import session from "express-session";
import { FrameioClient, WebAppAuth } from "frameio";
const auth = new WebAppAuth({
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
redirectUri: "http://localhost:3000/callback",
});
const app = express();
app.use(session({ secret: crypto.randomBytes(32).toString("hex"), resave: false, saveUninitialized: false }));
app.get("/login", (req, res) => {
const state = crypto.randomBytes(32).toString("hex");
(req.session as any).oauthState = state;
res.redirect(auth.getAuthorizationUrl({ state }));
});
app.get("/callback", async (req, res) => {
if (req.query.state !== (req.session as any).oauthState) {
return res.status(403).send("Invalid state parameter");
}
await auth.exchangeCode(req.query.code as string);
const client = new FrameioClient({ token: () => auth.getToken() });
const accounts = await client.accounts.index();
res.json(accounts);
});
app.listen(3000);

单页面应用程序 / PKCE(授权代码 + PKCE)

将其用于无法安全存储客户端密钥的基于浏览器的应用程序、桌面应用程序或 CLI 工具。 此流使用 PKCE (RFC 7636) 来保护授权代码交换。

1

生成授权 URL

import { SPAAuth } from "frameio";
const auth = new SPAAuth({
clientId: "YOUR_CLIENT_ID",
redirectUri: "https://yourapp.com/callback",
});
const state = crypto.randomUUID();
const result = await auth.getAuthorizationUrl({ state });
// result.url -> redirect the user here
// result.codeVerifier -> store this securely until the callback

getAuthorizationUrl 返回一个 AuthorizationUrlResult,其中包含完整 URL(嵌入 PKCE code_challenge)以及您在下一步中需要的 codeVerifier。

2

使用验证器交换代码

当用户被重定向回来时:

await auth.exchangeCode({
code: "CODE_FROM_CALLBACK",
codeVerifier: result.codeVerifier,
});
3

使用客户端

import { FrameioClient } from "frameio";
const client = new FrameioClient({ token: () => auth.getToken() });

就是这样。 刷新操作与 Web 应用程序相同 — SDK 会自动使用刷新令牌。 区别在于,刷新期间不会发送客户端密钥,因为 SPA 流专为公共客户端而设计。

codeVerifier 必须安全地存储在授权请求和代码交换之间的客户端。 在浏览器应用程序中使用 sessionStorage 或等效方式。


原生应用程序(授权代码 + PKCE)

将其用于桌面和移动应用程序。 在 Adobe Developer Console 中创建原生应用程序凭据时,Adobe 会为您分配一个格式为 adobe+<hash>://callback</hash> 的重定向 URI——您需要在操作系统级别注册您的应用程序来处理该自定义 URI 方案。 本地开发也支持环回重定向 (http://127.0.0.1:<port>/callback</port>)。 该流与 SPA 相同 — 使用 PKCE 且没有客户端密钥。

import { NativeAppAuth } from "frameio";
const auth = new NativeAppAuth({
clientId: "YOUR_CLIENT_ID",
redirectUri: "adobe+abc123def456://callback", // from your Adobe Developer Console Native App credential
// Also supports loopback: "http://127.0.0.1:8080/callback"
});
const { url, codeVerifier } = await auth.getAuthorizationUrl({
state: crypto.randomUUID(),
});
// Open system browser to `url`
// Listen for redirect on your custom URI scheme or loopback server
await auth.exchangeCode({ code: "CODE_FROM_REDIRECT", codeVerifier });
const client = new FrameioClient({ token: () => auth.getToken() });

重定向 URI 规则

Adobe 在两个点执行重定向 URI 规则:在 Adobe Developer Console 中注册凭据时,以及当 redirect_uri 参数访问 /authorize/v2 端点时。 您在此 SDK 中传递给 redirectUri 的值必须与您在凭据上注册的其中一个“重定向 URI 模式”匹配,否则 Adobe 将重定向到凭据上的默认重定向 URI。

  • Web 应用程序 和 SPA 凭据需要 HTTPS。
  • 原生应用程序凭据使用非 HTTPS 重定向 — 通常是 Developer Console 中显示的凭据 adobe+<hash>://callback</hash> URI。

有关您的凭据接受的确切模式,请参阅 Adobe Developer Console。

Python SDK 不包含原生应用程序凭据类,因为 Python 没有用于注册自定义

URI 方案处理程序的标准方法。 TypeScript SDK 支持包括原生应用程序在内的所有四种凭据类型。


手动令牌刷新

对于 Web 应用程序、SPA 和原生应用程序流,SDK 通过 getToken() 自动刷新令牌。 如果您需要显式控制,可以直接调用 refresh():

await auth.refresh(); // fetches a new access token using the refresh token

当您希望在关键操作之前强制刷新而不是依赖自动刷新缓冲区时,这样做非常有用。

refresh() 可用于 WebAppAuth、SPAAuth 和 NativeAppAuth 上。 如果没有可用的刷新令牌,则会抛出 ConfigurationError(即您必须先调用 exchangeCode())。 ServerToServerAuth 没有 refresh() 方法 — 而是使用 authenticate() 通过客户端凭据获取新令牌。


令牌持久性

所有身份验证类都支持 exportTokens() 和 importTokens() 来在重新启动时持久化令牌状态。 对于 Web 应用程序、SPA 和原生应用程序流程,这尤其重要,因为访问令牌和刷新令牌默认保存在内存中 — 如果您的应用程序重新启动,用户需要重新进行身份验证,除非您对这些令牌进行持久化。 对于服务器到服务器,持久性是可选的(客户端凭据始终可以生成新令牌),但导入缓存的令牌可以避免启动时出现额外的往返。

导出和导入

// After exchangeCode(), save the token state
const tokenData = auth.exportTokens();
// tokenData is: { access_token: "...", refresh_token: "...", expires_at: 1234567890.0 }
// Save it to your database, file, or secret store
// On next startup, restore it
auth.importTokens(tokenData);
const client = new FrameioClient({ token: () => auth.getToken() });
// The SDK will automatically refresh if the token is near expiry

安全存储导出的令牌。 它们包含用于授权凭证 API 访问权限的访问令牌和刷新令牌。 避免将令牌写入

生产中的纯文本文件。

使用 onTokenRefreshed 自动持久化

要在每次刷新时自动持久化令牌,请使用 onTokenRefreshed 回调:

import fs from "fs/promises";
const TOKEN_FILE = "tokens.json";
const auth = new WebAppAuth({
clientId: "...",
clientSecret: "...",
redirectUri: "...",
onTokenRefreshed: (tokens) => {
fs.writeFile(TOKEN_FILE, JSON.stringify(tokens));
},
});
// On startup, restore if available
try {
const saved = JSON.parse(await fs.readFile(TOKEN_FILE, "utf-8"));
auth.importTokens(saved);
} catch {
// No saved tokens — user will need to authenticate
}

该回调接收与 exportTokens() 相同结构的数据,并在每次成功刷新令牌后触发。


撤销令牌

要注销用户并通过 Adobe IMS 使其令牌失效:

await auth.revoke();

这会向 Adobe IMS 发出两个尽力撤销请求(一个用于访问令牌,一个用于刷新令牌,两者并行),然后清除所有本地令牌状态。对于机密客户端 (WebAppAuth),撤销请求使用 HTTP Basic Auth;对于公共客户端(SPAAuth、NativeAppAuth),client_id 作为查询参数发送。 撤销错误会被记录但不会被抛出。 撤销后,用户将需要重新进行身份验证。


错误处理

所有身份验证错误都继承自 FrameioAuthError,因此您可以广泛捕获它们或处理特定用例:

import {
FrameioAuthError,
AuthenticationError,
TokenExpiredError,
ConfigurationError,
NetworkError,
RateLimitError,
} from "frameio";
try {
await auth.exchangeCode("...");
} catch (error) {
if (error instanceof TokenExpiredError) {
// The refresh token has expired; redirect the user to sign in again
} else if (error instanceof AuthenticationError) {
// Token exchange failed
console.error(`Error: ${error.errorCode} - ${error.errorDescription}`);
} else if (error instanceof NetworkError) {
// Timeout or connection failure (after retries)
} else if (error instanceof RateLimitError) {
// 429 from Adobe IMS; retry after error.retryAfter seconds
} else if (error instanceof FrameioAuthError) {
// Catch-all for any other auth error
}
}

错误参考

例外引发的时间
ConfigurationError配置缺失或无效(例如 clientId 为空、非 HTTPS 重定向 URI、非 HTTPS imsBaseUrl)
AuthenticationErrorAdobe IMS 拒绝令牌交换或刷新(具有 .errorCode 和 .errorDescription)
TokenExpiredError刷新令牌本身已过期;用户必须重新进行身份验证
NetworkErrorHTTP 超时或完成所有重试后仍出现连接故障
RateLimitErrorAdobe IMS 返回 429;请查看 .retryAfter 获取退避指导
PKCEError可供消费者在 PKCE 流中使用;不会由 SDK 内部抛出

在生产中处理过期的刷新令牌

对于 Web 应用程序、SPA 和原生应用程序流,刷新令牌最终会过期。当这种情况发生时,getToken() 会引发 TokenExpiredError。 您应该捕获这个错误,并再次将用户重定向到授权流。

import { TokenExpiredError } from "frameio";
try {
const client = new FrameioClient({ token: () => auth.getToken() });
const assets = await client.files.list({ projectId: "..." });
} catch (error) {
if (error instanceof TokenExpiredError) {
// Clear persisted tokens and redirect user to login
await auth.revoke();
return res.redirect("/login");
}
}

配置参考

这些参数具有合理的默认值,很少需要设置。 如果您确实需要自定义行为(指向暂存 IMS、注入自定义 fetch、微调超时或连接日志记录器),请在构造授权类时将其中任何一个作为可选参数传递:

参数默认描述
scopes特定于流的默认值以空格分隔的 OAuth 权限范围。 S2S 默认为 openid AdobeID frame.s2s.all;面向用户的流默认为 openid email profile offline_access additional_info.roles。
imsBaseUrlhttps://ims-na1.adobelogin.comAdobe IMS 基础 URL。 针对暂存或非生产环境的覆盖设置。 必须使用 HTTPS。
fetchglobalThis.fetch用于代理、mTLS 或自定义 HTTP 处理的自定义 fetch 实施。
timeout30000令牌端点调用的 HTTP 请求超时时间(以毫秒为单位)。
maxRetries2瞬态故障(5xx、超时)的最大重试次数。 速率限制重试 (429) 单独跟踪。
refreshBuffer60令牌过期时间前触发主动刷新的秒数。
onTokenRefreshedundefined每次成功刷新令牌后触发的回调。 接收包含 access_token、refresh_token 和 expires_at 的对象。
logger无操作(静默)采用 debug、info、warn、error 方法的记录器实例(例如 console、pino、winston)。

暂存环境

通过重写 imsBaseUrl 指向暂存 Adobe IMS 实例。 如果您需要以编程方式引用生产值,SDK 还可以导出 DEFAULT_IMS_BASE_URL (https://ims-na1.adobelogin.com)。

const auth = new ServerToServerAuth({
clientId: "...",
clientSecret: "...",
imsBaseUrl: "https://ims-na1-stg1.adobelogin.com",
});

自定义获取

对于代理支持或自定义 TLS 配置:

import { ProxyAgent } from "undici";
const proxyAgent = new ProxyAgent("http://corporate-proxy:8080");
const auth = new ServerToServerAuth({
clientId: "...",
clientSecret: "...",
fetch: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
});

并发安全性

TypeScript SDK 可安全用于并发使用。 当多个 getToken() 调用同时发生且需要刷新时,只会触发一个刷新请求。 其他调用会等待相同的 promise,最终接收相同的结果。 无需外部锁定。 这种去重技术利用 JavaScript 的单线程事件循环和共享的 Promise — 如果刷新已在进行中,并发调用者会加入其中,而不是启动第二个请求。 如果在刷新操作期间调用 revoke(),刷新会因 AuthenticationError 被拒绝,同时令牌也会保持清除状态,撤销始终优先。