.NET Core 2.1 Apple Push Notifications
Asked Answered
G

3

1

I have to send push notifications to specific iOS devices with my .Net Core WebAPI that will be executed on a Windows 2008 Server R2. The server itself should not be the problem because it is working with a node.js library. But I want it to work with an WepAPI in ASP .Net Core 2.1 which is self hosted with the inbuilt Kestrel Server. Maybe you've got an idea how to solve this problem.

My Code:

// This will encode the jason web token apns needs for the authorization
// get the base64 private key of the .p8 file from apple
string p8File = System.IO.File.ReadAllText(Settings.Apn.PrivateKey);
p8File = p8File.Replace("-----BEGIN PRIVATE KEY-----", string.Empty);
p8File = p8File.Replace("-----END PRIVATE KEY-----", string.Empty);
p8File = p8File.Replace(" ", string.Empty);

byte[] keyData = Convert.FromBase64String(p8File);
ECDsa key = new ECDsaCng(CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob));

ECDsaSecurityKey securityKey = new ECDsaSecurityKey(key) { KeyId = Settings.Apn.KeyId };
SigningCredentials credentials = new SigningCredentials(securityKey, "ES256");

SecurityTokenDescriptor descriptor =
    new SecurityTokenDescriptor
        {
            IssuedAt = DateTime.Now,
            Issuer = Settings.Apn.TeamId,
            SigningCredentials = credentials
        };

JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
string encodedToken = jwtHandler.CreateEncodedJwt(descriptor);
this.log?.LogInformation($"Created JWT: {encodedToken}");

// The hostname is: https://api.development.push.apple.com:443
HttpClient client = new HttpClient { BaseAddress = new Uri(Settings.Apn.Hostname) };
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
this.log?.LogInformation("Initialized new HttpClient.");

// payload content for the apns
JObject payloadData = new JObject
                          {
                              new JProperty("alert", data.Message),
                              new JProperty("badge", 2),
                              new JProperty("sound", "default")
                          };
JObject payload = new JObject
                       {
                           new JProperty("aps", payloadData)
                       };
this.log?.LogInformation($"Setup payload: {payload}");

// HttpRequestMessage that should be send
HttpRequestMessage request = new HttpRequestMessage(
                                 HttpMethod.Post,
                                 $"{Settings.Apn.Hostname}/3/device/{data.DeviceId}")
                                 {
                                     Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json")
                                 };
this.log?.LogInformation("Setup HttpRequestMessage.");

// Setup the header
request.Headers.Add("Authorization", $"Bearer {encodedToken}");
request.Headers.Add("apns-id", Guid.NewGuid().ToString());
request.Headers.Add("apns-expiration", DateTime.Now.AddDays(1).ToString(CultureInfo.InvariantCulture));
request.Headers.Add("apns-priority", "10");
request.Headers.Add("apns-topic", "de.gefasoft-engineering.FabChat");

// Debug logging
this.log.LogDebug(request.ToString());
this.log.LogDebug(await request.Content.ReadAsStringAsync());
this.log.LogDebug(request.RequestUri.Host + request.RequestUri.Port);

// Send request
var result = await client.SendAsync(request);
this.log?.LogInformation("Sent request.");
this.log?.LogInformation(await result.Content.ReadAsStringAsync());

I always get following Exception thrown:

System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception. ---> System.ComponentModel.Win32Exception: The message received was unexpected or badly formatted --- End of inner exception stack trace ---

Germiston answered 19/11, 2018 at 16:27 Comment(2)
I am having a similar problem, working great in a node app but not dotnet core, have you found a solution?Histogenesis
@Histogenesis sorry for the late answer. I posted my code below!Germiston
B
2

Use CorePush lib

It's very lightweight. I use it across all my projects to send Firebase Android/WebPush and Apple iOS push notifications. Useful links:

  1. NuGet package
  2. Documentation

The interface is very simple and minimalistic:

Send APN message:

var apn = new ApnSender(settings, httpClient);
await apn.SendAsync(notification, deviceToken);

It can also send Android FCM message if needed:

var fcm = new FcmSender(settings, httpClient);
await fcm.SendAsync(deviceToken, notification);
Barye answered 26/2, 2021 at 3:37 Comment(1)
Please can you explain how can set the apnsettings and from where i get the info must i use it to pass it through the apnsender constractorPresentational
R
0

can you try adding version information to your request after the apns-topic line as below? It ran to completion and I got a "bad device token" error for the first time after adding the following line.

request.Version = new Version(2, 0);
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;

I saw the version setting command at the post below.

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

Raimundo answered 22/11, 2018 at 18:0 Comment(4)
I made it this way with a .p12 file and it works now. janaks.com.np/…Germiston
Sebi E, were you able to figure out how to send notification to production, to an app installed from app store? I could get the notification in debug mode, but when I installed the app from app store, it did not get the notication.Raimundo
civilator, I didn't test it, yet. But I think you need to create a new certificate for a published app on your apple developer account and send the request to "gateway.push.apple.com:2195" instead of "gateway.sandbox.push.apple.com:2195". Maybe this is helpful.Germiston
@Germiston you are using the old binary format, not the new http/2 based apiKries
G
0

I've already commented on the answer from @civilator. But I think, that some people read over it, so I'm posting it again. This is the code that worked for me. Sorry for the late answer!

private readonly string hostname = "gateway.sandbox.push.apple.com";
private readonly int port = 2195;

public async Task<RestResult<JObject>> SendPushNotification(string deviceToken, string message)
    {
        this.log?.LogInformation("Trying to send push notification.");
        X509Certificate2Collection certificatesCollection;

        // Setup and read the certificate
        // NOTE: You should get the certificate from your apple developer account.
        try
        {
            string certificatePath = Settings.Apn.Certificate;
            X509Certificate2 clientCertificate = new X509Certificate2(
                File.ReadAllBytes(certificatePath),
                Settings.Apn.Password);
            certificatesCollection = new X509Certificate2Collection(clientCertificate);
            this.log?.LogInformation("Setup certificates.");
        }
        catch (Exception e)
        {
            this.log?.LogError(e.ToString());
            return new RestResult<JObject> { Result = "exception", Message = "Failed to setup certificates." };
        }

        // Setup a tcp connection to the apns
        TcpClient client = new TcpClient(AddressFamily.InterNetwork);
        this.log?.LogInformation("Created new TcpClient.");
        try
        {
            IPHostEntry host = Dns.GetHostEntry(this.hostname);
            await client.ConnectAsync(host.AddressList[0], this.port);
            this.log?.LogInformation($"Opened connection to {this.hostname}:{this.port}.");
        }
        catch (Exception e)
        {
            this.log?.LogError("Failed to open tcp connection to the apns.");
            this.log?.LogError(e.ToString());
        }

        // Validate the Certificate you get from the APN (for more information read the documentation:
        // https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1).
        SslStream sslStream = new SslStream(
            client.GetStream(),
            false,
            new RemoteCertificateValidationCallback(this.ValidateServerCertificate),
            null);

        try
        {
            await sslStream.AuthenticateAsClientAsync(this.hostname, certificatesCollection, SslProtocols.Tls, false);
            MemoryStream memoryStream = new MemoryStream();
            BinaryWriter writer = new BinaryWriter(memoryStream);
            writer.Write((byte)0);
            writer.Write((byte)0);
            writer.Write((byte)32);

            writer.Write(HexStringToByteArray(deviceToken.ToUpper()));

            // Creating an payload object to send key values to the apns
            JObject aps = new JObject
                              {
                                  new JProperty("alert", message),
                                  new JProperty("badge", 0),
                                  new JProperty("sound", "default")
                              };

            JObject payload = new JObject
                                  {
                                    new JProperty("aps", aps)
                                  };

            string payloadString = JsonConvert.SerializeObject(payload);
            writer.Write((byte)0);
            writer.Write((byte)payloadString.Length);

            byte[] b1 = System.Text.Encoding.UTF8.GetBytes(payloadString);
            writer.Write(b1);
            writer.Flush();

            byte[] array = memoryStream.ToArray();
            sslStream.Write(array);
            sslStream.Flush();
            client.Dispose();
        }
        catch (AuthenticationException ex)
        {
            this.log?.LogError(ex.ToString());
            client.Dispose();
            return new RestResult<JObject> { Result = "exception", Message = "Authentication Exception." };
        }
        catch (Exception e)
        {
            this.log?.LogError(e.ToString());
            client.Dispose();
            return new RestResult<JObject> { Result = "exception", Message = "Exception was thrown." };
        }

        this.log?.LogInformation("Notification sent.");
        return new RestResult<JObject> { Result = "success", Message = "Notification sent. Check your device." };
    }

    #region Helper methods

    private static byte[] HexStringToByteArray(string hex)
    {
        return Enumerable.Range(0, hex.Length)
            .Where(x => x % 2 == 0)
            .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
            .ToArray();
    }

    // The following method is invoked by the RemoteCertificateValidationDelegate.
    private bool ValidateServerCertificate(
        object sender,
        X509Certificate certificate,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    {
        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            this.log?.LogInformation("Server Certificate validated.");
            return true;
        }

        this.log?.LogError($"Server Certificate error: {sslPolicyErrors}");

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

    #endregion
Germiston answered 16/9, 2019 at 9:0 Comment(1)
I use the same method to send a push notification to Apple devices. But nothing happens, I search for a solution in .NET but found always the same code. Has anyone a solution for me?Nainsook

© 2022 - 2024 — McMap. All rights reserved.