401 Error on CloudKit Server-to-Server Authentication on Node JS - cloudkit

I'm trying to query my public CloudKit database using server-to-server authentication. I've generated the key according to Apple's docs, but no matter what I do I get this error:
401 - Unauthorized
data: {
uuid: '...',
serverErrorCode: 'AUTHENTICATION_FAILED',
reason: 'no auth method found'
}
As far as I can tell, I've set everything up per the docs, but obviously I'm doing something wrong. Here's what I've got in my Node app so far:
let date = moment().format('YYYY-MM-DD[T]HH:mm:ss[Z]')
let domain = 'https://api.apple-cloudkit.com'
let subpath = '/database/1/iCloud.<my container>/development/public/users/current'
let key = fs.readFileSync(__dirname +'/../eckey.pem', 'utf8')
let keyID = 'abc123...'
let requestBody = ''
let bodyHash = crypto.createHash('SHA256').update(requestBody).digest('base64')
let message = date+':'+bodyHash+':'+subpath
let signature = crypto.createSign('RSA-SHA256').update(message).sign(key, 'base64')
let headers = {
'X-Apple-CloudKit-Request-KeyID': keyID,
'X-Apple-CloudKit-Request-ISO8601Date': date,
'X-Apple-CloudKit-Request-SignatureV1': signature
}
try{
await axios.post(domain+subpath, requestBody, { headers: headers })
console.log('--- :) ---')
}catch(error){
console.log('=== :( ===')
console.log(error)
}
I've already reviewed this helpful SO post, but I'm still stuck.
Can anyone see what I might be doing wrong?

I had to do a ton of troubleshooting to figure this out, but for the sake of posterity, here's what I had wrong:
=== Fix # 1 ===
My date was generating local time which was inaccurate because the format implies Zulu/UTC time (because of the Z).
The fix was to add .utc() to the Moment:
let date = moment().utc().format('YYYY-MM-DD[T]HH:mm:ss[Z]')
=== Fix # 2 ===
Apparently Axios didn't like how I was formatting the request. Changing it to this (with the baseURL and url separate) works:
let response = await axios({
method: 'post',
baseURL: baseURL,
url: '/records/modify',
data: query,
headers: headers
})
Seems to be working great now with these fixes in place.

Related

Downloading files with a manually generated signed url errors with "SignatureDoesNotMatch"

I've followed this guide to implement a URL signer for cloud storage download URLs in Rust.
Bucket and service account have necessary permissions.
I'm using signBlob technique to sign the string.
Here is the output of my canonical request.
GET
/abc.pd
x-goog-algorithm=GOOG4%2DRSA%2DSHA256&x-goog-credential=basak%2Dservice%40alibasak%2Eiam%2Egserviceaccount%2Ecom%2F20220521%2Fauto%2Fstorage%2Fgoog4%5Frequest&x-goog-date=20220521T004156Z&x-goog-expires=3600&x-goog-signedHeaders=host
host:basak.storage.googleapis.com
host
UNSIGNED-PAYLOAD
My string to sign.
(Hash is calculated from the canonical request with SHA256)
GOOG4-RSA-SHA256
20220521T004156Z
20220521/auto/storage/goog4_request
43b27d5947adf3b915d8a5a51cfe2f5cf1344a12b3d3731287cf4741525eabef
The final signed url I produce.
https://basak.storage.googleapis.com/abc.pd?x-goog-algorithm=GOOG4%2DRSA%2DSHA256&x-goog-credential=basak%2Dservice%40alibasak%2Eiam%2Egserviceaccount%2Ecom%2F20220521%2Fauto%2Fstorage%2Fgoog4%5Frequest&x-goog-date=20220521T004156Z&x-goog-expires=3600&x-goog-signedHeaders=host&x-goog-signature=TBqhQ9edLBnGC0z8jhWPAt6NDGM87PHcdAZBt2bcVfd9N/zE1i/HY0jUi5XZoMUgABoBvU36dizS4lr8PrOjXG6GT9KgXbEBcrQqPb83outeAfhL2pgXgbQjXcetFX7cYzY3GSULRWs7+7wH0rxMWiQ6E3tahraBUXI9VZ2XqbUGLuZZXtOhExQ14dKWOnvVVEl0C5BehMEXpDzMFXSWUrsuDMpDlN86nwaJgcGlTNBBrot7J2gMde+xGcJ4zC/c3BADoKHGdjhyOzQh7zToQHnpkLHdEVILUD7k4CN6f9TNzvUGsqNABJ3H4t3fwDgZ/2OqSJ9Na6Xisi2OMMgaTQ==
Trying a get request with this URL results with this error,
<?xml version='1.0' encoding='UTF-8'?>
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message>
<StringToSign>GOOG4-RSA-SHA256
20220521T004156Z
20220521/auto/storage/goog4_request
e38b8ab7532e4fa3009a7234313086ed45551be91398a5eef856eef4bfd857cf</StringToSign>
<CanonicalRequest>GET /abc.pd x-goog-algorithm=GOOG4-RSA-SHA256&x-goog-credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fauto%2Fstorage%2Fgoog4_request&x-goog-date=20220521T004156Z&x-goog-expires=3600&x-goog-signedHeaders=host
host:basak.storage.googleapis.com
host
UNSIGNED-PAYLOAD</CanonicalRequest>
</Error>
I've read that sometimes a header mismatch was causing this, I've tried to make this request in browser, curl and with a node script. Each of those give me the same error.
Could you point me to the right direction about debugging this or maybe you may think of causes which I'm not aware yet.
Thanks a lot.
Update
John Hanley suggested that the problem might be in the encoding.
I've applied the fixes he suggested such as sorting the headers and took the example in the end of this page as a reference to percent encode the URL.
Now the canonical request looks like this
GET
/abc.pd
X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220521T082931Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host
host:basak.storage.googleapis.com
host
UNSIGNED-PAYLOAD
and the final URL looks like this,
https://basak.storage.googleapis.com/abc.pd?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220521T082931Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=SEf8qecHjF+T+Uy71jygrEzrj2I1TXeKdUAOpm+BEf0Xm0W9e/LH5MC5BN+JA+OZ44EHN///Ai8ZO7GXqmE6+xbjrVhPlVETkLQSQ84GVecxj6onauRmpDTpxW8RXc+fXdzia75YndT5D1HKjvy8nI26Va8YMG2U3W/A7HMjg5YG+Cmcow3Pw1jpjIG+03gLLobhVMTnyp63S5AnycWT3Wzf7uo6l/WR7MxMK7pKA3isXXhQg7g9o8XUFFMvesDZUsI6mLYakxFHKHL42p1h6/P1SCFke1lpuYa9pV/EVnUMVfLp1ZfkgA1WBLqKZzyPNhMepfgIsUiUv2AKx/VoWQ==
I've also used the cli command,
gsutil -i basak-service#alibasak.iam.gserviceaccount.com signurl -d 10m -u gs://basak/abc.pd
to sign the url for comparison and the result is
https://storage.googleapis.com/basak/abc.pd?x-goog-signature=454801bc0d9ae19b5c5465a4e76846abbb1775549fe2532c839952125a54c5b1cb8f89385ac1cc1a8313e23945951d259a2ddc5a1c95890a205db1ab30a32d6efe8a2e706d03c68de6c4a502f50ff1a1b7fae5b94e3aa85768bfe473abf557eb8ae4e2b15ff9a5bb73ccb3d0bd1b8470cbe0bcb7ec6538fc575664672d641cb9f3c63ec04c41c13a6f2f6329290ce82bc57a700137edcf6fbade0885dd8130ebe2ba9bfe48f91ec94bf6e85b2ac8a7a26aeda77cbd5b0c30136d77defffeb5493f08bf9479f84522c1cb78503693e8ceab79fe0c6282ac4ecaa7e33b6355d2a7f870409b777512819ef54628a86a43b14ce8370477d11a9f857c2ec4ada90d6b&x-goog-algorithm=GOOG4-RSA-SHA256&x-goog-credential=basak-service%40alibasak.iam.gserviceaccount.com%2F20220521%2Fus%2Fstorage%2Fgoog4_request&x-goog-date=20220521T090829Z&x-goog-expires=600&x-goog-signedheaders=host
this url works perfectly.
The differences are:
Header order starts with the signature and continues sorted.
Headers are lowercase
The signature does not contain slashes. (It could be that I need to decode the signature? I'll check.)
I've implemented the first two in my code quickly and nothing changed.
On the other hand I suspect that maybe I'm doing something wrong with the signature since neither the example nor the gsutil generated url have slashes in the signature..
As John Hanley suggested I am sharing my code.
(The code is written in a sketchy way to debug and make it work first. Imports and use statements omitted.)
const SERVICE_ACCOUNT_EMAIL: &str = "basak-service#alibasak.iam.gserviceaccount.com";
#[derive(Serialize)]
struct SignRequest {
// The sequence of service accounts in a delegation chain. Each service account must be granted the roles/iam.serviceAccountTokenCreator role on its next service account in the chain. The last service account in the chain must be granted the roles/iam.serviceAccountTokenCreator role on the service account that is specified in the name field of the request.
// The delegates must have the following format: projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID}. The - wildcard character is required; replacing it with a project ID is invalid.
delegates: Vec<String>,
// Required. The bytes to sign.
// A base64-encoded string.
payload: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignResponse {
key_id: String,
signed_blob: String,
}
fn credential_scope(date: &str, location: &str, service: &str, request_type: &str) -> String {
format!("{date}/{location}/{service}/{request_type}")
}
const FRAGMENT: &AsciiSet = &CONTROLS
// ?=!#$&'()*+,:;#[]."
.add(b'/')
.add(b' ')
.remove(b'=')
.remove(b'!')
.remove(b'#')
.remove(b'$')
.remove(b'&')
.remove(b'\'')
.remove(b'(')
.remove(b')')
.remove(b'*')
.remove(b'+')
.remove(b',')
.remove(b':')
.remove(b';')
.add(b'#')
.remove(b'[')
.remove(b']')
.remove(b'.')
.remove(b'"');
async fn generate_signed_url(
bucket_name: &str,
object_name: &str,
expiration: &str,
token: &gcp_auth::Token,
) -> Result<String, reqwest::Error> {
// We'll use google's signing service to generate a signature.
let sign_blob_url = format!("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{SERVICE_ACCOUNT_EMAIL}:signBlob?alt=json");
let mut query_parameters = std::collections::BTreeMap::new();
let canonical_uri = format!("/{object_name}");
let date = chrono::offset::Utc::today().format("%Y%m%d").to_string();
let time_stamp = chrono::offset::Utc::now()
.format("%Y%m%dT%H%M%SZ")
.to_string();
let credential_scope = credential_scope(&date, "auto", "storage", "goog4_request");
let credential = format!("{SERVICE_ACCOUNT_EMAIL}/{credential_scope}");
let host = format!("{bucket_name}.storage.googleapis.com");
let algorithm = "GOOG4-RSA-SHA256";
query_parameters.insert("x-goog-algorithm", algorithm);
query_parameters.insert("x-goog-credential", &credential);
query_parameters.insert("x-goog-date", &time_stamp);
query_parameters.insert("x-goog-expires", expiration);
query_parameters.insert("x-goog-signedHeaders", "host");
let mut canonical_query_string =
query_parameters
.iter()
.fold("".to_owned(), |mut acc, (k, v)| {
//
let encoded_k = percent_encode(k.as_bytes(), FRAGMENT);
let encoded_v = percent_encode(v.as_bytes(), FRAGMENT);
acc.push_str(&format!("{}={}&", encoded_k, encoded_v));
acc
});
canonical_query_string.pop();
// let canonical_headers = format!("content-type:text/plain\nhost:{host}");
// let signed_headers = "content-type;host";
let canonical_headers = format!("host:{host}");
let signed_headers = "host";
// HTTP_VERB
// PATH_TO_RESOURCE
// CANONICAL_QUERY_STRING
// CANONICAL_HEADERS
// \n
// SIGNED_HEADERS
// PAYLOAD
let canonical_request = format!(
"GET\n{canonical_uri}\n{canonical_query_string}\n{canonical_headers}\n\n{signed_headers}\nUNSIGNED-PAYLOAD"
);
println!("canonical_request: {}", canonical_request);
let mut hasher = Sha256::new();
hasher.update(canonical_request);
let hashed_canonical_request = format!("{:x}", hasher.finalize());
let string_to_sign =
format!("{algorithm}\n{time_stamp}\n{credential_scope}\n{hashed_canonical_request}");
println!("string_to_sign: {}", string_to_sign);
let body = SignRequest {
delegates: vec![],
payload: base64::encode(string_to_sign),
};
let client = reqwest::Client::new();
let response = client
.post(sign_blob_url)
.bearer_auth(token.as_str())
.json(&body)
.send()
.await?
.bytes()
.await?;
dbg!(&response);
let sign_response: SignResponse = serde_json::from_slice(&response).unwrap();
let signed_url = format!(
"https://{host}{canonical_uri}?x-goog-signature={}&{canonical_query_string}",
// percent_encode(response.signed_blob.as_bytes(), FRAGMENT)
sign_response.signed_blob
);
Ok(signed_url)
}
First of all big thanks to John Hanley for motivating me to resolve this.
As he suggested there were multiple issues.
The percent encoding was incorrect and comparing it with the CLI output helped me to correct it.
The headers should be sorted and the same technique of comparing also helped me to find the right sorting.
The major issue was something else though.
It is my mistake that I've missed this in the docs.
In the doc of signedBlob it clearly states that the signature in the response is a Base64 encoded string. I was using it plainly instead.
After receiving the response doing,
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignResponse {
key_id: String,
signed_blob: String,
}
let response: SignResponse = serde_json::from_slice(&response).unwrap();
// First decode the signature
let decoded_signature = base64::decode(response.signed_blob).unwrap();
// Then make a string with hex representation of bytes
let hex_encoded_signature = hex::encode(decoded_signature);
// Then construct the url
let signed_url = format!("https://{host}{canonical_uri}?x-goog-signature={hex_encoded_signature}&{canonical_query_string}");
resolved the issue and I can even use the resulting URL in the browser!
By experimentation I've also derived that using lowercase (x-goog-..) or uppercase (X-Goog..) headers or even mixing them do not change anything.
This comment also confused me a little throughout the process:
When defining the resource path, you must percent encode the following reserved characters: ?=!#$&'()*+,:;#[]." Any other percent encoding used in the URL should also be included in the resource path.
which is taken from this source
Because it says to percent encode that character set but does not include characters such as /, etc. which should be percent encoded also and such as . shouldn't be encoded.
I am still confused about it a little but maybe I should read more about percent encoding techniques.
Anyway I'm happy that it is resolved and hope it helps someone later.

Basic Auth not working in Alamofire Swift

I am trying to get a Client Credential token, which is needed for Spotify's public Web API to search for public tracks.
It is extremely simple to get a token using postman.
REQUEST BODY PARAMETER: VALUE: grant_type Set it to client_credentials.
The header of this POST request must contain the following parameter:
HEADER PARAMETER: VALUE: Authorization Base 64 encoded string that contains the client ID and client secret key. The field must have the format: Authorization: Basic <base64 encoded client_id:client_secret>
I need to get a Spotify token in Swift. I started with this decodable struct:
struct SpotifyAuth: Decodable {
var access_token: String
var expires_in: Int
}
I have then tried dozens of variations of the following code to no avail:
let headers : HTTPHeaders = ["Content-Type":"application/x-www-form-urlencoded"]
let paramaters : Parameters = ["grant_type": "client_credentials", ]
AF.request("https://accounts.spotify.com/api/token", method: .post, parameters: paramaters, encoding: URLEncoding.httpBody, headers: headers ).authenticate(username: "{clientID}", password:"{clientSecrent}").responseJSON { response in
if response.error == nil {
do {
let spotifyAuth = try JSONDecoder().decode(SpotifyAuth.self, from: response.data!)
completion(.success(spotifyAuth))
} catch let error {
completion(.failure(error))
}
} else {
completion(.failure(response.error!))
}
}
Is anyone aware of what I am doing wrong/the correct way of acquiring a simple token from Spotify?
authenticate() is used to respond to authentication challenges. It does not unconditionally add the header. If the server does not respond with a Basic challenge you'll need to add the header yourself. Alamofire has a convenience method to help: HTTPHeader.authorization(username:password:). This will properly generate the Basic header for you.

Twitter REST API oauth '215 Bad Authentication data' using fetch in react-native

I'm trying to build a react native app to show a user's twitter timeline, but can't manage to access the twitter REST api because I am getting a
215 Bad Authentication data
error.
I've signed in the user correctly, gotten all the access tokens, and using fetch to make a request.
I've also verified that all my keys and tokens are correct, yet I still can't manage to figure out why I'm getting this error, my code is available below;
Can anyone please advise me on how to debug this or tell me what's wrong with my code?
thanks.
code:
let header = this._buildRequestHeader(twitter_token, twitter_tokenSecret);
console.log(header);
fetch('https://api.twitter.com/1.1/statuses/home_timeline.json', {
method: 'GET',
headers: {
'Accept': '*/*',
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': ' '+header
}
}).then((response) => response.json())
.then((json) => {
console.log(json);
})
_getBaseString:
_getBaseString(method, url, parameter_string){
return method+'&'+
encodeURIComponent(url)+'&'+encodeURIComponent(parameter_string);
}
_getSignature:
_getSignature(user_auth_token, accesstoken_secret, data){
// let signing_key = encodeURIComponent(Constants.TWITTER_CONSUMER_SECRET)+'&'+
// encodeURIComponent(Constants.ACCESS_TOKEN_SECRET);
let signing_key = encodeURIComponent(Constants.TWITTER_CONSUMER_SECRET)+'&'+
encodeURIComponent(accesstoken_secret);
console.log('signing data');
console.log(data);
return this.b64EncodeUnicode(hmacsha1(signing_key, data));
}
_buildRequestHeader:
_buildRequestHeader(user_auth_token, accesstoken_secret){
// https://dev.twitter.com/oauth/overview/creating-signatures
// https://dev.twitter.com/oauth/overview/authorizing-requests
let include_entities_key = encodeURIComponent('include_entities');
let include_entities_val = encodeURIComponent('false');
let oauth_consumer_key_key = encodeURIComponent('oauth_consumer_key');
let oauth_consumer_key_val = encodeURIComponent(Constants.TWITTER_COMSUMER_KEY);
let oauth_nonce_key = encodeURIComponent('oauth_nonce');
let oauth_nonce_val = encodeURIComponent(this._getNonce());
let oauth_signature_method_key = encodeURIComponent('oauth_signature_method');
let oauth_signature_method_val = encodeURIComponent('HMAC-SHA1');
let oauth_timestamp_key = encodeURIComponent('oauth_timestamp');
var val = Date.now() / 1000;
console.log(val);
console.log('parse'+parseInt(val));
let oauth_timestamp_val = encodeURIComponent(parseInt(val));
let oauth_token_key = encodeURIComponent('oauth_token');
// let oauth_token_val = encodeURIComponent(Constants.ACCESS_TOKEN);
let oauth_token_val = encodeURIComponent(user_auth_token);
let oauth_version_key = encodeURIComponent('oauth_version');
let oauth_version_val = encodeURIComponent('1.0');
// let parameter_string = include_entities_key+'='+include_entities_val+'&'+
let parameter_string = oauth_consumer_key_key+'='+oauth_consumer_key_val+'&'+
oauth_nonce_key+'='+oauth_nonce_val+'&'+
oauth_signature_method_key+'='+oauth_signature_method_val+'&'+
oauth_timestamp_key+'='+oauth_timestamp_val+'&'+
oauth_token_key+'='+oauth_token_val+'&'+
oauth_version_key+'='+oauth_version_val;
let data = this._getBaseString('GET', 'https://api.twitter.com/1.1/statuses/home_timeline.json',
parameter_string);
let signature = this._getSignature(user_auth_token, accesstoken_secret, data)
console.log('signature'+signature);
// 1499887682711
// 1318622958
let oauth_signature_key = encodeURIComponent('oauth_signature');
let oauth_signature_val = encodeURIComponent(signature);
let request_header_string = 'OAuth '+
oauth_consumer_key_key+'="'+oauth_consumer_key_val+'", '+
oauth_nonce_key+'="'+oauth_nonce_val+'", '+
oauth_signature_key+'="'+oauth_signature_val+'", '+
oauth_signature_method_key+'="'+oauth_signature_method_val+'", '+
oauth_timestamp_key+'="'+oauth_timestamp_val+'", '+
oauth_token_key+'="'+oauth_token_val+'", '+
oauth_version_key+'="'+oauth_version_val+'"';
return request_header_string;
}
But I get a 215 Bad Authentication data. Can someone please advise?
Have You try to use this package?
https://www.npmjs.com/package/react-native-twitter
or
https://github.com/GoldenOwlAsia/react-native-twitter-signin
let me know did it help.
Best Regards.
Maciej Adamczewski
I figured out what the problem was: The HMAC-SHA1 functionI was using was already doing the base-64 encoding for me.
and as Maciej Adamczewski pointed out, there was an unnecessary whitespace in the header string

Cloudant function-clause error at HTTP GET request

This is my first question here and I have not much experience in coding so please bear with me. Thanks!
I defined some documents in my Bluemix Cloudant account with different cars which have different characteristics. I want to get one entry from an IOS Swift front-end App.
This is an example query url:
https://$ACCOUNT-bluemix.cloudant.com/cars/_design/car_index/_search/car_index_name?q=size:small
Now the problem: If I use this url in a browser I get the correct results in JSON format back without any error. But if the app makes the request a function-clause error is logged while the request itself seems to be successful.
I read that a function_clause error is caused by some bug in the Javascript Cloudant uses for indexing the documents. The Javascript I'm using is exactely the same as Cloudant states it in the tutorials.
Has anyone an idea why it works in the browser but not in the App?
Thank you very much for any help!
Here is all the code:
This is the method I use in swift to make the request:
func databaseRequest(size: String, interior: String, fuel: String) {
let baseURL = "https://$ACCOUNT-bluemix.cloudant.com/cars/_design/car_index/_search/car_index_name?q="
let queryURL = "size:\(size)"
let completeURL: String = baseURL + queryURL
let completeURLModified = completeURL.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
let requestURL = URL(string: completeURLModified!)
var request = URLRequest(url: requestURL!)
request.httpMethod = "GET"
request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request){data, response, error in
guard error == nil else{
print("There was an error:", error as Any)
return
}
guard data == data else{
print("Data is empty")
return
}
let jsonResponse = try! JSONSerialization.jsonObject(with: data!, options: [])
print("This is JSON Response", jsonResponse)
}; task.resume()
}
This is the response from the JSON answer:
This is JSON Response {
error = "unknown_error";
reason = "function_clause";
ref = 1944801346;
}
The rest of log from http headers if this is helpful:
Optional(<NSHTTPURLResponse: 0x6080000349c0> { URL: https://$ACCOUNT-bluemix.cloudant.com/cars/_design/car_index/_search/car_index_name?q=size:small } { status code: 500, headers {
"Cache-Control" = "must-revalidate";
"Content-Length" = 70;
"Content-Type" = "application/json";
Date = "Thu, 24 Nov 2016 04:41:03 GMT";
Server = "CouchDB/2.0.0 (Erlang OTP/17)";
"Strict-Transport-Security" = "max-age=31536000";
Via = "1.1 lb1.bm-cc-dal-01 (Glum/1.31.3)";
"X-Cloudant-Backend" = "bm-cc-dal-01";
"X-Content-Type-Options" = nosniff;
"X-Couch-Request-ID" = 51e5e0b5e1;
"X-Couch-Stack-Hash" = 1944801346;
"X-CouchDB-Body-Time" = 0;
Last but not least the Javascript file I use as Index in the design document in Cloudant:
function (doc) {
index("name", doc.name, {"store": true});
if (doc.fuel){ index("fuel", doc.fuel, {"store": true});}
if (doc.interior){ index("interior", doc.interior, {"store": true});}
if (doc.size){index("size", doc.size, {"store": true});
}}
I think this error is due to cloudant trying to decode whatever you passed as \(credentials) as a base64 encoded string. If \(credentials) is not a valid base64 encoded string (e.g. contains characters other than a-z, A-Z, 0-9, +, / and =), then my guess is that cloudant's base64 decoding function fails with the above error.
You need to make sure that \(credentials) is the string <your_username>:<your_password> encoded correctly. E.g. if your username is john and your password is doe, then \(credentials) should be am9objpkb2U=.

Kraken private API with F# returns EGeneral: invalid arguments

I am trying to access the Kraken private API using F#. The code to access the public API runs perfectly fine, but when i try to access the private API i am always getting the error "EGeneral:Invalid arguments".
#r "FSharp.Data.dll"
open FSharp.Data
open System
open System.Text
open System.Security.Cryptography
let baseUri = "https://api.kraken.com"
let key = MY_KRAKEN_API_KEY
let secret = MY_KRAKEN_API_SECRET
let path = "/0/private/Balance"
let nonce = DateTime.UtcNow.Ticks
let bodyText = "nonce=" + nonce.ToString()
let hmac (key : byte []) (data : byte[]) =
use hmac = new HMACSHA512(key)
hmac.ComputeHash(data)
let sha256 (data : string) =
use sha = SHA256Managed.Create()
sha.ComputeHash(Encoding.UTF8.GetBytes(data))
let createSignature (nonce : int64) body (path : string) secret =
let shaSum = nonce.ToString() + body |> sha256
let data = Array.append (Encoding.UTF8.GetBytes path) shaSum
let key = Convert.FromBase64String secret
hmac key data |> Convert.ToBase64String
let signature = createSignature nonce bodyText path secret
let response = Http.RequestString (
url = baseUri + path,
httpMethod = "POST",
headers = ([("API-Key", key); ("API-Sign", signature)] |> Seq.ofList),
body = TextRequest bodyText
)
Does anybody see what i am doing wrong?
EDIT:
The Kraken.com API documentation is awailable here: https://www.kraken.com/help/api
I suppose the header signature is incorrect. The docu requires the following two values to be submitted in the header:
API-Key = API key API-Sign = Message signature using HMAC-SHA512 of
(URI path + SHA256(nonce + POST data)) and base64 decoded secret API
key
EDIT 2:
The remaining parameters need to be transmitted with a POST method. In my case this is only the "nonce" value in the body part of the HTTP request.
I had the same error while I was writing a C# library for Kraken and I found a solution of this problem:
This error does not appears if the API key or the sign are wrong or missing. The problem is that you do not add a mediatype to your request. I do not know how it works in F# but look at this example:
using (var client = new HttpClient())
{
string address = String.Format("{0}/{1}/public/{2}", _url, _version, method);
// Does not work with this:
// var content = new StringContent(postData, Encoding.UTF8);
var content = new StringContent(postData, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = await client.PostAsync(address, content);
return await response.Content.ReadAsStringAsync();
}
The "application/x-www-form-urlencoded" is the critical path. If you do not send a request with that, you get the "EGeneral: invalid arguments"-error. With it, everything works fine. At least in my case.