Search code examples
pythonauthenticationservercloudkitcloudkit-web-services

CloudKit Server-to-Server auth: Keep getting 401 Authentication failed


I have been recently exploring the CloudKit and related frameworks. I got the communication with my app working, as well as with my website using CloudKitJS. Where I am struggling is the Server-to-Server communication (which I would need for exporting data from public database in csv.

I have tried Python package requests-cloudkit, which others were suggesting. I have created a Server-to-Server token, and have copied only the key between START and END line once creating the eckey.pem file. I then got this code:

from requests_cloudkit import CloudKitAuth
from restmapper import restmapper
import json
KEY_ID = '[my key ID from CK Dashboard]'
SECRET_FILE_KEY = 'eckey.pem'
AUTH = CloudKitAuth(KEY_ID, SECRET_FILE_KEY)
PARAMS = {
        'query':{
                'recordType': '[my record type]'
        },
}
CloudKit = restmapper.RestMapper("https://api.apple-cloudkit.com/database/1/[my container]/development/")
cloudkit = CloudKit(auth=AUTH)
response = cloudkit.POST.public.records.query(json.dumps(PARAMS))

I am then getting the 401 Authentication failed response. I am stuck on this for days, so I would be grateful for any help or advice. 😊


Solution

  • Creating the server-to-server key is an important first step, but in order to make HTTP requests after that, you have to sign each request.

    Look for the Authenticate Web Service Requests section near the bottom of this documentation page.

    It's a little bit convoluted, but you have to carefully construct signed headers to include with each request you make. I'm not familiar with how to do it in Python, but here's how I do it in NodeJS which may help:

    //Get the timestamp in a very specific format
    let date = moment().utc().format('YYYY-MM-DD[T]HH:mm:ss[Z]')
    
    //Construct the subpath
    let endpoint = '/records/lookup'
    let path = '/database/1/iCloud.*****/development/public'
    let subpath = path+endpoint
    
    //Get the key file
    let privateKeyFile = fs.readFileSync('../../'+SECRET_FILE_KEY, 'utf8')
    
    //Make a string out of your JSON query
    let query = {
      recordType: '[my record type]'
    }
    let requestBody = JSON.stringify(query)
    
    //Hash the query
    let bodyHash = crypto.createHash('sha256').update(requestBody, 'utf8').digest('base64')
    
    //Assemble the components you just generated in a special format
    //[Current date]:[Request body]:[Web service URL subpath]
    let message = date+':'+bodyHash+':'+subpath
      
    //Sign it
    let signature = crypto.createSign('RSA-SHA256').update(message).sign(privateKeyFile, 'base64')
    
    //Assemble your headers and include them in your HTTP request
    let headers = {
      'X-Apple-CloudKit-Request-KeyID': KEY_ID,
      'X-Apple-CloudKit-Request-ISO8601Date': date,
      'X-Apple-CloudKit-Request-SignatureV1': signature
    }
    

    This is a bit hairy at first, but I just put all this stuff in a function that I reuse whenever I need to make a request.

    Apple's documentation has pretty much been abandoned and it's hard to find good help with CloudKit Web Services these days.