带全域委派的服务账户执行Apps Script时Gmail API 403权限错误
我正在运行一个Google Apps Script,通过带全域委派(DWD)的服务账户,为Google Workspace域内用户批量更新Gmail签名。脚本能成功生成AccessToken,也能更新被模拟的超级管理员的签名,但操作非管理员用户(如user@mydomain.com)时,始终返回403 Forbidden错误:
403 Forbidden: { "error": { "code": 403, "message": "Delegation denied for admin@email.com", ... } }
已验证的配置
已完成以下标准排查,确认配置无误:
- 被模拟用户:服务账户模拟的admin@email.com是活跃超级管理员
- 服务账户Client ID:脚本属性中配置的Client ID与管理控制台的DWD条目完全一致(已尝试生成新ID)
- 全域委派范围:管理控制台注册的范围仅包含所需的两个权限:
https://www.googleapis.com/auth/gmail.settings.basic,https://www.googleapis.com/auth/admin.directory.user.readonly - API请求格式:使用正确的请求路径:
PUT https://gmail.googleapis.com/gmail/v1/users/{userEmail}/settings/sendAs/{userEmail} - 同步等待:每次配置修改后均等待30分钟以上
脚本代码
function updateSignaturesFromSheet() { const privateKey = PropertiesService.getScriptProperties().getProperty('PRIVATE_KEY'); const clientEmail = PropertiesService.getScriptProperties().getProperty('CLIENT_EMAIL'); const adminEmail = 'admin@email.com'; // Admin email registered in DWD if (!privateKey || !clientEmail) { Logger.log(' ERROR: CLIENT_EMAIL or PRIVATE_KEY not set in Script Properties.'); return; } const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const data = sheet.getDataRange().getValues(); Logger.log(` Found ${data.length - 1} rows of user data to process.`); let accessToken; try { accessToken = getServiceAccountAccessToken(clientEmail, privateKey, adminEmail); } catch(e) { Logger.log(` Failed to get DWD token: ${e.message}`); return; } for (let i = 1; i < data.length; i++) { const [email, name, position, phone, website, logoURL] = data[i]; if (!email || !name) continue; const userEmail = email.toString().trim(); const companyName = "Company Name"; const cleanPhone = phone ? phone.toString().replace(/[^0-9+()\s-]/g, '') : ''; const telLink = cleanPhone.replace(/[^0-9+]/g, ''); const signatureHtml = `<div style="font-family:Arial, sans-serif; font-size:14px; color:#333;"> ${name}<br> ${position ? `${position}<br>` : ''} <strong>${companyName}</strong><br> ${logoURL ? `<img src="${logoURL}" width="150" style="display:block;margin:5px 0;">` : ''} ${cleanPhone ? `Office: <a href="tel:${telLink}" style="color:#000;">${cleanPhone}</a><br>` : ''} ${website ? `<a href="${website}" target="_blank" style="color:#0073e6;">${website}</a>` : ''} </div>`; try { const url = `https://gmail.googleapis.com/gmail/v1/users/${userEmail}/settings/sendAs/${userEmail}`; const options = { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, payload: JSON.stringify({ signature: signatureHtml }), muteHttpExceptions: true }; const response = UrlFetchApp.fetch(url, options); const responseCode = response.getResponseCode(); if (responseCode === 200) Logger.log(` Signature updated for: ${userEmail}`); else Logger.log(`Failed for ${userEmail} (Code: ${responseCode}): ${response.getContentText()}`); } catch (err) { Logger.log(` Error for ${email}: ${err.message}`); } } Logger.log(' Signature update process complete.'); } function getServiceAccountAccessToken(clientEmail, privateKey, adminEmail) { const correctedPrivateKey = privateKey.trim().replace(/\\n/g, '\n'); const jwtHeader = Utilities.base64EncodeWebSafe(JSON.stringify({ alg: "RS256", typ: "JWT" })); const jwtClaim = Utilities.base64EncodeWebSafe(JSON.stringify({ iss: clientEmail, scope: "https://www.googleapis.com/auth/gmail.settings.basic https://www.googleapis.com/auth/admin.directory.user.readonly", aud: "https://oauth2.googleapis.com/token", exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), sub: adminEmail })); const signatureInput = `${jwtHeader}.${jwtClaim}`; const signature = Utilities.base64EncodeWebSafe( Utilities.computeRsaSha256Signature(signatureInput, correctedPrivateKey) ); const jwt = `${signatureInput}.${signature}`; const tokenResponse = UrlFetchApp.fetch("https://oauth2.googleapis.com/token", { method: "post", payload: { grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt }, muteHttpExceptions: true }); const tokenData = JSON.parse(tokenResponse.getContentText()); if (!tokenData.access_token) throw new Error(`Failed to get access token: ${tokenResponse.getContentText()}`); return tokenData.access_token; }
解决建议
修正全域委派范围格式
在Google Workspace管理控制台的「域宽委派」中,多个权限范围需用空格分隔而非逗号。若之前用了逗号,会导致范围未正确识别,需修改为:https://www.googleapis.com/auth/gmail.settings.basic https://www.googleapis.com/auth/admin.directory.user.readonly。切换API请求方法
将脚本中的method: 'PATCH'改为method: 'PUT',Gmail API更新签名时PUT方法更稳定,能确保覆盖原有签名配置。验证AccessToken权限
用jwt.io解码生成的AccessToken,检查scope字段是否包含正确的两个权限,sub字段是否为admin@email.com,确保令牌具备委派访问的权限。确认用户主邮箱地址
确保脚本中使用的userEmail是用户的主邮箱地址而非别名。可调用Gmail API的users.sendAs.list接口,确认目标地址为用户isPrimary: true的别名。检查管理员权限完整性
登录Google Workspace管理控制台,进入「管理员角色」,确认超级管理员角色包含「Gmail设置管理」权限,确保admin@email.com未被移除相关权限。
内容的提问来源于stack exchange,提问作者nano nano

