Skip to content

Commit 1619232

Browse files
author
Kamil Strzelecki
committed
Removed JWT Kit dependency and implemented JWT token generation using CryptoKit
1 parent 7b61d98 commit 1619232

6 files changed

Lines changed: 135 additions & 102 deletions

File tree

Package.resolved

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import PackageDescription
66
let package = Package(
77
name: "AppStoreManager",
88
platforms: [
9-
.macOS(.v10_15)
9+
.macOS(.v11)
1010
],
1111
products: [
1212
.library(
@@ -22,24 +22,20 @@ let package = Package(
2222
)
2323
],
2424
dependencies: [
25-
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.4.0"),
26-
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.1.2")),
27-
],
25+
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.1.2"))
26+
],
2827
targets: [
2928
.target(
3029
name: "AppStoreManager"),
3130
.target(
32-
name: "AppStoreManagerAuthorization",
33-
dependencies: [
34-
.product(name: "JWTKit", package: "jwt-kit")
35-
]),
31+
name: "AppStoreManagerAuthorization"),
3632
.executableTarget(
3733
name: "AppStoreManagerShell",
3834
dependencies: [
3935
"AppStoreManagerAuthorization",
4036
"AppStoreManager",
41-
.product(name: "ArgumentParser", package: "swift-argument-parser"),
42-
]),
37+
.product(name: "ArgumentParser", package: "swift-argument-parser")
38+
]),
4339
.testTarget(
4440
name: "AppStoreManagerTests",
4541
dependencies: [

Sources/AppStoreManagerAuthorization/AppStoreManagerAuthorization.swift

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
//
77

88
import Foundation
9-
import JWTKit
10-
import CryptoKit
119

1210
/// Helper object which generate the JWT token to authorize requests to the _AppStoreConnect API_.
1311
///
@@ -44,39 +42,9 @@ public struct AppStoreConnectSigner {
4442
///
4543
/// - Parameter privateKey: The private key used to sign the token.
4644
/// - Returns: The signed token to use to authorize requests.
47-
public func generateJWTToken(usingPrivateKey privateKey: Data) throws -> String {
48-
let signer = JWTSigners()
49-
try signer.use(.es256(key: .private(pem: privateKey)))
50-
return try signer.sign(createPayload(), kid: .init(string: keyIdentifier))
51-
}
52-
53-
/// Creates the instance of a `Claims` required by the `SwiftJWT` framework.
54-
///
55-
/// The [Apple documentation](https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests) specifies the `aud` parameter as a `String` but the `ClaimsStandardJWT` gets an array. From the [JWT documentation](https://tools.ietf.org/html/rfc7519#section-4.1.3):
56-
/// ```
57-
/// In the special case when the JWT has one audience,
58-
/// the "aud" value MAY be a single case-sensitive
59-
/// string containing a StringOrURI value.
60-
/// ```
61-
///
62-
/// Hopefully this will works 🙏
63-
private func createPayload() -> ClaimsAppStoreJWT {
64-
ClaimsAppStoreJWT(
65-
iss: payload.issuerIdentifier,
66-
aud: payload.audience,
67-
exp: payload.expirationTime)
68-
}
69-
}
70-
71-
private struct ClaimsAppStoreJWT {
72-
let iss: String
73-
let aud: String
74-
let exp: Date
75-
}
76-
77-
extension ClaimsAppStoreJWT: JWTPayload {
78-
func verify(using signer: JWTKit.JWTSigner) throws {
79-
try AudienceClaim(stringLiteral: aud).verifyIntendedAudience(includes: aud)
45+
public func generateJWTToken(usingPrivateKey privateKey: String) throws -> String {
46+
let generator = JWTGenerator(keyIdentifier: keyIdentifier, payload: payload)
47+
return try generator.token(withPrivateKey: privateKey)
8048
}
8149
}
8250
/// The payload required to create a JWT token.
@@ -92,7 +60,7 @@ public struct AppStoreConnectPayload {
9260

9361
/// The token's expiration time; tokens that expire more than 4 minutes in the future are not valid (Ex: 1528408800)
9462
/// It is encoded as a *exp* value in the TWT payload
95-
fileprivate var expirationTime: Date {
63+
internal var expirationTime: Date {
9664
return Date(timeIntervalSinceNow: 60 * 4)
9765
}
9866

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// JWTGenerator.swift
3+
//
4+
//
5+
// Created by Kamil Strzelecki on 17/05/2022.
6+
// Copyright © 2022 Kamil Strzelecki. All rights reserved.
7+
//
8+
9+
import CryptoKit
10+
import Foundation
11+
12+
13+
internal struct JWTGenerator {
14+
15+
internal let keyIdentifier: String
16+
internal let payload: AppStoreConnectPayload
17+
18+
private let encoder: JSONEncoder = {
19+
let encoder = JSONEncoder()
20+
encoder.dateEncodingStrategy = .secondsSince1970
21+
return encoder
22+
}()
23+
24+
internal func token(withPrivateKey privateKey: String) throws -> String {
25+
let header = try headerBytes()
26+
let payload = try payloadBytes()
27+
let period = Array(Character(".").utf8)
28+
29+
let dataToSign = header + period + payload
30+
let signatureData = try sign(dataToSign, withPrivateKey: privateKey)
31+
32+
let signedData = Data(dataToSign + period + signatureData)
33+
return String(decoding: signedData, as: UTF8.self)
34+
}
35+
36+
private func sign(_ dataToSign: [UInt8], withPrivateKey privateKey: String) throws -> [UInt8] {
37+
let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey)
38+
let signature = try privateKey.signature(for: dataToSign)
39+
return Array(signature.rawRepresentation.base64URLEncodedData())
40+
}
41+
42+
private func headerBytes() throws -> [UInt8] {
43+
let header = Header(kid: keyIdentifier)
44+
let data = try encoder.encode(header)
45+
return Array(data.base64URLEncodedData())
46+
}
47+
48+
private func payloadBytes() throws -> [UInt8] {
49+
let payload = Payload(payload)
50+
let data = try encoder.encode(payload)
51+
return Array(data.base64URLEncodedData())
52+
}
53+
}
54+
55+
56+
extension JWTGenerator {
57+
58+
fileprivate struct Header: Encodable {
59+
60+
fileprivate let alg = "ES256"
61+
fileprivate let typ = "JWT"
62+
fileprivate let kid: String
63+
}
64+
}
65+
66+
67+
extension JWTGenerator {
68+
69+
fileprivate struct Payload: Encodable {
70+
71+
fileprivate let iss: String
72+
fileprivate let aud: String
73+
fileprivate let exp: Date
74+
75+
fileprivate init(_ payload: AppStoreConnectPayload) {
76+
self.iss = payload.issuerIdentifier
77+
self.aud = payload.audience
78+
self.exp = payload.expirationTime
79+
}
80+
}
81+
}
82+
83+
84+
extension Data {
85+
86+
/// Converts data to a base64-url encoded data.
87+
///
88+
/// https://tools.ietf.org/html/rfc4648#page-7
89+
internal func base64URLEncodedData() -> Data {
90+
var data = base64EncodedData()
91+
92+
for (index, byte) in data.enumerated() {
93+
switch byte {
94+
case 0x2B:
95+
data[index] = 0x2D
96+
case 0x2F:
97+
data[index] = 0x5F
98+
default:
99+
continue
100+
}
101+
}
102+
103+
return data.split(separator: 0x3D)
104+
.first ?? Data()
105+
}
106+
}
107+
108+
109+
extension JWTGenerator {
110+
111+
internal enum Error: Swift.Error {
112+
case cannotConvertDataToString
113+
}
114+
}

Sources/AppStoreManagerShell/Authorise.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,14 @@ struct Authorise: ParsableCommand {
3131

3232

3333
func run() throws {
34-
let privateKey: String
35-
if self.privateKey.hasPrefix("-----BEGIN") == false,
36-
self.privateKey.contains("\\n") {
37-
34+
var privateKey = privateKey
35+
36+
if privateKey.hasPrefix("-----BEGIN") == false {
3837
privateKey = """
3938
-----BEGIN PRIVATE KEY-----
40-
\(self.privateKey.replacingOccurrences(of: "\\n", with: "\n"))
39+
\(privateKey.replacingOccurrences(of: "\\n", with: "\n"))
4140
-----END PRIVATE KEY-----
4241
"""
43-
} else {
44-
privateKey = self.privateKey
4542
}
4643

4744
let authorizationService = AuthorizationService(

Sources/AppStoreManagerShell/AuthorizationService.swift

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,37 +27,13 @@ struct AuthorizationService {
2727
}
2828

2929
func authorizationToken() -> Result<String, Self.Error> {
30-
createPrivateKey(for: data.privateKey)
31-
.map { (privateKey) -> (AppStoreConnectSigner, privateKey: Data) in
32-
let payload = AppStoreConnectPayload(issuerIdentifier: self.data.issuerId)
33-
return (AppStoreConnectSigner(keyIdentifier: self.data.keyId, payload: payload), privateKey)
34-
}
35-
.flatMap { (signerAndPrivateKey) -> Result<String, Self.Error> in
36-
let (signer, privateKey) = signerAndPrivateKey
37-
do {
38-
let token = try signer.generateJWTToken(usingPrivateKey: privateKey)
39-
return .success(token)
40-
} catch {
41-
return .failure(.init(message: "Could not generate JWT token"))
42-
}
43-
}
44-
}
45-
46-
private func createPrivateKey(for privateToken: String) -> Result<Data, Self.Error> {
47-
guard let privateKey = data.privateKey.data(using: .utf8) else {
48-
return .failure(.init(message: "Provided private key is invalid"))
49-
}
50-
return .success(privateKey)
51-
}
52-
53-
private func mapError<Anything>(error: Result<Anything, Self.Error>.ConcatenatedError<Self.Error>) -> Self.Error {
54-
55-
switch error {
56-
case let .both(payloadError, keyIdentifierError):
57-
return .init(message: "\(payloadError.message)\n\(keyIdentifierError.message)")
58-
case let .failure(error),
59-
let .other(error):
60-
return error
30+
do {
31+
let payload = AppStoreConnectPayload(issuerIdentifier: data.issuerId)
32+
let signer = AppStoreConnectSigner(keyIdentifier: data.keyId, payload: payload)
33+
let token = try signer.generateJWTToken(usingPrivateKey: data.privateKey)
34+
return .success(token)
35+
} catch {
36+
return .failure(.init(message: "Could not generate JWT token"))
6137
}
6238
}
6339
}

0 commit comments

Comments
 (0)