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

5

13

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
Forgat answered 9/5, 2017 at 12:46 Comment(0)
H
16

You can't really do this on raw .NET Framework at the moment. The new JWT-based APNS server uses HTTP/2 only, which .NET Framework does not yet support.

.NET Core's version of System.Net.Http, however, does, provided you meet the following prerequisites:

  • On Windows, you must be running Windows 10 Anniversary Edition (v1607) or higher, or the equivalent build of Windows Server 2016 (I think).
  • On Linux, you must have a version of libcurl that supports HTTP/2.
  • On macOS, you have to compile libcurl with support for HTTP/2, then use the DYLD_INSERT_LIBRARIES environment variable in order to load your custom build of libcurl.

You should be able to use .NET Core's version of System.Net.Http in the .NET Framework if you really want.

I have no idea what happens on Mono, Xamarin or UWP.

There are then three things you have to do:

  1. Parse the private key that you have been given. This is currently an ECDSA key, and you can load this into a System.Security.Cryptography.ECDsa object.
  • On Windows, you can use the CNG APIs. After parsing the base64-encoded DER part of the key file, you can then create a key with new ECDsaCng(CngKey.Import(data, CngKeyBlobFormat.Pkcs8PrivateBlob)).
  • On macOS or Linux there is no supported API and you have to parse the DER structure yourself, or use a third-party library.
  1. Create a JSON Web Token / Bearer Token. If you use the System.IdentityModel.Tokens.Jwt package from NuGet, this is fairly simple. You will need the Key ID and Team ID from Apple.
public static string CreateToken(ECDsa key, string keyID, string teamID)
{
    var securityKey = new ECDsaSecurityKey(key) { KeyId = keyID };
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

    var descriptor = new SecurityTokenDescriptor
    {
          IssuedAt = DateTime.Now,
          Issuer = teamID,
          SigningCredentials = credentials
    };

    var handler = new JwtSecurityTokenHandler();
    var encodedToken = handler.CreateEncodedJwt(descriptor);
    return encodedToken;
}
  1. Send an HTTP/2 request. This is as normal, but you need to do two extra things:
  2. Set yourRequestMessage.Version to new Version(2, 0) in order to make the request using HTTP/2.
  3. Set yourRequestMessage.Headers.Authorization to new AuthenticationHeaderValue("bearer", token) in order to provide the bearer authentication token / JWT with your request.

Then just put your JSON into the HTTP request and POST it to the correct URL.

Hiroshige answered 1/2, 2018 at 12:52 Comment(3)
This works for us. Thanks! But IssuedAt = DateTime.Now is not a good idea. APN will throw 403 in this case. We have replaced it with NotBefore = DateTime.UtcNow.AddSeconds(-5) and it seems to help. See: #42576398Profitsharing
It probably worked for me because I'm UTC+10/11. :DHiroshige
Warning for those using shared environments. The call to CngKey.Import requires the user profile being loaded in IIS. This is usually not allowed in shared enviornments. gorniv's solution works around this issue with the caveat of taking an extra dependency. Anyone know of another way around this problem?Archil
Q
6
private string GetToken()
    {
        var dsa = GetECDsa();
        return CreateJwt(dsa, "keyId", "teamId");
    }
    
    private ECDsa GetECDsa()
    {
        using (TextReader reader = System.IO.File.OpenText("AuthKey_xxxxxxx.p8"))
        {
        var ecPrivateKeyParameters =
            (ECPrivateKeyParameters)new Org.BouncyCastle.OpenSsl.PemReader(reader).ReadObject();

        var q = ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D).Normalize();
        var qx = q.AffineXCoord.GetEncoded();
        var qy = q.AffineYCoord.GetEncoded();
        var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();

        // Convert the BouncyCastle key to a Native Key.
        var msEcp = new ECParameters {Curve = ECCurve.NamedCurves.nistP256, Q = {X = qx, Y = qy}, D = d};
        return ECDsa.Create(msEcp);
        }
    }
    
    private string CreateJwt(ECDsa key, string keyId, string teamId)
    {
        var securityKey = new ECDsaSecurityKey(key) { KeyId = keyId };
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

        var descriptor = new SecurityTokenDescriptor
        {
            IssuedAt = DateTime.Now,
            Issuer = teamId,
            SigningCredentials = credentials,
            
        };

        var handler = new JwtSecurityTokenHandler();
        var encodedToken = handler.CreateEncodedJwt(descriptor);
        return encodedToken;
    }
Quesada answered 26/12, 2018 at 8:23 Comment(2)
Great solution for token creation. Bourne Koloh answer will do the rest. Thanks guys!!! .... saved my day!Cluny
Great solution. I think you can release a library open source for it! Love guys!!!Stalker
S
6

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.

Stepaniestepbrother answered 28/8, 2020 at 3:58 Comment(1)
Thank you for this - best example I've seen so far for the .NET Framework. It would be helpful to mention this requires the Jose-JWT package as well.Katricekatrina
G
3

It have tried the above on ASP.NET CORE 2.1 and 2.2 to no avail. The response I always got was "The message received was unexpected or badly formatted" with HttpVersion20 enabled, which made me doubt whether http2 implementation is concrete.

Below is what worked on ASP.NET CORE 3.0;

 var teamId = "YOURTEAMID";
 var keyId = "YOURKEYID";

            try
            {
                //
                var data = await System.IO.File.ReadAllTextAsync(Path.Combine(_environment.ContentRootPath, "apns/"+config.P8FileName));
                var list = data.Split('\n').ToList();
                var prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
                //
                var key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
                //
                var token = CreateToken(key, keyId, teamId);
                //
                var deviceToken = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
                var url = string.Format("https://api.sandbox.push.apple.com/3/device/{0}", deviceToken);
                var request = new HttpRequestMessage(HttpMethod.Post, url);
                //
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
                //

                request.Headers.TryAddWithoutValidation("apns-push-type", "alert"); // or background
                request.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
                //Expiry
                //
                request.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
                //Send imediately
                request.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
                //App Bundle
                request.Headers.TryAddWithoutValidation("apns-topic", "com.xx.yy");
                //Category
                request.Headers.TryAddWithoutValidation("apns-collapse-id", "test");

                //
                var body = JsonConvert.SerializeObject(new
                {
                    aps = new
                    {
                        alert = new
                        {
                            title = "Test",
                            body = "Sample Test APNS",
                            time = DateTime.Now.ToString()
                        },
                        badge = 1,
                        sound = "default"
                    },
                    acme2 = new string[] { "bang", "whiz" }
                })
                //
                request.Version = HttpVersion.Version20;
                //
                using (var stringContent = new StringContent(body, Encoding.UTF8, "application/json"))
                {
                    //Set Body
                    request.Content = stringContent;
                    _logger.LogInformation(request.ToString());
                    //
                    var handler = new HttpClientHandler();
                    //
                    handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
                    //
                    handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;

                    //Continue
                    using (HttpClient client = new HttpClient(handler))
                    {
                        //
                        HttpResponseMessage resp = await client.SendAsync(request).ContinueWith(responseTask =>
                        {
                            return responseTask.Result;
                            //

                        });
                        //
                        _logger.LogInformation(resp.ToString());
                        //
                        if (resp != null)
                        {
                            string apnsResponseString = await resp.Content.ReadAsStringAsync();
                            //
                            handler.Dispose();
                            //ALL GOOD ....
                            return;
                        }
                        //
                        handler.Dispose();
                    }
                }
            }
            catch (HttpRequestException e)
            {
                _logger.LogError(5, e.StackTrace, e);
            }

For CreateToken() Refer Above Recommended solution by yaakov,

Gorcock answered 4/10, 2019 at 13:36 Comment(3)
what operating system are you running this on?Godson
This is on Windows 10 - 1703 buildGorcock
If someone is trying this to run on AWS lambda, the EDsCNG would not work since it's Windows specific. Need to use @gorniv's asnwer to generate using Bouncy Castle.Puttyroot
S
0

I has a problem like you. And i seen @gorniv answer. So it's work with me!

May be you can use: https://www.nuget.org/packages/Apple.Auth.Signin for it!

Goodluck!

Stalker answered 21/11, 2020 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.