Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement apple token based push notifications (using p8 file) in C#?

For an app with some kind of chat based features I want to add push notification support for receiving new messages. What I want to do is use the new token based authentication (.p8 file) from Apple, but I can't find much info about the server part.

I came across the following post: How to use APNs Auth Key (.p8 file) in C#?

However the answer was not satisfying as there was not much detail about how to:

  • establish a connection with APNs
  • use the p8 file (except for some kind of encoding)
  • send data to the Apple Push Notification Service
like image 587
J. Rahmati Avatar asked May 09 '17 12:05

J. Rahmati


1 Answers

Because Token (.p8) APNs only works in HTTP/2, thus most of the solutions only work in .net Core. Since my project is using .net Framework, some tweak is needed. If you're using .net Framework like me, please read on.

I search here and there and encountered several issues, which I managed to fix and pieced them together.

Below is the APNs class that actually works. I created a new class library for it, and placed the .P8 files within the AuthKeys folder of the class library. REMEMBER to right click on the .P8 files and set it to "Always Copy". Refer Get relative file path in a class library project that is being referenced by a web project.

After that, to get the location of the P8 files, please use AppDomain.CurrentDomain.RelativeSearchPath for web project or AppDomain.CurrentDomain.BaseDirectory for win application. Refer Why AppDomain.CurrentDomain.BaseDirectory not contains "bin" in asp.net app?

To get the token from the P8, you'll need to use the BouncyCastle class, please download it from Nuget.

using Jose;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Security.Cryptography;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

    namespace PushLibrary
    {
        public class ApplePushNotificationPush
        {
            //private const string WEB_ADDRESS = "https://api.sandbox.push.apple.com:443/3/device/{0}";
            private const string WEB_ADDRESS = "https://api.push.apple.com:443/3/device/{0}";
    
            private string P8_PATH = AppDomain.CurrentDomain.RelativeSearchPath + @"\AuthKeys\APNs_AuthKey.p8";
    
            public ApplePushNotificationPush()
            {
    
            }
    
            public async Task<bool> SendNotification(string deviceToken, string title, string content, int badge = 0, List<Tuple<string, string>> parameters = null)
            {
                bool success = true;
    
                try
                {
                    string data = System.IO.File.ReadAllText(P8_PATH);
                    List<string> list = data.Split('\n').ToList();
    
                    parameters = parameters ?? new List<Tuple<string, string>>();
    
                    string prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
                    ECDsaCng key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
    
                    string token = GetProviderToken();
    
                    string url = string.Format(WEB_ADDRESS, deviceToken);
                    HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url);
    
                    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
    
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-push-type", "alert"); // or background
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
                    //Expiry
                    //
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
                    //Send imediately
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
                    //App Bundle
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-topic", "com.xxx.yyy");
                    //Category
                    httpRequestMessage.Headers.TryAddWithoutValidation("apns-collapse-id", "test");
    
                    //
                    var body = JsonConvert.SerializeObject(new
                    {
                        aps = new
                        {
                            alert = new
                            {
                                title = title,
                                body = content,
                                time = DateTime.Now.ToString()
                            },
                            badge = 1,
                            sound = "default"
                        },
                        acme2 = new string[] { "bang", "whiz" }
                    });
    
                    httpRequestMessage.Version = new Version(2, 0);
    
                    using (var stringContent = new StringContent(body, Encoding.UTF8, "application/json"))
                    {
                        //Set Body
                        httpRequestMessage.Content = stringContent;
    
                        Http2Handler.Http2CustomHandler handler = new Http2Handler.Http2CustomHandler();
    
                        handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls;
    
                        //handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
    
                        //Continue
                        using (HttpClient client = new HttpClient(handler))
                        {
                            HttpResponseMessage resp = await client.SendAsync(httpRequestMessage).ContinueWith(responseTask =>
                            {
                                return responseTask.Result;
                            });
    
                            if (resp != null)
                            {
                                string apnsResponseString = await resp.Content.ReadAsStringAsync();
    
                                handler.Dispose();
                            }
    
                            handler.Dispose();
                        }
                    }
                }
                catch (Exception ex)
                {
                    success = false;
                }
    
                return success;
            }
    
            private string GetProviderToken()
            {
                double epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
                Dictionary<string, object> payload = new Dictionary<string, object>()
                {
                    { "iss", "YOUR APPLE TEAM ID" },
                    { "iat", epochNow }
                };
                var extraHeaders = new Dictionary<string, object>()
                {
                    { "kid", "YOUR AUTH KEY ID" },
                    { "alg", "ES256" }
                };
    
                CngKey privateKey = GetPrivateKey();
    
                return JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeaders);
            }
    
            private CngKey GetPrivateKey()
            {
                using (var reader = File.OpenText(P8_PATH))
                {
                    ECPrivateKeyParameters ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
    
                    var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
                    var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
    
                    var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
    
                    return EccKey.New(x, y, d);
                }
            }
        }
    }

Secondly, if you noticed, I am using the custom WinHTTPHandler to make the code to support HTTP/2 based on How to make the .net HttpClient use http 2.0?. I am creating this using another class library, remember to download WinHTTPHandler from Nuget.

    public class Http2CustomHandler : WinHttpHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            request.Version = new Version("2.0");

            return base.SendAsync(request, cancellationToken);
        }
    }

After that, just call the "SendNotification" on the ApplePushNotificationPush class and you should get the message on your iPhone.

like image 134
TPG Avatar answered Sep 22 '22 06:09

TPG