Establishing a token-based connection to APNs

As a proof of concept, I am trying to create bare-bones mac-command line app (in Swift) that allows me to send a string as push notification to my own iPhone.

I'm trying to follow the guide from Apple, however, I'm stuck creating the Jason Web Token. The guide kindly refers to JWT documentation, and leaves the most difficult part as an exercise to the user: actually creating the JWT (turning the difficulty up to 11 for me ;) ).

I've tried a number of things (mostly by scraping on the internets, sadly), but when I try to verify my token at the [JWT site](https://jwt. io), is always invalid.

High level, here is my understanding (let's sanity check my thinking, before diving into details:

  1. Create 3 JSON-strings: header, claims and payload
  2. Base64 encode each one separately
  3. Combine these three pieces of data like so
let Data((headerBase64String + "." + claimsBase64String + "." + payloadBase64String).utf8)
  1. Encrypt this this data using an algorithm called 'ES256' (no idea how to do this, could not find any documentation within Cryptokit on this one).
  2. Print the data as string, and enter the results into [JWT site](https://jwt. io). No joy.

I would like to use nothing but default libraries (i.e. Foundation, Cryptokit etc), and not make use of IBM's Kibana project (which is what majority of online suggestions seem to be doing). I'm aiming for a thorough understanding here.

Where do I need help to kickstart my understanding?

Replies

Pasted in below is the code I use to create JWT tokens. I’ve never used it with APNs but I know it works for the App Store Connect API and, AFAICT, those both use the same token setup.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import CryptoKit

enum QJWT {

    private static func jwtHeader(kid: String) -> String {
        struct Header: Codable {
            var alg: String
            var kid: String
            var typ: String
        }
        let header = Header(alg: "ES256", kid: kid, typ: "JWT")
        return try! JSONEncoder().encode(header).base64URLEncodedString
    }

    private static func jwtPayload(iss: String, exp: Date) -> String {
        // We round the expiry date up because it’s not clear how servers are going
        // to cope with fractional values.
        let exp = Date(timeIntervalSinceReferenceDate: exp.timeIntervalSinceReferenceDate.rounded(.up))
        struct Payload: Codable {
            var iss: String
            var exp: Date
            var aud: String
        }
        let payload = Payload(iss: iss, exp: exp, aud: "appstoreconnect-v1")
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .secondsSince1970
        return try! encoder.encode(payload).base64URLEncodedString
    }

    static func jwtSignedToken(kid: String, iss: String, exp: Date, privateKey: P256.Signing.PrivateKey) throws -> String {
        let header = jwtHeader(kid: kid)
        let payload = jwtPayload(iss: iss, exp: exp)
        let signingInput = "\(header).\(payload)"
        // Use an explicit SHA-256 digest rather than relying on P256 defaulting
        // to that.
        let sha256 = SHA256.hash(data: Data(signingInput.utf8))
        let sig1 = try privateKey.signature(for: sha256)
        let sig = sig1.rawRepresentation
        return "\(signingInput).\(sig.base64URLEncodedString)"
    }
}

// The following is modelled on the C# code in Appendix C of [RFC
// 7515][rfc7515].
//
// [rfc7515]: <https://tools.ietf.org/html/rfc7515>

private extension Data {

    init?(base64URLEncodedString: String) {
        let unpadded = base64URLEncodedString
            .replacingOccurrences(of: "-", with: "+")
            .replacingOccurrences(of: "_", with: "/")
        let padCount: Int
        switch unpadded.count % 4 {
        case 0: padCount = 0
        case 1: return nil
        case 2: padCount = 2
        case 3: padCount = 1
        default: fatalError()
        }
        self.init(base64Encoded: String(unpadded + String(repeating: "=", count: padCount)))
    }

    var base64URLEncodedString: String {
        let base64 = self.base64EncodedString()
        return String(base64.split(separator: "=").first!)
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
    }
}