用于向 Google API (Swift) 验证服务帐户的 JWT 签名无效

Posted

技术标签:

【中文标题】用于向 Google API (Swift) 验证服务帐户的 JWT 签名无效【英文标题】:Invalid JWT signature for authenticating service account to Google API (Swift) 【发布时间】:2021-10-03 17:18:10 【问题描述】:

我是 Google API 的新手;我正在尝试在没有用户交互的情况下检索服务帐户的访问令牌。我读过这样做的方法是生成一个 JWT,然后向 API 运行一个请求,但是当我将 JWT 发送给 Google 时,我不断收到此错误:

"error":"invalid_grant","error_description":"Invalid JWT Signature."

这是生成 JWT 的代码:

extension Data 
func urlSafeBase64EncodedString() -> String 
    return base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")




struct Header: Encodable 
    let alg = "RS256"
    let typ = "JWT"


struct Payload: Encodable 
    let iss = "content-uploader@project-id.iam.gserviceaccount.com"
    let scope = "https://www.googleapis.com/auth/devstorage.full_control"
    let aud = "https://oauth2.googleapis.com/token"
    let iat = Date().timeIntervalSince1970
    let exp = Date().timeIntervalSince1970.advanced(by: 1000)


let secret = "-----BEGIN PRIVATE KEY-----HERE IS A TON OF NUMBERS, SLASHES, PLUS SIGNS, GOING ON FOR HUNDREDS OF CHARACTERS. THIS WAS TAKEN FROM THE KEY DOWNLOADED AS A JSON FROM THE CREDENTIALS PAGE FOR THE SERVICE ACCOUNT IN GOOGLE CLOUD.\n-----END PRIVATE KEY-----\n"
let privateKey = SymmetricKey(data: secret.data(using: .utf8)!)

let headerJSONData = try! JSONEncoder().encode(Header())
let headerBase64String = headerJSONData.urlSafeBase64EncodedString()

let payloadJSONData = try! JSONEncoder().encode(Payload())
let payloadBase64String = payloadJSONData.urlSafeBase64EncodedString()

let toSign = (headerBase64String + "." + payloadBase64String).data(using: .utf8)!

let signature = HMAC<SHA256>.authenticationCode(for: toSign, using: privateKey)
let signatureBase64String = Data(signature).urlSafeBase64EncodedString()

let token = [headerBase64String, payloadBase64String, signatureBase64String].joined(separator: ".")

下面是正在运行的请求:

if let url = URL(string: "https://oauth2.googleapis.com/token?grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=\(token)") 
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

let task = URLSession.shared.dataTask(with: request)  data, response, error in
    if data != nil 
        print("Data: \(String(data: data!, encoding: .utf8)!)")
    
.resume()


【问题讨论】:

1) 您不能直接使用服务帐户 JSON 密钥文件中的私钥 string。格式为 PEM。您必须加载 PEM 密钥并将其转换为库所需的格式(通常是二进制字符串)。检查您的库中是否有类似 getKeyStringFromPEM() 2) 私钥是非对称的(RSA 私钥),而不是对称的。 3) 我不会用 Swift 编写代码,所以希望我的提示会有所帮助。 在标题中设置“alg”="RS256 并稍后加载私钥,这都可以,但是然后您使用 HMAC 签名,这是错误的。我也不是Swift 编码器,但我相信您可以找到使用 RS256 签名的示例。 谢谢你们——我需要使用从 .p12 转换的 PEM 密钥切换到 RSA 和 RS256——它现在可以工作了。 【参考方案1】:

我需要使用从 .p12 文件转换的 PEM 密钥切换到 RSA 和 RS256。使用 JWT.io 库有所帮助。 这是我的工作代码:

import JWTKit // from jwt.io
func generateJWT() -> String? 
        struct Header: JWTPayload 
            enum CodingKeys: String, CodingKey 
                case alg = "alg"
                case type = "typ"
            
            
        var alg = "RS256"
        var type: String = "JWT"
        
        func verify(using signer: JWTSigner) throws 
            //            print(self.expireTime > Date().timeIntervalSince1970)
            fatalError()
        
    
    
    struct Payload: JWTPayload 
        enum CodingKeys: String, CodingKey 
            case email = "iss"
            case scope = "scope"
            case aud = "aud"
            case createdAt = "iat"
            case expireTime = "exp"
        
        
        var email: String = "content-uploader@*************.iam.gserviceaccount.com"
        var scope: String = "https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/cloud-platform"
        var aud: String = "https://oauth2.googleapis.com/token"
        var createdAt: Double = Date().timeIntervalSince1970
        var expireTime: Double = Date().advanced(by: 1000).timeIntervalSince1970
        
        
        func verify(using signer: JWTSigner) throws 
            print(self.expireTime > Date().timeIntervalSince1970)
            fatalError()
        
    
    
    do 
        if let certificatePath = Bundle.main.path(forResource: "pem-file-name", ofType: "pem") 
            let certificateUrl = URL(fileURLWithPath: certificatePath)
            let certififcateData = try Data(contentsOf: certificateUrl)
            let signers = JWTSigners()
            let key = try RSAKey.private(pem: certififcateData)
            signers.use(.rs256(key: key))
            
            //                MARK: HEADER NOT USED
            let header = Header()
            let payload = Payload()
            let jwt = try signers.sign(payload)
            //                let jwt = try signers.sign(payload)
            print("JWT: \(jwt)")
            return jwt
         else 
            return nil
        
     catch 
        print(error)
        return nil
    

【讨论】:

以上是关于用于向 Google API (Swift) 验证服务帐户的 JWT 签名无效的主要内容,如果未能解决你的问题,请参考以下文章

在Swift中使用Google Calendar API时收到错误“超出未经身份验证的每日限制”

除了 AppDelegate 和 UIViewController 之外的一个类中的 GIDSignInDelegate 使用示例,用于 swift 中的 Google 身份验证

我如何注册Google Classroom API的推送通知?

Xamarin.Auth OAuth Google API,用于Android的用户身份验证

Google APi + Passport + React:身份验证流程

在 Python 3 上的 Django 中使用 Google 站点验证 API