Search code examples
swiftgoogle-sheetsoauth-2.0jwt

Google Document API Failing Authentication with a Google Service Account - 'Empty or missing scope not allowed.'


My Swift app needs to read a Google Document using the Google Document API using a Google Service Account for authentication. The app first requests an OAuth2 access token to use in making the call to the Google Drive API. Google provide libraries for some languages to perform this function but they do not include a library for Swift. So the app has to request and receive the access token by other means.

The app uses SwiftJWT to generate a signed JWT claim to send to /www.googleapis.com/oauth2/v4/token and expects the access token to be returned. However the error message from the call is -

**error = "invalid_scope";"error_description" = "Empty or missing scope not allowed."; **

The Service Account being used is named <my_project>@<...>.iam.gserviceaccount.com and has IAM roles 'owner' and 'viewer' in the project. It should therefore have permission to use the Google Documents API. (However in assigning roles to the Service Account I dont see any roles that specifically mention a documents/Google Documents API scope)

The private key used in the signed JWT request is generated from a key attached to the to this Google Service accouunt.

I have tried many ways to fix the issue but still coming up short. Any insights would be very helpful. Thanks in advance for you review...

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 {
        // Handle the case where the conversion fails
        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 {
        <------------------------------------------------ Error message is returned here
        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)")
        }
}

The post to https://oauth2.googleapis.com/token should return an access token but reports

error = "invalid_scope"; "error_description" = "Empty or missing scope not allowed.";


Solution

  • I figured it out. You need to pass scopes in the claims, not in the request body. However, SwiftJWT does not provide such a struct, so we can subclass it ourselves. This one worked for me:

    struct GoogleAPIClaims: Claims {
        var iss: String
        var sub: String
        var scope: String
        var aud: String
        var exp: Date
        var iat: Date
    }