Backstage 接入 Google OAuth 登录:Provider 配置、Sign-in Resolver 与源码级实现解析
本文基于 Backstage 仓库中的 Google 认证 Provider 文档,完整讲解如何为 Backstage 接入 Google OAuth 登录:从 Google Cloud Console 创建 OAuth 凭据,到在 app-config.yaml 中配置 provider、选择 sign-in resolver,再到后端模块安装与前端登录页接入。文章同时结合 @backstage/plugin-auth-backend-module-google-provider 插件的真实源码(authenticator.ts、resolvers.ts、module.ts)与单元测试(module.test.ts),帮助你理解 Google 认证在 Backstage 中的底层工作原理,并能在自己的开发环境中一步不落地跑通。
概述:Google Provider 在 Backstage 中的位置
Backstage 的 core-plugin-api 包自带 Google 认证 Provider,可让用户通过 Google OAuth 完成登录认证。在仓库中,它的实现位于 @backstage/plugin-auth-backend-module-google-provider 插件目录下(plugins/auth-backend-module-google-provider),该模块面向 Backstage 新后端系统(New Backend System),通过 createBackendModule 向 auth 插件注册名为 google 的 provider。
从源码结构看,Google Provider 的实现由三部分构成(见 index.ts):
googleAuthenticator:基于passport-google-oauth20策略的 OAuth 认证器,负责与 Google 交互;googleSignInResolvers:Google 专属的 sign-in resolver 工厂;authModuleGoogleProvider(默认导出):后端模块,把上述两者装配到 auth 插件。
整个认证流程遵循 Backstage 通用的 OAuth 离线访问流程(详见 OAuth 与 OIDC 文档):前端弹窗指向 auth 后端 /start 端点,跳转到 Google 的 OAuth 授权页面,用户同意后回调 /handler/frame 端点完成令牌交换。该流程基于服务端离线访问实现,不依赖第三方 Cookie,在严格隐私设置的浏览器上也能稳定工作。
第一步:在 Google Cloud Console 创建 OAuth 凭据
要在 Backstage 中启用 Google 认证,首先需要创建 OAuth 客户端凭据。请按以下步骤操作:
- 登录 Google Cloud Console(
https://console.cloud.google.com); - 从顶栏下拉菜单中选择或新建一个项目;
- 导航到 APIs & Services > Credentials(API 与服务 > 凭据);
- 点击 Create Credentials(创建凭据),选择
OAuth client ID(OAuth 客户端 ID); - 如有需要,先配置 OAuth consent screen(OAuth 同意屏幕):
- 本地开发环境下,不需要填写任何 Authorized domain(已获授权的域名);
- Scopes(范围)选择
openid、auth/userinfo.email和auth/userinfo.profile。这与源码中 authenticator.ts 声明的requiredscopes 完全一致:scopes: { required: [ 'openid', `https://www.googleapis.com/auth/userinfo.email`, `https://www.googleapis.com/auth/userinfo.profile`, ], }, - 如果用户类型选择 External(外部),需要把自己添加为测试用户(Test user),否则 OAuth 授权将被拒绝;
- 将 Application Type(应用类型)设为
Web Application,并填写以下设置:Name(名称):Backstage(或你自己的应用名);Authorized JavaScript origins(已获授权的 JavaScript 来源):http://localhost:3000;Authorized Redirect URIs(已获授权的重定向 URI):http://localhost:7007/api/auth/google/handler/frame;
- 点击 Create(创建),生成
clientId与clientSecret。
这里重定向 URI 的路径 /api/auth/google/handler/frame 正是 module.test.ts 中断言的回调地址格式:测试验证启动后的授权跳转 redirect_uri 为 http://localhost:${server.port()}/api/auth/google/handler/frame。该地址由 auth 后端依据 app.baseUrl 动态拼接,因此生产环境部署时需把这里的两个 localhost 地址替换为实际域名。
第二步:在 app-config.yaml 中配置 Google Provider
创建好凭据后,在 app-config.yaml 的根级 auth 配置下添加 provider 配置:
auth:
environment: development
providers:
google:
development:
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
## uncomment to set lifespan of user session
# sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code
signIn:
resolvers:
# See https://backstage.io/docs/auth/google/provider#resolvers for more resolvers
- resolver: emailMatchingUserEntityAnnotation
auth.providers.google 是典型的「环境键 -> provider 配置」结构,这里的 development 键对应 auth.environment 的值。每个 provider 配置包含两个核心字段:
clientId:上一步生成的客户端 ID,形如10023341500512-beui241gjwwkrdkr2eh7dprewj2pp1q.apps.googleusercontent.com;clientSecret:与该客户端 ID 关联的客户端密钥。
可选配置项
sessionDuration:用户会话的生命周期。支持ms库的时间格式(如'24h'、'2 days')、ISO 8601 时长格式以及{ hours: 24 }这类人类可读的 HumanDuration 对象。
从仓库中的配置类型定义 config.d.ts 可以看到,该 provider 实际支持的配置键还包括:
| 配置键 | 类型 | 说明 |
|---|---|---|
clientId | string | OAuth 客户端 ID(前端可见,@visibility frontend) |
clientSecret | string | 客户端密钥(@visibility secret,禁止暴露到前端) |
callbackUrl | string(可选) | 自定义回调地址,缺省时由 auth 后端根据 baseUrl 推导 |
additionalScopes | string \| string[](可选) | 在必需 scope 之外追加的 OAuth scope |
signIn.resolvers | 数组 | sign-in resolver 列表,详见下一节 |
sessionDuration | HumanDuration \| string(可选) | 会话生命周期 |
其中 clientSecret 标记为 @visibility secret,意味着 Backstage 的配置系统会阻止它被下发到前端,这对安全至关重要。如需额外的 Google API 权限,可通过 additionalScopes 追加,例如 https://www.googleapis.com/auth/cloud-platform。
第三步:理解并选择 Sign-in Resolvers
Backstage 的 auth provider 默认只用于「访问委托」(access delegation),即代表用户向后端外部系统请求资源。要让 Google 成为真正的登录方式,必须显式配置 signIn.resolvers,告诉 Backstage 如何把 Google 身份映射为 Backstage 用户身份。相关背景可参阅 Sign-in Identities and Resolvers。
Google Provider 内置的三个 Resolver
Google Provider 开箱即用地提供以下 resolver:
emailMatchingUserEntityProfileEmail:将 Google 账户的 email 与 Catalog 中spec.profile.email匹配的 User 实体关联。找不到匹配项时抛出NotFoundError;emailLocalPartMatchingUserEntityName:将 Google 账户 email 的本地部分(local part,即@之前的部分)与name匹配的 User 实体关联。找不到匹配项时抛出NotFoundError;emailMatchingUserEntityAnnotation:将 Google 账户 email 与google.com/email注解值匹配的 User 实体关联。找不到匹配项时抛出NotFoundError。
注意:resolvers 会按配置顺序依次尝试,但只有抛出
NotFoundError时才会被跳过(继续尝试下一个),其他错误会直接中断登录。
仓库中 resolvers.ts 的源码展示了 Google 专属 resolver 的实现方式,例如 emailMatchingUserEntityAnnotation:
export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info, ctx) => {
const { profile } = info;
if (!profile.email) {
throw new Error('Google profile contained no email');
}
return ctx.signInWithCatalogUser(
{
annotations: {
'google.com/email': profile.email,
},
},
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: profile.email } }
: undefined,
},
);
};
},
});
这里有两个关键实现事实:
- 如果 Google 返回的 profile 没有 email,resolver 会直接抛错拒绝登录;
- resolver 支持
dangerouslyAllowSignInWithoutUserInCatalog选项。开启后,当 Catalog 中找不到对应的 User 实体时,仍会基于profile.email生成一个后备身份并签发令牌,而不抛出NotFoundError。
在 module.ts 中,注册 provider 时会把 googleSignInResolvers 与所有 provider 通用的 commonSignInResolvers 合并注册:
providers.registerProvider({
providerId: 'google',
factory: createOAuthProviderFactory({
authenticator: googleAuthenticator,
signInResolverFactories: {
...googleSignInResolvers,
...commonSignInResolvers,
},
}),
});
这也印证了原文档:emailMatchingUserEntityProfileEmail 与 emailLocalPartMatchingUserEntityName 是所有 provider 通用的 resolver,而 emailMatchingUserEntityAnnotation 是 Google 专属。配置类型 config.d.ts 中仅允许这三个 resolver 名称,其中 emailLocalPartMatchingUserEntityName 额外支持 allowedDomains(允许登录的域名白名单)。
Resolver 使用建议
emailLocalPartMatchingUserEntityName由于只匹配 email 的本地部分,强烈建议同时配置allowedDomains限制可登录的域名,例如:signIn: resolvers: - resolver: emailLocalPartMatchingUserEntityName allowedDomains: - acme.org- 为降低账户劫持风险,通常只为其中一个 auth provider 配置 sign-in resolver,避免同一用户通过多套身份进入系统后映射到不同身份。
使用自定义 Resolver
如果内置 resolver 不满足需求,可以编写完全自定义的 sign-in resolver。完整方案见 Sign-in Identities and Resolvers,核心步骤是:
- 从
app-config.yaml的 provider 配置中移除signIn.resolvers(否则配置优先); - 在
packages/backend/src/index.ts中,用createBackendModule构造自定义 provider 模块,通过createOAuthProviderFactory传入googleAuthenticator和自定义的signInResolver回调,替换原来的backend.add(import('@backstage/plugin-auth-backend-module-google-provider'))。
例如,在 resolver 回调中可以用 ctx.signInWithCatalogUser({ entityRef: { name } }) 查找 Catalog 用户并签发令牌;也可以直接调用 ctx.issueToken 跳过 Catalog 校验(此时务必自行做域名白名单等校验,否则存在安全风险)。Google 的 googleAuthenticator 正是 createOAuthProviderFactory 期望的 OAuth 认证器形态,二者可无缝组合。
第四步:后端安装 Google Provider 模块
Google Provider 以独立包形式发布。在 Backstage 根目录执行:
# 在你的 Backstage 根目录执行
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-google-provider
然后在后端入口文件 packages/backend/src/index.ts 中注册该模块:
// packages/backend/src/index.ts
backend.add(import('@backstage/plugin-auth-backend'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend-module-google-provider'));
/* highlight-add-end */
对照仓库中 packages/backend/src/index.ts 的真实写法,auth 相关模块正是以这种 backend.add(...) 的形式逐条注册的(如 @backstage/plugin-auth-backend、GitHub provider、guest provider、openshift provider 等)。@backstage/plugin-auth-backend-module-google-provider 的默认导出即 module.ts 中创建的 authModuleGoogleProvider 后端模块,注册后 auth 插件会自动识别 auth.providers.google 配置并启用该 provider。
仓库中的测试 module.test.ts 给出了该模块的完整验证流程,可以作为集成验证的参考:它用 startTestBackend 启动一个包含 auth 后端与 Google provider 的测试实例,然后请求 /api/auth/google/start?env=development,断言:
- 返回
302重定向到https://accounts.google.com/o/oauth2/v2/auth; - 授权 URL 携带
access_type=offline、prompt=consent、include_granted_scopes=true、response_type=code等参数(这些参数由 authenticator.ts 的start方法注入); - scope 为
openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile; - 设置了
google-nonceCookie,回调地址为/api/auth/google/handler/frame。
这意味着:只要你的 app-config.yaml 中 clientId/clientSecret 配置正确,auth 后端启动后访问 /api/auth/google/start 即可触发完整的 Google 授权跳转。
第五步:将 Provider 添加到 Backstage 前端登录页
后端配置完成后,还需要在前端接入登录页。使用 @backstage/core-plugin-api 提供的 googleAuthApiRef 引用和 @backstage/core-components 提供的 SignInPage 组件即可,具体做法参见 登录配置说明。
在新前端系统(New Frontend System)下,通过 SignInPageBlueprint 创建自定义登录页并注册为 app 插件的扩展,示例(文档原文):
// packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
import { SignInPage } from '@backstage/core-components';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
const signInPage = SignInPageBlueprint.make({
params: {
loader: async () => props =>
(
<SignInPage
{...props}
provider={{
id: 'google-auth-provider',
title: 'Google',
message: 'Sign in using Google',
apiRef: googleAuthApiRef,
}}
/>
),
},
});
export default createApp({
features: [
/* ...其他插件... */
createFrontendModule({
pluginId: 'app',
extensions: [signInPage],
}),
],
});
SignInPage 会在应用其他路由渲染之前呈现,负责提供当前用户身份;用户完成 Google 授权后,通过 onSignInSuccess 回调获得有效的 Backstage 用户身份,其余应用界面才会渲染。
几点补充:
- 如果希望同时提供多种登录方式(例如开发环境允许 guest 登录),可将
provider换成providers数组(多 Provider 示例); - 可以通过读取
auth.environment配置,在开发环境渲染 Google 登录、生产环境使用代理登录页(如ProxiedSignInPage provider="gcpiap"),实现按环境条件渲染登录方式(条件渲染示例); - 如果希望采用「重定向」而非「弹窗」的登录流程,可在
app-config.yaml根级开启enableExperimentalRedirectFlow: true(文档说明)。
googleAuthApiRef 属于 Backstage 的 Utility API(OAuthApi & ProfileInfoApi & SessionApi 形态),前端插件可通过它按需请求 Google OAuth Access Token,用于调用 Google 外部服务,这也是 Backstage user-to-server OAuth 设计的一部分(相关背景)。
常见问题排查
错误一:Provider 未配置为支持登录
界面提示 "The 'Google' provider is not configured to support sign-in"。可能原因与解决方法:
signIn.resolvers未添加到 Google Provider 配置中——补上即可;app-config.yaml存在语法错误——运行yarn backstage-cli config:check --strict可帮助定位。
错误二:无法解析用户身份
界面提示 "Failed to sign-in, unable to resolve user identity"。原因通常是所配置的 resolver 无法在 Catalog 中找到匹配的 User 实体。解决办法是从组织的可信数据源导入 User / Group 数据,例如通过现有的组织数据 Provider(如 Microsoft Entra ID(Azure AD/MS Graph)、GitHub Org、GitLab 等),或编写自定义 Entity Provider 注入用户数据。
安全提醒:请谨慎配置 sign-in resolver,它直接决定了谁能登录你的 Backstage 实例、以什么身份登录。除有意支持多种登录方式外,请只为其中一个 auth provider 配置 sign-in resolver;开启
dangerouslyAllowSignInWithoutUserInCatalog会绕过 Catalog 校验并可能把权限授予未纳入管理的用户,生产环境务必慎用。
小结
接入 Google OAuth 登录需要依次完成四件事:在 Google Cloud Console 创建 OAuth 客户端(回调指向 /api/auth/google/handler/frame)、在 app-config.yaml 配置 auth.providers.google 并选择 sign-in resolver、安装并注册 @backstage/plugin-auth-backend-module-google-provider 后端模块、在前端通过 googleAuthApiRef 与 SignInPage 接入登录页。仓库源码(authenticator.ts、resolvers.ts、module.ts、config.d.ts)与测试(module.test.ts)完整印证了文档中的每一步配置,可作为你集成与排障时的第一手依据。若需自定义登录身份映射,可进一步参考 Sign-in Identities and Resolvers 与 Auth 总览。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



