Setting up DeviceCheck on iOS is super easy, but implementing the server-side using C# is difficult as there are hardly any examples and some tricky JWT code is needed, which has to be absolutely perfect for it to work. Does anyone have a solution?
Here is a complete solution. I stripped out the first and last line from the p8 file e.g. "key begins here" (or whatever) and also took out all the new lines so that the key was just one long line
static void Main(string[] args)
{
string deviceToken = ""; //you get this from the device
string transcationId = Guid.NewGuid().ToString();
var payload = new Dictionary<string, object>() {
{ "device_token", deviceToken },
{ "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1000 },
{ "transaction_id", transcationId }
};
var token = GetProviderToken();
var payloadJson = JsonConvert.SerializeObject(payload);
using (var httpClient = new HttpClient())
{
using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.development.devicecheck.apple.com/v1/query_two_bits"))
{
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}");
request.Content = new StringContent(payloadJson);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
var response = httpClient.SendAsync(request).Result;
Console.WriteLine(response);
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
}
}
}
private static CngKey GetPrivateKey()
{
using (var reader = File.OpenText(#".\applekey.p8.txt"))
{
var 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);
}
}
private static string GetProviderToken()
{
var epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
var payload = new Dictionary<string, object>()
{
{"iss", "<10 DIGIT TEAM CODE FROM APPLE DEV CENTER>"},
{"iat", epochNow}
};
var extraHeaders = new Dictionary<string, object>()
{
{"kid", "<THE NAME OF THE P8 FILE, 10 DIGITS>"}
};
var privateKey = GetPrivateKey();
return JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeaders);
}
Related
I am attempting to upgrade to newest APNS connectivity functionality but when I submit a request I get a http 400 bad request result with no reason. What am I missing / doing wrong?
Method for sending below:
public async void SendAsync(string deviceToken, string p8privateKey, string p8privateKeyId, string teamId)
{
var path = $"/3/device/{deviceToken}";
var obj = new
{
aps = new
{
alert = "test00001"
}
};
var json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://api.development.push.apple.com:443" + path))
{
Version = new Version(2, 0)
};
string jwToken = CreateJwtToken(p8privateKey, p8privateKeyId, teamId);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwToken);
request.Headers.TryAddWithoutValidation(":method", "POST");
request.Headers.TryAddWithoutValidation(":path", path);
request.Headers.Add("apns-topic", "com.the-app");
request.Content = new StringContent(json);
string sReq = JsonConvert.SerializeObject(request);
string jsonContent = request.Content.ReadAsStringAsync().Result;
WinHttpHandler handler = new WinHttpHandler();
HttpClient http = new HttpClient(handler);
var t = await http.SendAsync(request);
}
Here are the other used methods:
private static string CreateJwtToken(string p8privateKeyId, string p8privateKey, string teamId)
{
var header = JsonHelper.Serialize(new { alg = "ES256", kid = p8privateKeyId });
var payload = JsonHelper.Serialize(new { iss = teamId, iat = ToEpoch(DateTime.UtcNow) });
var key = CngKey.Import(Convert.FromBase64String(p8privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
using (var dsa = new ECDsaCng(key))
{
dsa.HashAlgorithm = CngAlgorithm.Sha256;
var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
var signature = dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData));
return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
}
}
private static int ToEpoch(DateTime time)
{
var span = DateTime.UtcNow - new DateTime(1970, 1, 1);
return Convert.ToInt32(span.TotalSeconds);
}
I have limited to the payload to what I think is the bear minimum requirements. I am new to jwt/http2 and APNS.
I could very well be missing something simple.
I'm trying to create a web app (hosted on Azure) for clients to be able to submit work items to our team services page. Basically a support ticket page so they don't have to call to explain their backlog all the time.
Below is the class and method i've made to create work items, following Microsoft's sample code, with some obvious changes for privacy reasons. This method is triggered by a button click, and so far I cannot get it to create any work items.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
namespace customapp
{
public class CreateWorkItem
{
public void CreateWorkItemMethod()
{
string personalAccessToken = "xxxxxxxxx";
string credentials = Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "xxx", personalAccessToken)));
Object[] patchDocument = new Object[1];
patchDocument[0] = new { op = "add", path = "/fields/System.Title", value = "Test" };
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
var patchValue = new StringContent(JsonConvert.SerializeObject(patchDocument), Encoding.UTF8, "application/json-patch+json");
var method = new HttpMethod("PATCH");
var request = new HttpRequestMessage(method, "https://example.visualstudio.com/exampleproject/_apis/wit/workitems/$Support&20Ticket?api-version=1.0") { Content = patchValue };
var response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
var result = response.Content.ReadAsStringAsync().Result;
}
}}}}
In the url for the PATCH I am using the team project's ID (in place of /exampleproject you see below). Our site is set up to have an overall project, let's call it ""Master", and inside is a team project for each client, for example "ClientProject". So basically I want to create a "Support Ticket" work item in Master->ClientProject->Backlog/Board.
Using Master\\areapath instead (not Master\areapath).
Sample body:
[
{
"op": "add",
"path": "/fields/System.Title",
"value": "PBIAPI2"
},
{
"op": "add",
"path": "/fields/System.AreaPath",
"value": "Scrum2015\\SharedArea"
}
]
On the other hand, it’s better to create work item by using VSTS/TFS API with Microsoft Team Foundation Server Extended Client package.
Simple sample code:
var u = new Uri("https://[account].visualstudio.com");
VssCredentials c = new VssCredentials(new Microsoft.VisualStudio.Services.Common.VssBasicCredential(string.Empty, "[personal access token]"));
var connection = new VssConnection(u, c);
var workitemClient = connection.GetClient<WorkItemTrackingHttpClient>();
var workitemtype = "Product Backlog Item";
string teamProjectName = "Scrum2015";
var document = new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchDocument();
document.Add(
new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation()
{
Path = "/fields/Microsoft.VSTS.Common.Discipline",
Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
Value = "development"
});
document.Add(
new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation()
{
Path = "/fields/System.Title",
Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
Value = string.Format("{0} {1}", "RESTAPI", 6)
});
document.Add(new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation()
{
Path = "/fields/System.AreaPath",
Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
Value =string.Format("{0}\\{1}",teamProjectName, "SharedArea")
});
document.Add(
new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation()
{
Path = "/fields/System.AssignedTo",
Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
Value = "[user account]"
});
document.Add(
new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation()
{
Path = "/fields/System.Description",
Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
Value = "destest"
});
var workitem= workitemClient.CreateWorkItemAsync(document, teamProjectName, workitemtype).Result;
The method must be POST and Uri correct for consume the Api tfs is :
https://dev.azure.com/{organization}/{proyect}/_apis/wit/workitems/${type}?api-version=5.0
Check in :
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work%20items/create?view=azure-devops-rest-5.0
The next code function for me.
static void Main(string[] args)
{
CreateWorkItem();
}
public static void CreateWorkItem()
{
string _tokenAccess = "************"; //Click in security and get Token and give full access https://azure.microsoft.com/en-us/services/devops/
string type = "Bug";
string organization = "type your organization";
string proyect = "type your proyect";
string _UrlServiceCreate = $"https://dev.azure.com/{organization}/{proyect}/_apis/wit/workitems/${type}?api-version=5.0";
dynamic WorkItem = new List<dynamic>() {
new
{
op = "add",
path = "/fields/System.Title",
value = "Sample Bug test"
}
};
var WorkItemValue = new StringContent(JsonConvert.SerializeObject(WorkItem), Encoding.UTF8, "application/json-patch+json");
var JsonResultWorkItemCreated = HttpPost(_UrlServiceCreate, _tokenAccess, WorkItemValue);
}
public static string HttpPost(string urlService, string token, StringContent postValue)
{
try
{
string request = string.Empty;
using (HttpClient httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", token))));
using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(new HttpMethod("POST"), urlService) { Content = postValue })
{
var httpResponseMessage = httpClient.SendAsync(httpRequestMessage).Result;
if (httpResponseMessage.IsSuccessStatusCode)
request = httpResponseMessage.Content.ReadAsStringAsync().Result;
}
}
return request;
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
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
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:
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.
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;
}
Send an HTTP/2 request. This is as normal, but you need to do two extra things:
Set yourRequestMessage.Version to new Version(2, 0) in order to make the request using HTTP/2.
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.
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.
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;
}
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,
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!
I'm trying to send push notifications to iOS devices, using token-based authentication.
As required, I generated an APNs Auth Key in Apple's Dev Portal, and downloaded it (it's a file with p8 extension).
To send push notifications from my C# server, I need to somehow use this p8 file to sign my JWT tokens. How do I do that?
I tried to load the file to X509Certificate2, but X509Certificate2 doesn't seem to accept p8 files, so then I tried to convert the file to pfx/p12, but couldn't find a way to do that that actually works.
I found a way to do that, using BouncyCastle:
private static CngKey GetPrivateKey()
{
using (var reader = File.OpenText("path/to/apns/auth/key/file.p8"))
{
var 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);
}
}
And now creating and signing the token (using jose-jwt):
private static string GetProviderToken()
{
var epochNow = (int) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
var payload = new Dictionary<string, object>()
{
{"iss", "your team id"},
{"iat", epochNow}
};
var extraHeaders = new Dictionary<string, object>()
{
{"kid", "your key id"}
};
var privateKey = GetPrivateKey();
return JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeaders);
}
I hope this will be a solution;
private static string GetToken(string fileName)
{
var fileContent = File.ReadAllText(fileName).Replace("-----BEGIN PRIVATE KEY-----", "").Replace
("-----END PRIVATE KEY-----", "").Replace("\r", "");
var signatureAlgorithm = GetEllipticCurveAlgorithm(fileContent);
ECDsaSecurityKey eCDsaSecurityKey = new ECDsaSecurityKey(signatureAlgorithm)
{
KeyId = "S********2"
};
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken token = handler.CreateJwtSecurityToken(
issuer: "********-****-****-****-************",
audience: "appstoreconnect-v1",
expires: DateTime.UtcNow.AddMinutes(5),
issuedAt: DateTime.UtcNow,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(eCDsaSecurityKey, SecurityAlgorithms.EcdsaSha256));
return token.RawData;
}
private static ECDsa GetEllipticCurveAlgorithm(string privateKey)
{
var keyParams = (ECPrivateKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));
var normalizedEcPoint = keyParams.Parameters.G.Multiply(keyParams.D).Normalize();
return ECDsa.Create(new ECParameters
{
Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id),
D = keyParams.D.ToByteArrayUnsigned(),
Q =
{
X = normalizedEcPoint.XCoord.GetEncoded(),
Y = normalizedEcPoint.YCoord.GetEncoded()
}
});
}
A way to create the key without BouncyCastle:
var privateKeyString = (await File.ReadAllTextAsync("<PATH_TO_KEY_FILE>"))
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Replace("\n", "");
using var algorithm = ECDsa.Create();
algorithm.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKeyText), out var _);
var securityKey = new ECDsaSecurityKey(algorithm) { KeyId = "<KEY_ID>" };
I try to add new product to Woocommerce using c# RestSharp, but answer from server is:
{"errors":[{"code":"woocommerce_api_authentication_error","message":"oauth_consumer_key parameter mangler"}]}
For adding product i use next code:
public string AddProduct()
{
Method method = Method.POST;
string result = "";
string endpoint = "products";
var client = new RestClient(ApiUrl);
var parameters = new Dictionary<string, string>();
var request = createRequestWithParams(parameters, endpoint, method);
request.RequestFormat = DataFormat.Json;
request.AddJsonBody(new DTO.WCProduct { title = "eeee2", type = "simple", regular_price = "777", description = "Descr" });
AddOAuthparams(ref parameters, method.ToString(), endpoint);
result = client.Execute(request).Content;
return result;
}
Where Method createRequestWithParams is :
private RestRequest createRequestWithParams(Dictionary<string, string> parameters, string res, Method methos)
{
var req = new RestRequest(res, methos);
foreach (var item in parameters)
{
req.AddParameter(item.Key, item.Value);
}
return req;
}`
Where Method AddOAuthparams is :
void AddOAuthparams(ref Dictionary<string, string> parameters, string method, string endpoint)
{
parameters["oauth_consumer_key"] = this.ConsumerKey;
parameters["oauth_timestamp"] =
DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds.ToString();
parameters["oauth_timestamp"] = parameters["oauth_timestamp"].Substring(0, parameters["oauth_timestamp"].IndexOf(",")); //todo fix for . or ,
parameters["oauth_nonce"] = Hash(parameters["oauth_timestamp"]);
parameters["oauth_signature_method"] = "HMAC-SHA256";
parameters["oauth_signature"] = GenerateSignature(parameters, method, endpoint);
}
public string GenerateSignature(Dictionary<string, string> parameters, string method, string endpoint)
{
var baserequesturi = Regex.Replace(HttpUtility.UrlEncode(this.ApiUrl + endpoint), "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());
var normalized = NormalizeParameters(parameters);
var signingstring = string.Format("{0}&{1}&{2}", method, baserequesturi,
string.Join("%26", normalized.OrderBy(x => x.Key).ToList().ConvertAll(x => x.Key + "%3D" + x.Value)));
var signature =
Convert.ToBase64String(HashHMAC(Encoding.UTF8.GetBytes(this.ConsumerSecret),
Encoding.UTF8.GetBytes(signingstring)));
Console.WriteLine(signature);
return signature;
}
private Dictionary<string, string> NormalizeParameters(Dictionary<string, string> parameters)
{
var result = new Dictionary<string, string>();
foreach (var pair in parameters)
{
var key = HttpUtility.UrlEncode(HttpUtility.UrlDecode(pair.Key));
key = Regex.Replace(key, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper()).Replace("%", "%25");
var value = HttpUtility.UrlEncode(HttpUtility.UrlDecode(pair.Value));
value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper()).Replace("%", "%25");
result.Add(key, value);
}
return result;
}
** But if i try to Get info about products or Delete some product this functions worked fine, **
This code i find on Github https://github.com/kloon/WooCommerce-REST-API-Client-Library
I think, that my function for signature is worked bad, but i don't understand what should be fixed.
Maybe this is an old question. But it will be worth for someone came from google searching any hint.
edit: From the error message, i think you forgot to include the oauth_consumer_key as the message. Make sure your request include the oauth_consumer_key, by check the RestRequest query parameter.
Btw instead using the implementation from kloon, you can use the Woocommerce C# Library from my repository
Try the below solution its very easy to integrate and require very less line of codes.It Works for me
static void Main(string[] args)
{
string requestURL = #"http://www.example.co.uk/test/wp-json/wc/v1/products";
UriBuilder tokenRequestBuilder = new UriBuilder(requestURL);
var query = HttpUtility.ParseQueryString(tokenRequestBuilder.Query);
query["oauth_consumer_key"] = "consumer_key";
query["oauth_nonce"] = Guid.NewGuid().ToString("N");
query["oauth_signature_method"] = "HMAC-SHA1";
query["oauth_timestamp"] = (Math.Truncate((DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds)).ToString();
string signature = string.Format("{0}&{1}&{2}", "POST", Uri.EscapeDataString(requestURL), Uri.EscapeDataString(query.ToString()));
string oauth_Signature = "";
using (HMACSHA1 hmac = new HMACSHA1(Encoding.ASCII.GetBytes("consumer_Secret&")))
{
byte[] hashPayLoad = hmac.ComputeHash(Encoding.ASCII.GetBytes(signature));
oauth_Signature = Convert.ToBase64String(hashPayLoad);
}
query["oauth_signature"] = oauth_Signature;
tokenRequestBuilder.Query = query.ToString();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(tokenRequestBuilder.ToString());
request.ContentType = "application/json; charset=utf-8";
// request.Method = "GET";
request.Method = "POST";
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
string json = File.ReadAllText(#"D:\JsonFile.txt");//File Path for Json String
streamWriter.Write(json);
streamWriter.Flush();
}
var httpResponse = (HttpWebResponse)request.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
}
}