Backstage 自定义认证 Provider 模块开发指南:基于 auth-backend 扩展新认证方式
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本文是一份面向 Backstage 贡献者与平台开发者的技术指南,讲解如何为auth-backend添加全新的外部认证 Provider(如自建 OAuth 服务、企业内部 SSO 或反向代理认证)。文章以官方文档 docs/auth/add-auth-provider.md 为核心骨架,结合当前仓库中auth-backend、plugin-auth-node与各内置认证模块的真实源码,深入剖析认证流程、核心接口与完整落地步骤。读完本文,你将掌握AuthProviderRouteHandlers接口模型、OAuthEnvironmentHandler多环境机制、Passport 策略封装方式,以及从yarn new创建模块到接入后端、调试验证的全流程实战能力。
认证(Authentication)是如何工作的
Backstage 应用本身不直接持有用户凭证,而是通过接入各种外部认证提供者来完成身份验证。在auth-backend中,每个外部 Provider 都被包装成一个实现了AuthProviderRouteHandlers接口的对象。该接口(定义于 plugins/auth-node/src/types.ts)由四个方法构成,每个方法默认挂载在/api/auth/[provider]/method形式的端点上:
/auth/[provider]/start -> 从网页发起一次登录 /auth/[provider]/handler/frame -> 处理一次已完成的外部认证操作(回调) /auth/[provider]/refresh -> 刷新一次登录的有效性 /auth/[provider]/logout -> 登出已登录的用户其中refresh与logout是可选方法(接口中声明为refresh?、logout?),只有 Provider 支持时才被挂载。整个登录流程如下:
- 用户尝试登录;
- 前端打开一个弹出窗口(popup),指向
auth端点。该端点先完成一些初始准备(如写入 nonce cookie、拼接 state 参数),然后在弹窗内将用户重定向到外部认证方; - 外部认证方验证用户身份,并把验证结果(成功或失败)返回给包装器的
handler/frame端点; handler/frame渲染出的网页向打开弹窗的父页面发出适当的响应,随后弹窗关闭;- 用户点击界面上的登出入口,网页向
logout端点发出请求完成登出。
这段流程在源码中有着完整的落点:bindProviderRouters函数(见 plugins/auth-backend/src/providers/router.ts)会为每个已注册 Provider 逐一创建子路由并绑定方法:
r.get('/start', provider.start.bind(provider)); r.get('/handler/frame', provider.frameHandler.bind(provider)); r.post('/handler/frame', provider.frameHandler.bind(provider)); if (provider.logout) { r.post('/logout', provider.logout.bind(provider)); } if (provider.refresh) { r.get('/refresh', provider.refresh.bind(provider)); r.post('/refresh', provider.refresh.bind(provider)); } targetRouter.use(`/${providerId}`, r);可以确认:start只暴露 GET,handler/frame同时支持 GET 与 POST,logout为 POST,refresh在支持时同时暴露 GET 与 POST。若配置缺失,bindProviderRouters还会注册一个抛出NotFoundError的兜底路由,提示auth.providers.<providerId>配置缺失或环境变量未定义(见 plugins/auth-backend/src/providers/router.ts)。
核心接口:AuthProviderRouteHandlers
任何认证包装器都必须实现AuthProviderRouteHandlers接口(plugins/auth-node/src/types.ts)。接口对四个方法做了明确约定:
start(req, res):处理start路由,发起签名请求。请求可携带可选scopes;响应为重定向到外部认证方,同时写入 nonce cookie 并把 nonce 作为state查询参数带在重定向 URL 中;frameHandler(req, res):外部认证方完成登录或授权后重定向到callbackURL,由该方法处理。请求需携带 nonce cookie 与state参数;响应通过postMessage向父窗口发送包含accessToken、expiresInSeconds、idToken、scope等信息的载荷,若 Provider 支持刷新令牌还会设置 refresh token cookie;refresh?(req, res):可选。用于在持有 refresh token cookie 时换取新的访问令牌,也可被代理类 Provider 用于按需创建新会话;logout?(req, res):可选。处理登出请求,移除 refresh token cookie。
登录发起与回调:start 与 frameHandler
发起登录时,前端会打开一个弹窗,将登录请求发往由start方法处理的/start端点。start将用户重定向到外部认证方,认证方验证后把请求重定向回/handler/frame端点,由frameHandler方法接手。
frameHandler返回一个 HTML 响应,其中包含一段脚本,通过postMessage把请求结果发送给前端窗口。这条消息的类型就是WebMessageResponse(定义于 plugins/auth-node/src/flow/sendWebMessageResponse.ts):
export type WebMessageResponse = | { type: 'authorization_response'; response: ClientAuthResponse<unknown>; } | { type: 'authorization_response'; error: Error; };注意:官方文档中提到的postMessageResponse工具函数,在当前仓库中的实际实现名为sendWebMessageResponse。它封装了生成postMessage响应的全部逻辑,负责正确处理 CORS,接收express.Response、WebMessageResponse以及前端地址(appOrigin),最终返回内嵌脚本与消息的 HTML 页面。实现中有两个值得关注的细节(见 plugins/auth-node/src/flow/sendWebMessageResponse.ts):
- 数据会经过
safelyEncodeURIComponent编码(除常规编码外还会把'替换为%27),防止注入恶意脚本; - 由于
postMessage在 targetOrigin 被拒绝时会静默失败,脚本会先以'*'为 targetOrigin 发送一条config_info类型消息告知目标 origin,再以appOrigin为 targetOrigin 发送真正的授权响应。若父窗口收到第一条消息却始终等不到第二条,即可判定 targetOrigin 被拒绝,属于配置问题。
最终响应会带上X-Frame-Options: sameorigin与基于 SHA-256 哈希的 CSPscript-src指令,以缓解跨站风险。
认证环境(env)隔离
env概念是 auth-backend 工作方式的核心。它通过env查询参数标识应用运行的环境(development、staging、production等),同一运行时可以同时服务多个环境,并根据请求中的env参数分派到对应的处理器。
OAuthEnvironmentHandler(plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts)是OAuthHandlers的实用包装器:它实现AuthProviderRouteHandlers接口,同时支持多个env。从源码看,getEnvFromRequest会先从req.query.env读取环境,取不到时再尝试从state参数解码出的 OAuth 状态中提取(decodeOAuthState),最后在getProviderForEnv中按环境查找处理器,环境缺失或未配置会分别抛出InputError与NotFoundError。
要实例化同一 Provider 在不同环境下的多个实例,请使用OAuthEnvironmentHandler.mapConfig。它遍历"环境名 → 配置"的配置对象,把每个环境的配置块分别交给工厂函数。给定如下配置:
development: clientId: abc clientSecret: secret production: clientId: xyz clientSecret: supersecretOAuthEnvironmentHandler.mapConfig(config, envConfig => ...)会按顶层development与production键拆分配置,把每一块作为envConfig传入回调。源码实现(plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts)即遍历config.keys()并为每个环境调用一次factoryFunc。
AuthProviderFactory则是需要实现的工厂函数,为给定 Provider 生成AuthProviderRouteHandlers。当前仓库中所有受支持的 Provider 都提供了一个返回OAuthEnvironmentHandler的AuthProviderFactory,从而能同时处理多个环境的认证。
为什么选择 Passport
Backstage 选用了 Passport 作为认证平台,原因是它拥有覆盖面极广的认证策略(strategy)生态。在实现自定义 Provider 时,可以直接复用 Passport 社区现有的策略包,再通过PassportOAuthAuthenticatorHelper将其无缝接入 Backstage 的认证框架,大幅降低实现成本。
如何添加一个新的策略 Provider
快速指南
- 根据需求,新建一个认证 Provider 模块(OAuth 类型),或创建一个基于代理(proxy)认证的 Provider;
- 把新模块接入后端(
packages/backend/src/index.ts)。
创建新的认证 Provider 模块
以虚构的服务foobar为例。使用yarn new创建新模块:选择backend-module模板,插件 ID 填auth-backend,模块 ID 填foobar-provider。确保模块把对应的 passport provider 声明为依赖:
cd plugins/auth-backend-backend-module-foobar-provider yarn add passport-provider-a yarn add @types/passport-provider-a实现 OAuth 类型的 Provider
定义后端模块
新模块通过authProvidersExtensionPoint扩展 auth-backend。扩展点接口定义于 plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts,其registerProvider接收{ providerId, factory };而auth-backend侧(plugins/auth-backend/src/authPlugin.ts)注册该扩展点时会对重复的providerId抛出错误,保证 Provider 标识唯一。
import { createBackendModule } from '@backstage/backend-plugin-api'; import { authProvidersExtensionPoint, commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; import { providerAuthenticator } from './authenticator'; /** @public */ export const authModuleFoobarProvider = createBackendModule({ pluginId: 'auth', moduleId: 'foobar', register(reg) { reg.registerInit({ deps: { providers: authProvidersExtensionPoint, }, async init({ providers }) { providers.registerProvider({ providerId: 'foobar', factory: createOAuthProviderFactory({ authenticator: providerAuthenticator, signInResolverFactories: { ...commonSignInResolvers, }, }), }); }, }); }, });createOAuthProviderFactory会基于 authenticator 生成一个标准的AuthProviderFactory,并自动完成多环境包装;commonSignInResolvers提供了通用的登录解析器(如emailMatchingUserEntityAnnotation、emailLocalPartMatchingUserEntityName等,见 plugins/auth-node/src/sign-in/commonSignInResolvers.ts),用于把外部身份映射为 Backstage 目录中的用户。
实现 authenticator
authenticator 负责基于 Passport 策略创建策略实例,并利用配置文件中的密钥(clientId、clientSecret等)驱动认证流程。它通过createOAuthAuthenticator创建,并借助PassportOAuthAuthenticatorHelper复用通用的 start / authenticate / refresh 逻辑(该 Helper 的from、defaultProfileTransform、start等实现见 plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts)。
import { Strategy as ProviderStrategy } from 'passport-provider-a'; import { createOAuthAuthenticator, PassportOAuthAuthenticatorHelper, PassportOAuthDoneCallback, PassportProfile, } from '@backstage/plugin-auth-node'; /** @public */ export const providerAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, scopes: { // Scopes required by the provider required: ['openid', 'email', 'profile', 'offline_access'], }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); return PassportOAuthAuthenticatorHelper.from( new ProviderStrategy( { clientID: clientId, clientSecret: clientSecret, // ... other options }, ( accessToken: string, refreshToken: string, params: any, fullProfile: PassportProfile, done: PassportOAuthDoneCallback, ) => { done( undefined, { fullProfile, params, accessToken }, { refreshToken }, ); }, ), ); }, async start(input, helper) { return helper.start(input); }, async authenticate(input, helper) { return helper.authenticate(input); }, async refresh(input, helper) { return helper.refresh(input); }, });scopes.required声明了该 Provider 必需的权限范围,PassportOAuthAuthenticatorHelper.defaultProfileTransform负责把 Passport 的全量 profile 转换为前端展示用的精简ProfileInfo。
仓库中的真实实现示例
当前仓库中已有多个同类实现可直接对照参考:
- Google:
plugins/auth-backend-module-google-provider/src/authenticator.ts; - GitHub:
plugins/auth-backend-module-github-provider/src/authenticator.ts,其模块注册代码见plugins/auth-backend-module-github-provider/src/module.ts; - Okta:
plugins/auth-backend-module-okta-provider/src/authenticator.ts。
以 GitHub 为例,其 authenticator(plugins/auth-backend-module-github-provider/src/authenticator.ts)除了读取clientId/clientSecret,还会读取可选的enterpriseInstanceUrl配置,并据此动态拼接authorizationUrl、tokenUrl、userProfileUrl——这展示了一个真实 Provider 如何在initialize中读取更多配置项、扩展策略选项。其模块(plugins/auth-backend-module-github-provider/src/module.ts)在注册时还额外合并了githubSignInResolvers与commonSignInResolvers。
创建基于代理(Proxy)认证的 Provider
代理认证 Provider 是指借用另一个外部认证方完成身份验证的 Provider,例如 Google IAP(Identity-Aware Proxy)或 AWS ALB。仓库中已内置支持这两者(plugins/auth-backend-module-gcp-iap-provider与plugins/auth-backend-module-aws-alb-provider)。
其实现方式与 OAuth Provider 大体一致,区别在于authenticator函数不同:代理 Provider 不再走"重定向到外部 OAuth 端点"的流程,而是由反向代理或网关注入身份头信息,authenticator 负责从请求中解析并校验这些信息。需要新实现时,可直接参照上述两个内置模块。
Verify Callback
策略需要一个所谓的 verify callback。verify callback 的目的是找出持有某组凭证的用户。当 Passport 认证一个请求时,它会解析请求中包含的凭证,然后以这些凭证为参数调用 verify callback……如果凭证有效,verify callback 调用
done向 Passport 提供完成认证的用户。如果凭证无效(例如密码错误),应调用
done并传入false而非用户对象,以表示认证失败。——引自 Passport 官方配置文档
把新 Provider 接入后端
添加新模块的方式与任何其他模块或后端插件相同。在packages/backend/src/index.ts中:
若该 Provider 仅用于内部安装,则用内部导入路径:
backend.add(import('@internal/plugin-auth-backend-module-foobar-provider'));若模块已直接贡献给 Backstage,则用官方命名空间:
backend.add(import('@backstage/plugin-auth-backend-module-foobar-provider'));完成注册后,auth-backend会自动挂载以下端点(对应 plugins/auth-backend/src/providers/router.ts 中的绑定逻辑):
router.get('/auth/providerA/start'); router.get('/auth/providerA/handler/frame'); router.post('/auth/providerA/handler/frame'); router.post('/auth/providerA/logout'); router.get('/auth/providerA/refresh'); // if supported router.post('/auth/providerA/refresh'); // if supported可以看到每个端点都以/auth和 Provider 名称为前缀。
配置示例
在app-config.yaml的auth.providers下按环境提供配置即可启用(参考仓库根目录 app-config.yaml 中的内置 Provider 写法):
auth: environment: development providers: foobar: development: clientId: ${AUTH_FOOBAR_CLIENT_ID} clientSecret: ${AUTH_FOOBAR_CLIENT_SECRET}结合前文OAuthEnvironmentHandler.mapConfig的机制,auth.providers.foobar下的每个环境键(development、production等)都会生成一个独立的处理器,而auth.environment决定当前运行时默认使用哪个环境。
测试新 Provider
模块接入后,可以用 curl 发起一次登录来验证流程是否正常:
curl -i localhost:7007/api/auth/providerA/start预期会返回302重定向,并带有Location头。把该头中的 URL 粘贴到浏览器中打开,即可触发完整的授权流程——浏览器中会看到跳转到外部认证方、授权后回调并关闭弹窗的完整链路。若返回的是 404 或配置错误提示,可检查auth.providers.<providerId>配置与环境变量是否就绪,并查看后端启动日志中bindProviderRouters打印的Configuring auth provider: <providerId>记录。
小结
为 Backstage 添加新的认证 Provider,本质上是三件事:一是实现AuthProviderRouteHandlers接口(或更常用地,通过createOAuthProviderFactory+createOAuthAuthenticator组合出工厂);二是借助OAuthEnvironmentHandler.mapConfig让同一 Provider 优雅地支持多环境;三是通过authProvidersExtensionPoint把模块注册进auth-backend。无论是接入 Passport 社区已有的 OAuth 策略,还是实现基于 IAP / ALB 的代理认证,上述路径都完全一致,仓库中 Google、GitHub、Okta、GCP IAP、AWS ALB 等内置模块即是可复刻的最佳范本。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考