How to encode JWT for APN tokenization (.NET, C#)
Asked Answered
T

5

6

I'm trying to send Push Notifications to APN with tokenization. I tried to use a few libraries like jose-jwt and Microsot Jwt class for creating JWT token, but I can't wrap my head around it.

I get stuck on creating JWT and signing it with private key.

For communicating with certificates, I used PushSharp and it worked just fine. Can anyone help me with a working similar example but with tokens?

edit: following Apple's documentation here: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1

Sample code: the closest I came to something would look like this, but I don't know how to create CngKey properly

var payload = new Dictionary<string, object>()
            {
                { "iss", issuer },
                { "iat", DateTime.UtcNow }
            };

var headers = new Dictionary<string, object>()
            {
                 { "kid", keyIdentifier}
            };

CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256); //how to create this CngKey

string token = Jose.JWT.Encode(payload, key, JwsAlgorithm.ES256, headers);
Thickskinned answered 18/1, 2017 at 15:9 Comment(2)
could you include some code so we can look at? since you are asking general question i suggest you read this : stormpath.com/blog/token-authentication-asp-net-coreChloral
Thanks, I'll look into it. Added a code sample which shows where I'm stuck at.Thickskinned
T
5

Thanks for your answers, had to contact many supports to get this done. Here is what the final result looked like.

/// <summary>
    /// Method returns ECDSA signed JWT token format, from json header, json payload and privateKey (pure string extracted from *.p8 file - PKCS#8 format)
    /// </summary>
    /// <param name="privateKey">ECDSA256 key</param>
    /// <param name="header">JSON header, i.e. "{\"alg\":\"ES256\" ,\"kid\":\"1234567899"\"}"</param>
    /// <param name="payload">JSON payload, i.e.  {\"iss\":\"MMMMMMMMMM"\",\"iat\":"122222222229"}"</param>
    /// <returns>base64url encoded JWT token</returns>
    public static string SignES256(string privateKey, string header, string payload)
    {
        CngKey key = CngKey.Import(
            Convert.FromBase64String(privateKey), 
            CngKeyBlobFormat.Pkcs8PrivateBlob);

        using (ECDsaCng dsa = new ECDsaCng(key))
        {
            dsa.HashAlgorithm = CngAlgorithm.Sha256;
            var unsignedJwtData = 
                Url.Base64urlEncode(Encoding.UTF8.GetBytes(header)) + "." + Url.Base64urlEncode(Encoding.UTF8.GetBytes(payload));
            var signature = 
                dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData));
            return unsignedJwtData + "." + Url.Base64urlEncode(signature);
        }
    }
Thickskinned answered 1/3, 2017 at 13:21 Comment(6)
Could you comment on what you did to get this working? And explain where you found the answers that you needed if not here.Shipway
Jose-jwt forum and Apple support. I was missing Base64 encodings above to get it working. Apple guys eventually shaped the code above with their hints. I couldn't find any official documentation for working example in C#.Thickskinned
Any tips on how you actually sent the notification to the Apple server? I've tried numerous things to get it to work, but no luck. If you have any code, it would be greatly appreciated!Fornix
@Brad C - use HTTP/2 clientThickskinned
@Thickskinned How do you extract the private key from the p8 file?! Thanks!Tetrameter
This is great. I think this will really help. @Thickskinned Do you happen to have the snip of your code for where you use the HTTP/2 Client in .Net C# ?? I know that apple requires HTTP/2 but I have been having some trouble finding a reliable call and getting a response back that I can view. Thanks!Sapota
P
3

Here is what I use:

var privateKeyContent = System.IO.File.ReadAllText(AUTHKEYFILE);
var privateKey = privateKeyContent.Split('\n')[1];
var secretKeyFile = Convert.FromBase64String(privateKey);
var secretKey = CngKey.Import(secretKeyFile,CngKeyBlobFormat.Pkcs8PrivateBlob);
Parmenter answered 24/2, 2017 at 13:2 Comment(3)
Thanks for your answer, already had an solution. Just added it. Maybe you can use it.Thickskinned
running your code I receive: 'An unhandled exception of type 'System.Security.Cryptography.CryptographicException' occurred in System.Core.dll' This is with p8 file... What is wrong?Tetrameter
This won't work on non-Windows because of CngKey.ImportPerseus
B
3

if you want to run your app in MacOS or linux, you can't use CngKey in this case you can use ECDsa algorithm for encoding the JWT

private ECDsa encoder(string privateKey)
{
 var result = ECDsa.Create();
 result.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
 return result;
}
Bilodeau answered 10/5, 2022 at 16:13 Comment(1)
Need to mention that the privateKey must be the pure base64 without the header and footer and not wrapped.Magisterial
C
3

Pulling all the pieces together and extending some of the prior answers

This utilizes Microsoft.IdentityModel.Tokens which removes Windows dependencies.

Tested this under .NET 6

How to create the token:

public static string CreateSecurityToken(string tokenId, string teamId, string tokenKey, DateTime utcGenerateDateTime)
{
    var key = ECDsa.Create();
    key?.ImportPkcs8PrivateKey(Convert.FromBase64String(tokenKey), out _);

    var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId };
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

    var descriptor = new SecurityTokenDescriptor
    {
        IssuedAt = utcGenerateDateTime,
        Issuer = teamId,
        SigningCredentials = credentials
    };

    var handler = new JwtSecurityTokenHandler();
    var encodedToken = handler.CreateEncodedJwt(descriptor);
    return encodedToken;
}

How to validate the token:

public static SecurityToken ValidateSecurityToken(string token, string tokenId, string tokenKey)
{
    if (token == null) 
        return null;

    var key = ECDsa.Create();
    key?.ImportPkcs8PrivateKey(Convert.FromBase64String(tokenKey), out _);

    var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId };

    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        tokenHandler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = securityKey,
            ValidateIssuer = false,
            ValidateAudience = false,
            ClockSkew = TimeSpan.Zero
        }, out var validatedToken);

        return validatedToken;
    }
    catch
    {
        return null;
    }
}
Confluent answered 18/8, 2022 at 15:28 Comment(0)
C
1

Here is what i use;

public static string getAppleJWT(string privateKeyPath, string teamId, string keyId)
{

    var private_key = getPrivateKey(privateKeyPath);

    var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
    var issueTime = DateTime.Now;

    var payload = new Dictionary<string, object>()
        {
            { "iss", teamId },
            { "iat", (int)issueTime.Subtract(utc0).TotalSeconds }
        };

    var headers = new Dictionary<string, object>()
        {
             { "alg", "ES256"},
             { "kid", keyId}
        };


    return Jose.JWT.Encode(payload, private_key, JwsAlgorithm.ES256, headers);
}

public static CngKey getPrivateKey(string privateKeyPath)
{
    var privateKeyLines = System.IO.File.ReadAllLines(privateKeyPath).ToList();
    privateKeyLines.RemoveAt(privateKeyLines.Count - 1);
    privateKeyLines.RemoveAt(0);

    var privateKey = string.Join("", privateKeyLines);
    var secretKeyFile = Convert.FromBase64String(privateKey);
    var secretKey = CngKey.Import(secretKeyFile, CngKeyBlobFormat.Pkcs8PrivateBlob);
    return secretKey;
}
Coachwork answered 10/11, 2020 at 7:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.