🐛 fix: eSIM 交换流程补齐/归一化 MFA ref 并增强校验

- 在 eSIM 交换前对 mfaRef 进行 trim 归一化,按需同时设置 X-MFA-Ref 与 X-MFA-Challenge-Ref,并将 mfaRef 作为 GraphQL 顶层字段传入
- 完整短信激活流程中统一从 MFA 校验结果 ref 或 emailCodeRef 获取并校验 swapMfaRef,缺失时抛出缺少 ref 的本地化错误
- MFA 校验增加对 signature 缺失的保护,成功返回时同时携带 ref 供后续交换步骤使用
- Netlify GraphQL 代理函数解析 X-MFA-Ref / X-MFA-Challenge-Ref 与请求体中的 mfaRef,进行 trim 后优先取体内 ref 并回填到 graphqlBody.mfaRef,确保 schema 需要的顶层字段始终可用
This commit is contained in:
Abner
2026-05-18 19:36:28 +08:00
parent ec84274a91
commit 5a88dc6af3
2 changed files with 35 additions and 14 deletions

View File

@@ -29,6 +29,10 @@ exports.handler = withAuth(async (event, context, { auth, body }) => {
Object.entries(event.headers || {}).map(([k, v]) => [String(k).toLowerCase(), v])
);
const authHeader = lowerCaseHeaders['authorization'] || '';
const headerMfaRefRaw = lowerCaseHeaders['x-mfa-ref'] || lowerCaseHeaders['x-mfa-challenge-ref'] || '';
const headerMfaRef = typeof headerMfaRefRaw === 'string' ? headerMfaRefRaw.trim() : '';
const bodyMfaRef = typeof mfaRef === 'string' ? mfaRef.trim() : '';
const resolvedMfaRef = bodyMfaRef || headerMfaRef;
let accessToken = body.accessToken;
if (!accessToken && authHeader.startsWith('Bearer ')) {
accessToken = authHeader.slice(7);
@@ -94,8 +98,8 @@ exports.handler = withAuth(async (event, context, { auth, body }) => {
};
// swapSim 需要 mfaRef 作为顶层字段Giffgaff schema 要求)
if (mfaRef) {
graphqlBody.mfaRef = mfaRef;
if (resolvedMfaRef) {
graphqlBody.mfaRef = resolvedMfaRef;
}
// 供失败时刷新令牌使用的 verify-cookie 地址

View File

@@ -138,15 +138,22 @@ export class ESimService {
async swapSim(activationCode, mfaSignature, mfaRef) {
try {
const state = stateManager.getState();
const normalizedMfaRef = typeof mfaRef === 'string' ? mfaRef.trim() : '';
const requestHeaders = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${state.accessToken}`,
'X-MFA-Signature': mfaSignature,
'X-CF-Turnstile': window.__cfTurnstileToken || ''
};
if (normalizedMfaRef) {
requestHeaders['X-MFA-Ref'] = normalizedMfaRef;
requestHeaders['X-MFA-Challenge-Ref'] = normalizedMfaRef;
}
const response = await fetch(this.apiEndpoints.graphql, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${state.accessToken}`,
'X-MFA-Signature': mfaSignature,
'X-CF-Turnstile': window.__cfTurnstileToken || ''
},
headers: requestHeaders,
body: JSON.stringify({
accessToken: state.accessToken,
mfaSignature: mfaSignature,
@@ -157,7 +164,7 @@ export class ESimService {
mfaSignature
},
query: graphqlQueries.swapSim,
mfaRef
mfaRef: normalizedMfaRef || undefined
})
});
@@ -312,10 +319,12 @@ export class ESimService {
*/
async smsActivateFlow(smsCode) {
try {
const state = stateManager.getState();
// 1. 验证短信验证码
const mfaResult = await this.validateMFACodeForSwap(smsCode);
const swapMfaRef = (mfaResult.ref || stateManager.get('emailCodeRef') || '').trim();
if (!swapMfaRef) {
throw new Error(t('giffgaff.mfa.errors.missingRef'));
}
// 2. 预订eSIM
const reserveResult = await this.reserveESim();
@@ -324,7 +333,7 @@ export class ESimService {
const swapResult = await this.swapSim(
reserveResult.data.esim.activationCode,
mfaResult.signature,
state.emailCodeRef
swapMfaRef
);
// 4. 获取LPA轮询
@@ -370,6 +379,11 @@ export class ESimService {
*/
async validateMFACodeForSwap(code) {
const state = stateManager.getState();
const ref = (state.emailCodeRef || '').trim();
if (!ref) {
throw new Error(t('giffgaff.mfa.errors.missingRef'));
}
const response = await fetch(this.apiEndpoints.mfaValidation, {
method: 'POST',
@@ -381,7 +395,7 @@ export class ESimService {
body: JSON.stringify({
accessToken: state.accessToken,
cookie: stateManager.getCookie() || undefined,
ref: state.emailCodeRef,
ref,
code,
turnstileToken: window.__cfTurnstileToken || ''
})
@@ -397,9 +411,12 @@ export class ESimService {
const data = await response.json();
const signature = data.signature || '';
if (!signature) {
throw new Error(t('giffgaff.mfa.errors.missingSignature'));
}
stateManager.set('emailSignature', signature);
return { success: true, signature };
return { success: true, signature, ref };
}
}