How do I connect to the Apple News API from c#?

515 Views Asked by At

I'm trying to connect to Apple's News API. When generating the signature the results all appear the same from each of the different examples. However, I keep getting this error:

 {"errors":[{"code":"WRONG_SIGNATURE"}]}

Here is my c# code to generate the auth header.

public class Security
{
  public static string AuthHeader(string method, string url, object content=null)
  {
    var apiKeyId = "<YOUR KEY HERE...>"; //we get this value from our settings file.
    var apiKeySecret = "<YOUR SECRET HERE...>"; //we get this value from our settings file.
    if ( string.IsNullOrEmpty(apiKeyId) || string.IsNullOrEmpty(apiKeySecret)) return string.Empty;
    var encoding = new ASCIIEncoding();
    var dt = DateTime.Now.ToString(Constants.DateFormat);
    var canonicalRequest = string.Format("{0}{1}{2}", method, url, dt);
    var key = Convert.FromBase64String(apiKeySecret);
    var hmac = new HMACSHA256(key);
    var hashed = hmac.ComputeHash(encoding.GetBytes(canonicalRequest));
    var signature = Convert.ToBase64String(hashed);
    var authorizaton = string.Format(@"HHMAC; key={0}; signature={1}; date={2}", apiKeyId, signature, dt);
    return authorizaton;
  }
}

Short version of Constants class

public static class Constants
{
  public static readonly string ChannelId = "<YOUR CHANNEL ID HERE...>"; //again from our settings file
  public static readonly string BaseUrl = "https://news-api.apple.com";
  public static readonly string DateFormat = "yyyy-MM-ddTHH:mm:ssK";    
}

Short version of Actions class (SendCommand is the method that performs the request)

public class Actions
{
  public static string SendCommand(string action, string method)
  {
    var url = $"{Constants.BaseUrl}{action}";      
    var authheader = Security.AuthHeader(method, url, null);
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = method;
    request.Timeout = 1000;
    request.Headers.Add("Authorization", authheader);
    request.Accept = "application/json";
    request.ContentType = "application/json";
    var output = string.Empty;
    try
    {
      using (var response = request.GetResponse())
      {
        using (var reader = new StreamReader(response.GetResponseStream()))
          output = reader.ReadToEnd();
      }
    }
    catch (WebException e)
    {
      using (var reader = new StreamReader(e.Response.GetResponseStream()))
      {
        output = reader.ReadToEnd();
      }
    }          
    return output;
  }

  public static string ReadChannel()
  {
    var action = $"/channels/{Constants.ChannelId}";
    const string method = "GET";
    return SendCommand(action, method);
  }
}

I'm using the ReadChannel method for testing.

I've also tried examples in php and ruby with no luck.

Any ideas how to do this correctly?

2

There are 2 best solutions below

0
On BEST ANSWER

Pasted the authorization string generated from the original code on this post into fiddler and I was able to get a successful response from the Apple News API. It seems like HttpWebRequest isn't including the Authorization header correctly and submitting the same request with the property PreAuthenticate = true corrects this issue (HttpWebRequest.PreAuthenticate). Also, with a GET request the ContentType needs to be omitted so I've added a conditional statement to account for this too.

public class Actions
{
  public static string SendCommand(string action, string method)
  {
    var url = $"{Constants.BaseUrl}{action}";      
    var authheader = Security.AuthHeader(method, url, null);
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = method;
    request.Timeout = 1000;
    request.PreAuthenticate = true;
    request.Headers.Add("Authorization", authheader);
    request.Accept = "application/json";
    if(method.Equals("post", StringComparison.InvariantCultureIgnoreCase)) 
       request.ContentType = "application/json";
     var output = string.Empty;
    try
    {
      using (var response = request.GetResponse())
      {
        using (var reader = new StreamReader(response.GetResponseStream()))
          output = reader.ReadToEnd();
      }
    }
    catch (WebException e)
    {
      using (var reader = new StreamReader(e.Response.GetResponseStream()))
      {
        output = reader.ReadToEnd();
      }
    }          
    return output;
  }

  public static string ReadChannel()
  {
    var action = $"/channels/{Constants.ChannelId}";
    const string method = "GET";
    return SendCommand(action, method);
  }
}
3
On

The error complains something is wrong in how we compute the signature. Let's take a look at the Apple's example code to produces a correct signature, which is here:

https://developer.apple.com/documentation/apple_news/apple_news_api/about_the_news_security_model

Unfortunately I only found Python code. I don't know Python, either, but I can figure out enough to adapt it to just show the signature. We'll also need to know exactly what date value was used.

import base64
from hashlib import sha256
import hmac
from datetime import datetime

channel_id = 'cdb737aa-FFFF-FFFF-FFFF-FFFFFFFFFFFF'
api_key_id = '240ab880-FFFF-FFFF-FFFF-FFFFFFFFFFFF'
api_key_secret = 'HgyfMPjFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF='
url = 'https://news-api.apple.com/channels/%s' % channel_id
date = str(datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
canonical_request = 'GET' + url + date
key = base64.b64decode(api_key_secret)
hashed = hmac.new(key, canonical_request, sha256)
signature = hashed.digest().encode("base64").rstrip('\n')
print date
print signature

You can see the result here (the short link points to tutorialspoint.com, which was the first online python interpreter I found in Google):

http://tpcg.io/e1W4p1

If you don't trust the link, just know I was able to use it to figure out the following known-correct signature based on these known inputs:

Method: GET
URL: https://news-api.apple.com/channels/cdb737aa-FFFF-FFFF-FFFF-FFFFFFFFFFFF
DateTime: 2018-06-12T18:15:45Z
API Secret: HgyfMPjFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=
Signature: f3cOzwH7HGYPg481noBFwKgVOGAhH3jy7LQ75jVignA=

Now we can write C# code we can verity. When we can use those same inputs to produce the same signature result, we will have correct code.

Based on that, I was able to write this C# code:

public static void Main()
{
    string method = "GET";
    string url = "https://news-api.apple.com/channels/cdb737aa-FFFF-FFFF-FFFF-FFFFFFFFFFFF";
    string dateString = "2018-06-12T18:15:45Z";
    string apiKeySecret = "HgyfMPjFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=";

    var MyResult = GetSignature(method, url, dateString, apiKeySecret);
    var DocumentedResult = "f3cOzwH7HGYPg481noBFwKgVOGAhH3jy7LQ75jVignA=";

    Console.WriteLine(MyResult);
    Console.WriteLine(MyResult == DocumentedResult);
}

public static string GetSignature(string method, string url, string dt, string APISecret)
{
    var hmac = new HMACSHA256(Convert.FromBase64String(APISecret));
    var hashed = hmac.ComputeHash(Encoding.ASCII.GetBytes(method + url + dt));

    return Convert.ToBase64String(hashed);
}

Which you can see in action here:

https://dotnetfiddle.net/PQ73Zv

I don't have my own Apple API key to test with, so that's as far as I can take you.

One thing I did notice from the question is Apple's example has a "Z" at the end of the date string which is missing with original code here.