From 14221868a7aee9b784a520c1de0de39320a3f22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:37:09 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20add=20google=20auth=20?= =?UTF-8?q?dep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Package.resolved | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ Package.swift | 3 ++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index fb5b734..95f2966 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,33 @@ { "object": { "pins": [ + { + "package": "BigInt", + "repositoryURL": "https://github.com/attaswift/BigInt", + "state": { + "branch": null, + "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version": "5.3.0" + } + }, + { + "package": "CryptoSwift", + "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", + "state": { + "branch": null, + "revision": "af1b58fc569bfde777462349b9f7314b61762be0", + "version": "1.3.2" + } + }, + { + "package": "Auth", + "repositoryURL": "https://github.com/googleapis/google-auth-library-swift.git", + "state": { + "branch": null, + "revision": "4b510d91fc74f1415eae6dabc9836b8c3e1f44f6", + "version": "0.5.3" + } + }, { "package": "jwt-kit", "repositoryURL": "https://github.com/vapor/jwt-kit", @@ -10,6 +37,24 @@ "version": "4.3.0" } }, + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics.git", + "state": { + "branch": null, + "revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", + "version": "1.0.2" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" + } + }, { "package": "swift-crypto", "repositoryURL": "https://github.com/apple/swift-crypto.git", @@ -18,6 +63,15 @@ "revision": "a8911e0fadc25aef1071d582355bd1037a176060", "version": "2.0.4" } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "edfceecba13d68c1c993382806e72f7e96feaa86", + "version": "2.44.0" + } } ] }, diff --git a/Package.swift b/Package.swift index bd863b6..6b1676e 100644 --- a/Package.swift +++ b/Package.swift @@ -19,13 +19,14 @@ let package = Package( ], dependencies: [ .package(url:"https://github.com/vapor/jwt-kit", .upToNextMajor(from: "4.3.0")), + .package(url: "https://github.com/googleapis/google-auth-library-swift", from: "0.5.2"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "ACKLocalizationCore", - dependencies: ["JWTKit"]), + dependencies: ["JWTKit", "OAuth2"]), .target( name: "ACKLocalization", dependencies: ["ACKLocalizationCore"]), From 8486416b453a30ffcad07ed9bf53c560558c9e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:37:49 +0100 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8=20use=20service=20account=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ACKLocalizationCore/ACKLocalization.swift | 24 +++++++++++-------- .../Services/AuthAPIService.swift | 3 ++- .../Mocks/AuthAPIServiceMock.swift | 3 ++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Sources/ACKLocalizationCore/ACKLocalization.swift b/Sources/ACKLocalizationCore/ACKLocalization.swift index 0f6b391..c05d26f 100644 --- a/Sources/ACKLocalizationCore/ACKLocalization.swift +++ b/Sources/ACKLocalizationCore/ACKLocalization.swift @@ -60,7 +60,7 @@ public final class ACKLocalization { /// Fetches content of given sheet from spreadsheet using given `serviceAccount` /// /// If not `spreadsheetTabName` is provided, the first in the spreadsheet is used - public func fetchSheetValues(_ spreadsheetTabName: String?, spreadsheetId: String, serviceAccount: ServiceAccount) -> AnyPublisher { + public func fetchSheetValues(_ spreadsheetTabName: String?, spreadsheetId: String, serviceAccount: Data) -> AnyPublisher { let sheetsAPI = self.sheetsAPI return authAPI.fetchAccessToken(serviceAccount: serviceAccount) @@ -326,7 +326,11 @@ public final class ACKLocalization { do { if let serviceAccountPath = config.serviceAccount { let serviceAccount = try loadServiceAccount(from: serviceAccountPath) - return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, serviceAccount: serviceAccount) + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + serviceAccount: serviceAccount + ) } else if let apiKey = config.apiKey { return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, apiKey: apiKey) } else if let serviceAccountPath = ProcessInfo.processInfo.environment[Constants.serviceAccountPath] { @@ -342,12 +346,12 @@ public final class ACKLocalization { } else { let errorMessage = """ Unable to load API key or service account path. Please check if: - + - `apiKey` or `serviceAccount` attribute is provided in `localization.json` file or - `\(Constants.apiKey)` or `\(Constants.serviceAccountPath)` environment variable is set """ - + throw LocalizationError(message: errorMessage) } } catch { @@ -385,16 +389,16 @@ public final class ACKLocalization { } /// Loads service account from given `config` - private func loadServiceAccount(from path: String) throws -> ServiceAccount { + private func loadServiceAccount(from path: String) throws -> Data { guard let serviceAccountData = FileManager.default.contents(atPath: path) else { throw LocalizationError(message: "Unable to load service account at " + path) } - - do { - return try JSONDecoder().decode(ServiceAccount.self, from: serviceAccountData) - } catch { - throw LocalizationError(message: "Unable to read service account from `" + path + "` - " + error.localizedDescription) + + guard !serviceAccountData.isEmpty else { + throw LocalizationError(message: "Invalid service account data") } + + return serviceAccountData } /// Actually writes given `rows` to given `file` diff --git a/Sources/ACKLocalizationCore/Services/AuthAPIService.swift b/Sources/ACKLocalizationCore/Services/AuthAPIService.swift index df38614..3863aaf 100644 --- a/Sources/ACKLocalizationCore/Services/AuthAPIService.swift +++ b/Sources/ACKLocalizationCore/Services/AuthAPIService.swift @@ -8,11 +8,12 @@ import Combine import Foundation import JWTKit +import OAuth2 /// Protocol wrapping a service that fetches an access token from further communication public protocol AuthAPIServicing { /// Fetch access token for given `serviceAccount` - func fetchAccessToken(serviceAccount: ServiceAccount) -> AnyPublisher + func fetchAccessToken(serviceAccount: Data) -> AnyPublisher } /// Service that fetches an access token from further communication diff --git a/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift b/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift index dd9677b..6860b40 100644 --- a/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift +++ b/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift @@ -7,9 +7,10 @@ import ACKLocalizationCore import Combine +import Foundation final class AuthAPIServiceMock: AuthAPIServicing { - func fetchAccessToken(serviceAccount: ServiceAccount) -> AnyPublisher { + func fetchAccessToken(serviceAccount: Data) -> AnyPublisher { Empty().eraseToAnyPublisher() } } From 991ca97e39ab19b5ae4ee06ac5cb671ee0b6ed21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:38:22 +0100 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20replace=20jwc=20with=20google?= =?UTF-8?q?=20auth=20lib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/AuthAPIService.swift | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/Sources/ACKLocalizationCore/Services/AuthAPIService.swift b/Sources/ACKLocalizationCore/Services/AuthAPIService.swift index 3863aaf..02b6b9f 100644 --- a/Sources/ACKLocalizationCore/Services/AuthAPIService.swift +++ b/Sources/ACKLocalizationCore/Services/AuthAPIService.swift @@ -7,7 +7,6 @@ import Combine import Foundation -import JWTKit import OAuth2 /// Protocol wrapping a service that fetches an access token from further communication @@ -29,41 +28,36 @@ public struct AuthAPIService: AuthAPIServicing { // MARK: - API calls /// Fetch access token for given `serviceAccount` - public func fetchAccessToken(serviceAccount: ServiceAccount) -> AnyPublisher { - let jwt = try? self.jwt( - for: serviceAccount, - claims: claims(serviceAccount: serviceAccount, validFor: 60) - ) - let requestData = AccessTokenRequest(assertion: jwt ?? "") - var request = URLRequest(url: serviceAccount.tokenURL) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try? JSONEncoder().encode(requestData) + public func fetchAccessToken(serviceAccount: Data) -> AnyPublisher { + Deferred { + Future { promise in + guard let tokenProvider = ServiceAccountTokenProvider( + credentialsData: serviceAccount, + scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"] + ) else { + promise(.failure(.init(message: "Creating provider failed"))) + return + } - return session.dataTaskPublisher(for: request) - .validate() - .decode(type: AccessToken.self, decoder: JSONDecoder()) - .mapError(RequestError.init) - .eraseToAnyPublisher() + do { + try tokenProvider.withToken { token, error in + if let token = token, let accessToken = token.AccessToken { + let token = AccessToken( + accessToken: accessToken, + expiration: TimeInterval(token.ExpiresIn ?? 0), + type: token.TokenType ?? "" + ) + promise(.success(token)) + } else if let error { + promise(.failure(.init(underlyingError: error, message: "Retrieving access token failed"))) + } else { + promise(.failure(.init(message: "Access token was not provided"))) + } + } + } catch { + promise(.failure(.init(underlyingError: error, message: "Retrieving access token failed"))) + } + } + }.eraseToAnyPublisher() } - - // MARK: - Private helpers - - /// Create JWT token that will be sent to retrieve access token - private func jwt(for serviceAccount: ServiceAccount, claims: GoogleClaims) throws -> String { - let signers = JWTSigners() - try signers.use(.rs256(key: .private(pem: serviceAccount.privateKey))) - return try signers.sign(claims) - } - - private func claims(serviceAccount sa: ServiceAccount, validFor interval: TimeInterval) -> GoogleClaims { - let now = Int(Date().timeIntervalSince1970) - - return .init( - serviceAccount: sa, - scope: .readOnly, - exp: now + Int(interval), - iat: now - ) - } } From 61edaefa5c2a4c4931c87f1705e94f4e01c83a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:40:00 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=94=A5=20remove=20jwt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Package.resolved | 18 -------- Package.swift | 3 +- .../Model/GoogleClaims.swift | 41 ------------------- 3 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 Sources/ACKLocalizationCore/Model/GoogleClaims.swift diff --git a/Package.resolved b/Package.resolved index 95f2966..d66f81f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -28,15 +28,6 @@ "version": "0.5.3" } }, - { - "package": "jwt-kit", - "repositoryURL": "https://github.com/vapor/jwt-kit", - "state": { - "branch": null, - "revision": "1822bb0abf0a31a4b5078ec19061c548835253b5", - "version": "4.3.0" - } - }, { "package": "swift-atomics", "repositoryURL": "https://github.com/apple/swift-atomics.git", @@ -55,15 +46,6 @@ "version": "1.0.3" } }, - { - "package": "swift-crypto", - "repositoryURL": "https://github.com/apple/swift-crypto.git", - "state": { - "branch": null, - "revision": "a8911e0fadc25aef1071d582355bd1037a176060", - "version": "2.0.4" - } - }, { "package": "swift-nio", "repositoryURL": "https://github.com/apple/swift-nio.git", diff --git a/Package.swift b/Package.swift index 6b1676e..6585c27 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,6 @@ let package = Package( targets: ["ACKLocalization"]), ], dependencies: [ - .package(url:"https://github.com/vapor/jwt-kit", .upToNextMajor(from: "4.3.0")), .package(url: "https://github.com/googleapis/google-auth-library-swift", from: "0.5.2"), ], targets: [ @@ -26,7 +25,7 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "ACKLocalizationCore", - dependencies: ["JWTKit", "OAuth2"]), + dependencies: ["OAuth2"]), .target( name: "ACKLocalization", dependencies: ["ACKLocalizationCore"]), diff --git a/Sources/ACKLocalizationCore/Model/GoogleClaims.swift b/Sources/ACKLocalizationCore/Model/GoogleClaims.swift deleted file mode 100644 index cd05f5f..0000000 --- a/Sources/ACKLocalizationCore/Model/GoogleClaims.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// GoogleClaims.swift -// -// -// Created by Jakub Olejník on 11/12/2019. -// - -import Foundation -import JWTKit - -/// Struct that is used for generating second part of JWT token -struct GoogleClaims: JWTPayload { - enum Scope: String, Codable { - case readOnly = "https://www.googleapis.com/auth/spreadsheets.readonly" - } - - /// Service account email - let iss: String - - /// Required scope - let scope: Scope - - /// Desired auth endpoint - let aud: URL - - /// Date of expiration timestamp - let exp: Int - - /// Issued at date timestamp - let iat: Int -} - -extension GoogleClaims { - init(serviceAccount: ServiceAccount, scope: Scope, exp: Int, iat: Int) { - self.init(iss: serviceAccount.clientEmail, scope: scope, aud: serviceAccount.tokenURL, exp: exp, iat: iat) - } - - func verify(using signer: JWTSigner) throws { - try ExpirationClaim(value: Date(timeIntervalSince1970: TimeInterval(exp))).verifyNotExpired() - } -} From 4f1b72ef9eacac660846af90ea3572eaaa28affd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:46:27 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=8E=A8=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Package.swift | 5 ++++- Sources/ACKLocalizationCore/ACKLocalization.swift | 12 ++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 6585c27..9ce97e4 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,10 @@ let package = Package( targets: ["ACKLocalization"]), ], dependencies: [ - .package(url: "https://github.com/googleapis/google-auth-library-swift", from: "0.5.2"), + .package( + url: "https://github.com/googleapis/google-auth-library-swift", + .upToNextMajor(from: "0.5.2") + ), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/ACKLocalizationCore/ACKLocalization.swift b/Sources/ACKLocalizationCore/ACKLocalization.swift index c05d26f..95d3048 100644 --- a/Sources/ACKLocalizationCore/ACKLocalization.swift +++ b/Sources/ACKLocalizationCore/ACKLocalization.swift @@ -332,7 +332,11 @@ public final class ACKLocalization { serviceAccount: serviceAccount ) } else if let apiKey = config.apiKey { - return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, apiKey: apiKey) + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + apiKey: apiKey + ) } else if let serviceAccountPath = ProcessInfo.processInfo.environment[Constants.serviceAccountPath] { let serviceAccount = try loadServiceAccount(from: serviceAccountPath) return fetchSheetValues( @@ -342,7 +346,11 @@ public final class ACKLocalization { ) } else if let apiKey = ProcessInfo.processInfo.environment[Constants.apiKey] { let apiKey = APIKey(value: apiKey) - return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, apiKey: apiKey) + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + apiKey: apiKey + ) } else { let errorMessage = """ Unable to load API key or service account path. Please check if: From 1f7eae2149e3e052ae23a1c5f49e26c89a275d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:54:52 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=94=A7=20update=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ad995..701ef5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ## master +### Added +- Replaces custom solution for getting auth credentials with [google auth library](https://github.com/googleapis/google-auth-library-swift) ([#29](https://github.com/AckeeCZ/ACKLocalization/pull/36), kudos to @babacros) + ## 1.5.0 ## Added From ae9eeabc74617e4e4a8e37bed81e072c06993493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20Baba=CC=81c=CC=8Cek?= Date: Tue, 8 Nov 2022 17:55:42 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=90=9B=20fix=20PR=20number?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701ef5f..ed21872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ## master ### Added -- Replaces custom solution for getting auth credentials with [google auth library](https://github.com/googleapis/google-auth-library-swift) ([#29](https://github.com/AckeeCZ/ACKLocalization/pull/36), kudos to @babacros) +- Replaces custom solution for getting auth credentials with [google auth library](https://github.com/googleapis/google-auth-library-swift) ([#36](https://github.com/AckeeCZ/ACKLocalization/pull/36), kudos to @babacros) ## 1.5.0