diff --git a/Sources/APIProvider.swift b/Sources/APIProvider.swift index ea9206d6..ef4b2343 100644 --- a/Sources/APIProvider.swift +++ b/Sources/APIProvider.swift @@ -27,7 +27,7 @@ public struct APIConfiguration { let privateKey: JWT.PrivateKey /// Your issuer ID from the API Keys page in App Store Connect (Ex: 57246542-96fe-1a63-e053-0824d011072a) - let issuerID: String + let issuerID: String? /// Creates a new API configuration to use for initialising the API Provider. /// @@ -48,6 +48,25 @@ public struct APIConfiguration { } } + /// Creates a new API configuration to use for initialising the API Provider. + /// + /// - Parameters: + /// - individualPrivateKeyID: Your private key ID from App Store Connect (Ex: 2X9R4HXF34) + /// - individualPrivateKey: The contents of the individual private key from App Store Connect + public init(individualPrivateKeyID: String, individualPrivateKey: String) throws { + self.privateKeyID = individualPrivateKeyID + self.issuerID = nil + + guard let base64Key = Data(base64Encoded: individualPrivateKey) else { + throw JWT.Error.invalidBase64EncodedPrivateKey + } + do { + self.privateKey = try JWT.PrivateKey(derRepresentation: base64Key) + } catch { + throw JWT.Error.invalidPrivateKey + } + } + /// Creates a new API configuration to use for initialising the API Provider. /// /// - Parameters: @@ -69,6 +88,27 @@ public struct APIConfiguration { throw JWT.Error.invalidPrivateKey } } + + /// Creates a new API configuration to use for initialising the API Provider. + /// + /// - Parameters: + /// - individualPrivateKeyID: Your private key ID from App Store Connect (Ex: 2X9R4HXF34). Will be inferred from `privateKeyURL` if nil. + /// - individualPrivateKeyURL: A file URL that references the path to your private key file. + public init(individualPrivateKeyID: String? = nil, privateKeyURL: URL) throws { + self.issuerID = nil + if let individualPrivateKeyID { + self.privateKeyID = individualPrivateKeyID + } else { + let filename = privateKeyURL.deletingPathExtension().lastPathComponent + self.privateKeyID = String(filename.suffix(10)) + } + do { + let pemEncodedPrivateKey = try String(contentsOf: privateKeyURL) + self.privateKey = try JWT.PrivateKey(pemRepresentation: pemEncodedPrivateKey) + } catch { + throw JWT.Error.invalidPrivateKey + } + } } /// Provides access to all API Methods. Can be used to perform API requests. diff --git a/Sources/JWT/JWT.swift b/Sources/JWT/JWT.swift index de150d1c..d28b5a65 100644 --- a/Sources/JWT/JWT.swift +++ b/Sources/JWT/JWT.swift @@ -28,7 +28,7 @@ private struct Header: Codable { } /// The JWT Payload contains information specific to the App Store Connect APIs, such as issuer ID and expiration time. -private struct Payload: Codable { +private struct TeamPayload: Codable { enum CodingKeys: String, CodingKey { case issuerIdentifier = "iss" @@ -46,6 +46,24 @@ private struct Payload: Codable { let audience: String = "appstoreconnect-v1" } +private struct IndividualPayload: Codable { + + enum CodingKeys: String, CodingKey { + case subject = "sub" + case expirationTime = "exp" + case audience = "aud" + } + + /// The subject to pass to the payload when using individual keys + let subject: String = "user" + + /// The token's expiration time, in Unix epoch time; tokens that expire more than 20 minutes in the future are not valid (Ex: 1528408800) + let expirationTime: TimeInterval + + /// The required audience which is set to the App Store Connect version. + let audience: String = "appstoreconnect-v1" +} + protocol JWTCreatable { func signedToken(using privateKey: JWT.PrivateKey) throws -> JWT.Token } @@ -82,7 +100,7 @@ public struct JWT: Codable, JWTCreatable { private let header: Header /// Your issuer identifier from the API Keys page in App Store Connect (Ex: 57246542-96fe-1a63-e053-0824d011072a) - private let issuerIdentifier: String + private let issuerIdentifier: String? /// The token's expiration duration. Tokens that expire more than 20 minutes in the future are not valid, so set it to a max of 20 minutes. private let expireDuration: TimeInterval @@ -94,7 +112,7 @@ public struct JWT: Codable, JWTCreatable { /// - issuerIdentifier: Your issuer identifier from the API Keys page in App Store Connect (Ex: 57246542-96fe-1a63-e053-0824d011072a) /// - expireDuration: The token's expiration duration. /// Tokens that expire more than 20 minutes in the future are not valid, so set it to a max of 20 minutes. - public init(keyIdentifier: String, issuerIdentifier: String, expireDuration: TimeInterval) { + public init(keyIdentifier: String, issuerIdentifier: String?, expireDuration: TimeInterval) { header = Header(keyIdentifier: keyIdentifier) self.issuerIdentifier = issuerIdentifier self.expireDuration = expireDuration @@ -102,7 +120,14 @@ public struct JWT: Codable, JWTCreatable { /// Combine the header and the payload as a digest for signing. private func digest(dateProvider: DateProvider) throws -> String { - let payload = Payload(issuerIdentifier: issuerIdentifier, expirationTime: dateProvider().addingTimeInterval(expireDuration).timeIntervalSince1970) + let expirationTime = dateProvider().addingTimeInterval(expireDuration).timeIntervalSince1970 + let payload: Codable + if let issuerIdentifier { + payload = TeamPayload(issuerIdentifier: issuerIdentifier, expirationTime: expirationTime) + } else { + payload = IndividualPayload(expirationTime: expirationTime) + } + let headerString = try JSONEncoder().encode(header.self).base64URLEncoded() let payloadString = try JSONEncoder().encode(payload.self).base64URLEncoded() return "\(headerString).\(payloadString)"