@1meeting/auth-client-sdk
v0.2.0
Published
HighGood 认证中心 OAuth2 + PKCE 客户端:Node 机密客户端与浏览器公钥(PKCE)
Readme
@1meeting/auth-client-sdk
面向业务应用(A/B/未来扩展应用)的统一登录接入 SDK。
封装了平台仓 OAuth2 授权码 + PKCE 常用能力,帮助应用快速完成“跳转授权 -> 回调换码 -> 拉取用户信息”闭环。
快速上手请先看:QUICKSTART.md。
仓库内体系化集成说明(推荐):../../docs/05-client-sdk.md(流程、环境、authApiRoots、双入口、安全清单)。
全仓文档索引:../../docs/README.md。
1. 适用场景
- 子应用不自建账号体系,接入平台仓统一登录
- OAuth2 授权码模式(
response_type=code) - 支持 Node 后端(推荐)和浏览器 PKCE 场景
2. 安装
2.1 从 npmjs 安装(推荐)
npm i @1meeting/auth-client-sdk公开包可直接从 npmjs 安装,无需 GitHub 私有 registry token。
2.2 本地源码构建(本仓)
npm run build --prefix packages/auth-client-sdk3. 导出接口总览
Node 入口:@1meeting/auth-client-sdk
buildOAuth2AuthorizeUrl(input)(可选scope)buildOAuth2LogoutRedirectUrl(input)createNodeCodeVerifier()createS256CodeChallenge(verifier)createNodePkcePair()exchangeAuthorizationCode(input)(可选signal)exchangeRefreshTokenWithRoots(input)fetchUserInfoWithRoots(input)(可选signal)pickAccessTokenFromTokenResponse/pickRefreshTokenFromTokenResponse/pickExpiresInFromTokenResponse/pickTokenTypeFromTokenResponseparseTokenFromResponse(payload)、getApiErrorFromPayload(payload)pickUserInfoFromUserinfoResponse(payload)- 别名兼容:
createCodeVerifier(同createNodeCodeVerifier)createCodeChallenge(同createS256CodeChallenge)
浏览器入口:@1meeting/auth-client-sdk/browser
- 与 Node 入口对齐的 URL / 换码 / userinfo / 解析辅助函数(同上)
createBrowserCodeVerifier()、createS256CodeChallenge(verifier)(异步)、createBrowserPkcePair()(异步)exchangeAuthorizationCodePublic(input)(无clientSecret的换码封装)
4. 标准接入流程(推荐:后端换码)
- 服务端生成
state+ PKCE(codeVerifier/codeChallenge) - 组装授权地址并跳转平台仓
/api/oauth2/authorize - 回调拿到
code后,服务端调用exchangeAuthorizationCode - 从 token 响应中解析
accessToken - 调
fetchUserInfoWithRoots获取用户信息并建立本地会话
5. Node 接入示例(Express/BFF)
import crypto from 'node:crypto'
import {
createNodePkcePair,
buildOAuth2AuthorizeUrl,
exchangeAuthorizationCode,
fetchUserInfoWithRoots,
getApiErrorFromPayload,
parseTokenFromResponse,
pickAccessTokenFromTokenResponse,
pickUserInfoFromUserinfoResponse,
} from '@1meeting/auth-client-sdk'
// Step 1: 生成授权地址
const { codeVerifier, codeChallenge } = createNodePkcePair()
const state = crypto.randomUUID()
// 你需要把 codeVerifier 按 state 暂存到 session/redis
// saveState(state, { codeVerifier })
const authorizeUrl = buildOAuth2AuthorizeUrl({
authPublicBaseUrl: 'https://www.highgood.com/auth',
clientId: 'client_app_a',
redirectUri: 'https://www.highgood.com/a/api/auth/callback',
state,
nonce: crypto.randomUUID(),
codeChallenge,
codeChallengeMethod: 'S256',
})
// Step 2: 回调换码
const tokenResult = await exchangeAuthorizationCode({
code, // callback query.code
codeVerifier, // 从你保存的 state 中取回
redirectUri: 'https://www.highgood.com/a/api/auth/callback',
clientId: 'client_app_a',
clientSecret: 'app-a-secret',
authApiRoots: [
'http://auth-api.internal:3001', // 首选内网
'https://www.highgood.com/auth', // 可选 fallback
],
})
if (!tokenResult.ok) {
const err = getApiErrorFromPayload(tokenResult.payload)
throw new Error(err?.message || `token exchange failed: ${tokenResult.status}`)
}
const token = parseTokenFromResponse(tokenResult.payload)
const accessToken = token?.accessToken ?? pickAccessTokenFromTokenResponse(tokenResult.payload)
if (!accessToken) {
throw new Error('token payload missing accessToken')
}
// Step 3: 获取用户信息
const userInfoResult = await fetchUserInfoWithRoots({
accessToken,
authApiRoots: ['http://auth-api.internal:3001'],
})
if (!userInfoResult.ok) {
throw new Error(`userinfo failed: ${userInfoResult.status}`)
}
const user = pickUserInfoFromUserinfoResponse(userInfoResult.payload)
if (!user?.sub) {
throw new Error('userinfo payload invalid')
}6. 浏览器 PKCE 示例(纯前端)
import {
createBrowserPkcePair,
buildOAuth2AuthorizeUrl,
exchangeAuthorizationCodePublic,
} from '@1meeting/auth-client-sdk/browser'
const { codeVerifier, codeChallenge } = await createBrowserPkcePair()
const state = crypto.randomUUID()
sessionStorage.setItem(`pkce:${state}`, codeVerifier)
const authorizeUrl = buildOAuth2AuthorizeUrl({
authPublicBaseUrl: 'https://www.highgood.com/auth',
clientId: 'client_app_a',
redirectUri: 'https://www.highgood.com/a/callback',
state,
nonce: crypto.randomUUID(),
codeChallenge,
codeChallengeMethod: 'S256',
})
location.href = authorizeUrl
// 回调页中:
const code = new URL(location.href).searchParams.get('code')!
const callbackState = new URL(location.href).searchParams.get('state')!
const verifier = sessionStorage.getItem(`pkce:${callbackState}`)!
const tokenResult = await exchangeAuthorizationCodePublic({
code,
codeVerifier: verifier,
redirectUri: 'https://www.highgood.com/a/callback',
clientId: 'client_app_a',
authApiRoots: ['https://www.highgood.com/auth'],
})安全建议:生产优先用后端换码(BFF),不要在浏览器暴露机密
clientSecret。
7. API 详细说明
7.1 buildOAuth2AuthorizeUrl(input)
构建平台仓授权地址,返回完整 URL 字符串。
必填参数:
authPublicBaseUrl:认证中心对浏览器公开入口(如https://www.highgood.com/auth)clientIdredirectUristatenoncecodeChallengecodeChallengeMethod:固定S256
可选参数:
responseType:默认codescope:空格分隔(如openid profile);与extraQuery.scope同时存在时 以scope为准extraQuery:附加 query 参数
7.2 createNodePkcePair() / createBrowserPkcePair()
生成 { codeVerifier, codeChallenge },用于 PKCE 流程。
- Node 版同步
- Browser 版异步(WebCrypto)
7.3 exchangeAuthorizationCode(input)
调用认证中心 /api/oauth2/token 执行换码,返回:
type HttpResult = {
ok: boolean
status: number
payload: unknown
}特点:
authApiRoots支持多地址顺序尝试- 仅当
fetch抛网络级异常时才尝试下一个 root - 一旦拿到 任意 HTTP 响应(含 4xx/5xx)即返回(不重复消费授权码;也不为 5xx 自动换根以免语义混乱)
- 可选
signal:AbortController超时/取消
7.4 exchangeRefreshTokenWithRoots(input)
POST /api/oauth2/token,JSON:grantType: 'refresh_token'、refreshToken、clientId、clientSecret。
多根与 signal 行为同 exchangeAuthorizationCode。
7.5 fetchUserInfoWithRoots(input)
调用认证中心 /api/oauth2/userinfo 获取用户信息,返回 HttpResult(同上)。支持 signal。
7.6 buildOAuth2LogoutRedirectUrl(input)
拼浏览器顶层导航用的 GET …/api/oauth2/logout-redirect?return_to=…&global=1(global 可选)。
7.7 parseTokenFromResponse / pick* / getApiErrorFromPayload
parseTokenFromResponse:成功包络下返回TokenData(accessToken、refreshToken、tokenType、expiresIn),否则nullpickRefreshTokenFromTokenResponse等:按需取单字段getApiErrorFromPayload:从失败包络取code/message/traceId;code === 'OK'时返回null
7.8 pickUserInfoFromUserinfoResponse(payload)
从统一包络提取用户信息:
- 若
payload.code === 'OK'且存在data.sub,返回UserInfoData - 否则返回
undefined
8. 类型定义(核心)
HighgoodApiEnvelope<T>TokenDataUserInfoDataHttpResultBuildAuthorizeUrlInputBuildLogoutRedirectUrlInputExchangeCodeInputExchangeRefreshTokenInputFetchUserInfoInput
详见:src/types.ts
9. 常见问题
Q1: ERR_CONNECTION_REFUSED / token 请求失败
- 先检查
authApiRoots是否可达(内网地址优先) - 检查平台仓服务是否已启动
Q2: 回调后提示 state 不合法
- 确认
state -> codeVerifier保存与回调读取是同一份存储(session/redis) - 确认 state 未过期(建议 10~15 分钟 TTL)
Q3: redirect_uri 不匹配
- 确认 SDK 传入
redirectUri与平台注册 OAuth 客户端配置完全一致(协议/域名/路径)
Q4: 浏览器端换码失败
- 检查是否错误使用了机密客户端
- 建议改为后端(BFF)换码
Q5: 换码 / userinfo 请求挂起
- 为
exchangeAuthorizationCode/fetchUserInfoWithRoots/exchangeRefreshTokenWithRoots传入signal(例如AbortSignal.timeout(8000),按运行时支持情况 polyfill)
10. 发布建议
- 版本号遵循 semver(
0.1.x修复,0.2.x新能力);变更见CHANGELOG.md - 发布前至少验证:
- 授权跳转
- 回调换码
- userinfo
- 本地 loopback(localhost/127.0.0.1)场景
