You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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)

  1. 打开Google Apps Script编辑器,点击「扩展」→「Apps Script」,在编辑器中点击「库」,添加jsrsasign的官方脚本ID(可通过库的官方文档获取最新ID)
  2. 使用库中的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;
};

额外注意点

  1. 确保私钥格式正确:Apple提供的是PKCS#8格式的PEM私钥,需完整保留-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----头尾部。
  2. 检查payload参数:iss必须是Apple开发者账号的Issuer ID,exp不能超过20分钟(Apple令牌最大有效期),aud固定为appstoreconnect-v1。
  3. 权限验证:确保服务账号对应的API密钥已赋予App Store Connect API的相关权限。

内容的提问来源于stack exchange,提问作者codelover

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 16:17:04