Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support individual keys in SDK #279

Merged
merged 1 commit into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion Sources/APIProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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:
Expand All @@ -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.
Expand Down
33 changes: 29 additions & 4 deletions Sources/JWT/JWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -94,15 +112,22 @@ 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
}

/// 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)"
Expand Down