使用Google服务账户调用Google Document API认证失败:权限范围为空/缺失
问题背景
Swift应用需通过Google服务账户认证,调用Google Documents API读取文档。因无Swift官方库,采用SwiftJWT生成签名JWT请求https://oauth2.googleapis.com/token获取访问令牌,但返回invalid_scope错误。
服务账户信息:<my_project>@<...>.iam.gserviceaccount.com,已分配'owner'和'viewer'的IAM角色,使用该账户绑定的私钥签名JWT。
错误信息
error = "invalid_scope";"error_description" = "Empty or missing scope not allowed.";
相关代码
import SwiftJWT import Alamofire func getAuthToken() { let header = Header(kid: "<myiD>") let claims = ClaimsStandardJWT( iss: "<my_server_account>@<my_project>.iam.gserviceaccount.com", aud: ["https://oauth2.googleapis.com/token"], exp: Date().addingTimeInterval(3600), iat: Date() ) var jwt = JWT(header: header, claims: claims) let privateKey = """ -----BEGIN PRIVATE KEY----- <my_private_key> -----END PRIVATE KEY----- """ guard let privateKeyData = privateKey.data(using: .utf8) else { print("Failed to convert string to data") return } var signedJWT = "" do { signedJWT = try jwt.sign(using: .rs256(privateKey: privateKeyData)) } catch { print("Failed to sign JWT: \(error)") } // Exchange the JWT token for a Google OAuth2 access token let headers: HTTPHeaders = ["Content-Type": "application/x-www-form-urlencoded"] let params: Parameters = [ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": signedJWT, "scope": "https://www.googleapis.com/auth/documents.readonly" ] AF.request("https://oauth2.googleapis.com/token", method: .post, parameters: params, encoding: URLEncoding.httpBody, headers: headers).responseJSON { response in print(response.result) switch response.result { case .success(let value): let json = value as? [String: Any] if let json = json { let accessToken = json["access_token"] as? String if let accessToken = accessToken { fetchGoogleDocContent(with: accessToken) } } case .failure(let error): print("Error getting access token: \(error)") } }
解决思路
修正JWT Claims的scope和aud字段:
Google服务账户的JWT认证要求scope必须包含在JWT的Payload中,而非仅在POST请求参数里。同时aud字段需为单个字符串,不能是数组。需自定义Claims结构体:struct GoogleServiceAccountClaims: Claims { let iss: String let aud: String let exp: Date let iat: Date let scope: String }初始化Claims时传入正确参数:
let claims = GoogleServiceAccountClaims( iss: "<my_server_account>@<my_project>.iam.gserviceaccount.com", aud: "https://oauth2.googleapis.com/token", exp: Date().addingTimeInterval(3600), iat: Date(), scope: "https://www.googleapis.com/auth/documents.readonly" )确认API启用状态:登录Google Cloud控制台,检查Google Docs API是否已启用,未启用的话需手动开启。
验证JWT内容:对签名后的JWT进行本地解码,检查Payload中是否包含正确的
scope和aud字段,确保没有拼写错误。确认scope有效性:
https://www.googleapis.com/auth/documents.readonly是Google Docs只读访问的正确scope,确认无拼写错误。
内容的提问来源于stack exchange,提问作者David M

