如何用Google Apps Script生成ES256签名的JWT调用App Store Connect API
如何在Google Apps Script中生成符合ES256算法的App Store Connect API JWT令牌
需求背景
我需要为App Store Connect API生成采用ES256算法的JWT令牌,目前已通过以下Python代码成功实现API调用:
from datetime import datetime, timedelta import json import time import curlify import jwt import requests def generate_access_token(): # 服务账号私钥文件路径 PRIVATE_KEY_FILE = "private_apple_key" # Apple令牌端点标识 TOKEN_ENDPOINT = "appstoreconnect-v1" issuer_id = "fc7d6b48-0000-00000-00000-4e8230bfda8f" # 已脱敏 key_id = "WL000000XAU" # 已脱敏 ALGORITHM = 'ES256' try: key = open(PRIVATE_KEY_FILE, 'r').read() except IOError as e: key = PRIVATE_KEY_FILE token_gen_date = datetime.now() exp = int(time.mktime((token_gen_date + timedelta(minutes=20)).timetuple())) jwt_token = jwt.encode( {'iss': issuer_id, 'exp': exp, 'aud': TOKEN_ENDPOINT}, key, headers={'kid': key_id, 'typ': 'JWT'}, algorithm=ALGORITHM ).decode('ascii') print("\n", jwt_token, "\n") return jwt_token def get_user_list(): access_token_value = generate_access_token() headers = {"Authorization": f"Bearer {access_token_value}"} # 调用App Store Connect API response = requests.get("https://api.appstoreconnect.apple.com/v1/apps", headers=headers) # response = requests.get("https://api.appstoreconnect.apple.com/v1/userInvitations?filter[username]='abc@gme.com'", headers=headers) print(curlify.to_curl(response.request)) if response.status_code == 200: contents = json.loads(response.content) print(contents) else: print(response.status_code) print("\n", response.content) get_user_list() # generate_access_token()
由于安全及域权限限制,无法将Python代码与Google Sheets共享服务账号,因此需要改用Google Apps Script实现JWT生成及API调用,以自动化更新Google Sheets。
问题描述
我编写的Google Apps Script代码生成的令牌未被App Store Connect认可,返回401错误:
{ "errors": [ { "status": "401", "code": "NOT_AUTHORIZED", "title": "Authentication credentials are missing or invalid.", "detail": "Provide a properly configured and signed bearer token, and make sure that it has not expired. Learn more about Generating Tokens for API Requests" } ] }
我的Google Apps Script代码如下(参考了适配RSA256、HS256的示例,使用Utilities.computeHmacSha256Signature函数):
var appleCredentials = SecurityAdapter.getAppleCredentials(); var issuerId = appleCredentials["issuer_id"]; var key_id = appleCredentials["key_id"]; var apple_p_key = appleCredentials["p_key"]; // ----- ======== ...... ------- const createJwt = ({ privateKey, data = {} }) => { // 错误地使用HMAC-SHA256签名 var appleCredentials = SecurityAdapter.getAppleCredentials(); var key_id = appleCredentials["key_id"]; const header = { alg: 'ES256', typ: 'JWT', kid: key_id, }; const payload = { iss: "issuerId", // 此处错误:使用了字符串而非变量 exp: Math.floor(Date.now() / 1000) + 20 * 60, aud: 'appstoreconnect-v1', }; // 合并用户传入的payload数据 Object.keys(data).forEach(function (key) { payload[key] = data[key]; }); const base64Encode = (text, json = true) => { const data = json ? JSON.stringify(text) : text; return Utilities.base64EncodeWebSafe(data).replace(/=+$/, ''); }; const toSign = `${base64Encode(header)}.${base64Encode(payload)}`; const signatureBytes = Utilities.computeHmacSha256Signature(toSign, privateKey); const signature = base64Encode(signatureBytes, false); return `${toSign}.${signature}`; }; const generateAccessToken = () => { var appleCredentials = SecurityAdapter.getAppleCredentials(); var key_id = appleCredentials["key_id"]; var privateKey = appleCredentials["p_key"]; var issuerId = appleCredentials["issuer_id"]; const accessToken = createJwt({ privateKey, expiresInHours: 0.20, // 无实际作用 data: { iss: issuerId, exp: Math.floor(Date.now() / 1000) + 20 * 60, aud: 'appstoreconnect-v1', }, }); Logger.log(accessToken); };
排查发现
通过JWT验证工具对比,Google Apps Script生成的无效JWT与Python生成的有效JWT头部、载荷解码内容一致,但签名段长度不同:
- 无效JWT签名段:
DzXbndN_1l1O1Kr111111111195Zje0_7UKs9FrZck0 - 有效JWT签名段:
R98fbDcwOfvMjGPJqFJAdYNLI1111111111Y0LUzFbNDq5dARTS7nja6LRB0Kw3Z1OEgcKuz2oV2MayB9HOMKg
核心问题在于Utilities.computeHmacSha256Signature是HMAC-SHA256对称哈希签名函数,而ES256是ECDSA(椭圆曲线数字签名算法),两者完全不兼容,因此生成的签名无法通过Apple的验证。
解决方案
Google Apps Script的Utilities类没有内置ECDSA签名方法,需通过以下两种方式实现:
方法一:使用Web Crypto API实现ES256签名(推荐,基于V8运行环境)
Web Crypto API支持ECDSA签名,需先将PEM格式的私钥转换为CryptoKey对象,再进行签名,并处理签名格式以符合JWT要求:
const generateES256Jwt = () => { const appleCredentials = SecurityAdapter.getAppleCredentials(); const issuerId = appleCredentials["issuer_id"]; const keyId = appleCredentials["key_id"]; const privateKeyPem = appleCredentials["p_key"]; // 1. 定义JWT头部和载荷 const header = { alg: "ES256", typ: "JWT", kid: keyId }; const payload = { iss: issuerId, exp: Math.floor(Date.now() / 1000) + 20 * 60, // 20分钟过期 aud: "appstoreconnect-v1" }; // 2. Base64URL编码头部和载荷 const base64UrlEncode = (obj) => { return Utilities.base64EncodeWebSafe(JSON.stringify(obj)) .replace(/=+$/, '') .replace(/\+/g, '-') .replace(/\//g, '_'); }; const encodedHeader = base64UrlEncode(header); const encodedPayload = base64UrlEncode(payload); const signingInput = `${encodedHeader}.${encodedPayload}`; // 3. 转换PEM私钥为二进制格式 const pemToBinary = (pem) => { const pemHeader = "-----BEGIN PRIVATE KEY-----"; const pemFooter = "-----END PRIVATE KEY-----"; const stripped = pem.replace(pemHeader, '').replace(pemFooter, '').replace(/\s/g, ''); return Utilities.base64Decode(stripped); }; const privateKeyBinary = pemToBinary(privateKeyPem); // 4. 使用Web Crypto API导入密钥并签名 const cryptoKey = crypto.subtle.importKey( "pkcs8", privateKeyBinary, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"] ); const signatureBuffer = crypto.subtle.sign( { name: "ECDSA", hash: { name: "SHA-256" } }, cryptoKey, Utilities.newBlob([signingInput]).getBytes() ); // 5. 处理签名格式:将ASN.1 DER格式转换为JWT要求的r+s拼接格式 const derToJwtSignature = (derBuffer) => { const view = new DataView(derBuffer); let offset = 0; if (view.getUint8(offset++) !== 0x30) throw new Error("Invalid DER"); const length = view.getUint8(offset++); if (view.getUint8(offset++) !== 0x02) throw new Error("Invalid DER"); const rLength = view.getUint8(offset++); const r = new Uint8Array(derBuffer, offset, rLength); offset += rLength; if (view.getUint8(offset++) !== 0x02) throw new Error("Invalid DER"); const sLength = view.getUint8(offset++); const s = new Uint8Array(derBuffer, offset, sLength); // 确保r和s都是32字节(P-256曲线要求) const padTo32Bytes = (arr) => { if (arr.length >= 32) return arr; const padded = new Uint8Array(32); padded.set(arr, 32 - arr.length); return padded; }; const paddedR = padTo32Bytes(r); const paddedS = padTo32Bytes(s); // 拼接r和s并Base64URL编码 const combined = new Uint8Array([...paddedR, ...paddedS]); return Utilities.base64EncodeWebSafe(combined) .replace(/=+$/, '') .replace(/\+/g, '-') .replace(/\//g, '_'); }; // 6. 组合最终JWT const signature = derToJwtSignature(signatureBuffer); const jwt = `${encodedHeader}.${encodedPayload}.${signature}`; Logger.log(jwt); return jwt; }; // 调用示例:生成令牌并调用API const callAppStoreConnectApi = () => { const jwtToken = generateES256Jwt(); const headers = { "Authorization": `Bearer ${jwtToken}`, "Content-Type": "application/json" }; const response = UrlFetchApp.fetch("https://api.appstoreconnect.apple.com/v1/apps", { headers: headers }); Logger.log(response.getContentText()); };
方法二:引入第三方JS库(如jsrsasign)
- 打开Google Apps Script编辑器,点击「扩展」→「Apps Script」,在编辑器中点击「库」,添加jsrsasign的官方脚本ID(可通过库的官方文档获取最新ID)
- 使用库中的
KJUR.jws.JWS.sign方法生成ES256签名的JWT:
const generateJwtWithJsrsasign = () => { const appleCredentials = SecurityAdapter.getAppleCredentials(); const issuerId = appleCredentials["issuer_id"]; const keyId = appleCredentials["key_id"]; const privateKeyPem = appleCredentials["p_key"]; const header = { alg: "ES256", typ: "JWT", kid: keyId }; const payload = { iss: issuerId, exp: Math.floor(Date.now() / 1000) + 20 * 60, aud: "appstoreconnect-v1" }; // 使用jsrsasign库签名 const jwt = KJUR.jws.JWS.sign( "ES256", header, payload, privateKeyPem ); Logger.log(jwt); return jwt; };
额外注意点
- 确保私钥格式正确:Apple提供的是PKCS#8格式的PEM私钥,需完整保留
-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----头尾部。 - 检查payload参数:
iss必须是Apple开发者账号的Issuer ID,exp不能超过20分钟(Apple令牌最大有效期),aud固定为appstoreconnect-v1。 - 权限验证:确保服务账号对应的API密钥已赋予App Store Connect API的相关权限。
内容的提问来源于stack exchange,提问作者codelover
相关产品推荐
相关产品推荐

