Skip to content
Open
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
2 changes: 1 addition & 1 deletion Core/Sources/Configuration/OAuth/OAuthAccessToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// An access token returned from the authorization server used to authenticate against Google APIs.
public struct OAuthAccessToken: Codable {
public struct OAuthAccessToken: Codable, Sendable {
public let accessToken: String
public let tokenType: String
public let expiresIn: Int
Expand Down
2 changes: 1 addition & 1 deletion Core/Sources/Configuration/OAuth/OAuthPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct OAuthPayload: JWTPayload {
/// Using to nominate the account you want access to on the domain from a service account
var sub: String?

public func verify(using signer: JWTSigner) throws {
public func verify(using algorithm: some JWTAlgorithm) async throws {
try exp.verifyNotExpired()
}
}
53 changes: 31 additions & 22 deletions Core/Sources/Configuration/OAuth/OAuthServiceAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,40 +31,49 @@ public class OAuthServiceAccount: OAuthRefreshable {

// Google Documentation for this approach: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
public func refresh() -> EventLoopFuture<OAuthAccessToken> {
do {
let headers: HTTPHeaders = ["Content-Type": "application/x-www-form-urlencoded"]
let token = try generateJWT()
let body: HTTPClient.Body = .string("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(token)"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
let request = try HTTPClient.Request(url: GoogleOAuthTokenUrl, method: .POST, headers: headers, body: body)

return httpClient.execute(request: request, eventLoop: .delegate(on: self.eventLoop)).flatMap { response in

let promise = eventLoop.makePromise(of: OAuthAccessToken.self)

Task {
do {
let headers: HTTPHeaders = ["Content-Type": "application/x-www-form-urlencoded"]
let token = try await generateJWT()
let body: HTTPClient.Body = .string("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(token)"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
let request = try HTTPClient.Request(url: GoogleOAuthTokenUrl, method: .POST, headers: headers, body: body)

let response = try await httpClient.execute(request: request, eventLoop: .delegate(on: self.eventLoop)).get()

guard var byteBuffer = response.body,
let responseData = byteBuffer.readData(length: byteBuffer.readableBytes),
response.status == .ok else {
return self.eventLoop.makeFailedFuture(OauthRefreshError.noResponse(response.status))
}

do {
return self.eventLoop.makeSucceededFuture(try self.decoder.decode(OAuthAccessToken.self, from: responseData))
} catch {
return self.eventLoop.makeFailedFuture(error)
promise.fail(OauthRefreshError.noResponse(response.status))
return
}

let accessToken = try self.decoder.decode(OAuthAccessToken.self, from: responseData)
promise.succeed(accessToken)

} catch {
promise.fail(error)
}

} catch {
return self.eventLoop.makeFailedFuture(error)
}

return promise.futureResult
}

private func generateJWT() throws -> String {
private func generateJWT() async throws -> String {
let payload = OAuthPayload(iss: IssuerClaim(value: credentials.clientEmail),
scope: scope,
aud: AudienceClaim(value: GoogleOAuthTokenAudience),
exp: ExpirationClaim(value: Date().addingTimeInterval(3600)),
iat: IssuedAtClaim(value: Date()), sub: subscription)
let privateKey = try RSAKey.private(pem: credentials.privateKey.data(using: .utf8, allowLossyConversion: true) ?? Data())
return try JWTSigner.rs256(key: privateKey).sign(payload)

let privateKeyData = credentials.privateKey.data(using: .utf8, allowLossyConversion: true) ?? Data()
let privateKey = try Insecure.RSA.PrivateKey(pem: privateKeyData)

let keyCollection = JWTKeyCollection()
await keyCollection.add(rsa: privateKey, digestAlgorithm: .sha256)

return try await keyCollection.sign(payload)
}
}
64 changes: 63 additions & 1 deletion Core/Tests/CredentialTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,72 @@

import Foundation
import XCTest
import JWTKit

@testable import Core

final class CredentialTests: XCTestCase {

func testJWTGenerationDoesNotCrash() async throws {
// This test verifies that our JWT generation code with the new JWT Kit 5.x doesn't crash
// We use a valid RSA private key generated with OpenSSL

let validPrivateKey = """
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA2bP4t6JbghdRKsF7xd/4GvfTKc91AwK+xRk5wWWvHqZO70W3
4erjue0uBHePrh83wu1IGb4Xa6gFbV0g14h45Pyn/a3BLk+cJqpfUoIqmAhnOi83
bKMiQsIRFMQ2u2B/M8KCG/fE7X2b82oPObWI6Je/Ytwb3PtyLzCtvdBRMoi1JfoZ
/rd2hJlL24XXkCy4k0Y7xRHcZ1i1p5on5Z+ELJvzALPV1uwlJSH7iP5US1XDy4f7
nXzNj6UhDskCn0egDtfQV6xP1VG2iIBOVzsZMigT7rSPXdXH8UeES1HSwkE8asQa
6QmFm7NJEf68CnE1fuW8MzHII9v3BXfT1sMNYQIDAQABAoIBAFTjs6FPguU4WGMW
rT/cdK93YXTVO2hgIqlSi83Y669E5FSy1+AVKpVuYdpGENWxwJmW0t2O3S0SiIM7
pDnHMnT//DWUElcPnfEJ0D+pGBjOdgofLTqEZjCn4ec6F6l7GD7Dot5q//QnXa9N
9P/oxKkFuxA+ifLibYTvM2BnobHVGoUtQlULKyDip9VINETfxnAkL99OSZ0MZ9iC
qnpTqF5nPZLzzGhPW1+LBUW0T/cetIcQdtVbifed3dXJ05frzbbYcWjSCTS0+i/L
rKryipf4rOhXGo447Y+yM3+uivtX/WYJK8STun1AxX0Y9EFuXjSUse3XwkpVwST4
ZObhskECgYEA+SYIGouBr172hk1bifd3hdtcf+aRCiAK9gu4yQK8kLsQx2WnF3AV
SHUhhbZyJRVxQtYXn5qR7UE9ehhIgGtatcgp/nBSOUSniHz5MY6MSZedXz5BFreE
acX38mq+tcIX+Rl+dxdjO9s2R0I05n4fHIH8G54UDhDCYpEJZS1RZYkCgYEA37CR
ZyjIw7sAI323SRFZdCd+L0bvaBaocu9aGl2rxuomSIEpGZnXYEoy67jyXgnbxZya
Uv9RQ08JpHZCi7378fke/w96XVeSlFZM9KCKSpWwy0I/SmUmAPgX2ZWfW7Ekf0XJ
QNPUE/KQUEfqRxV3LSn2eYnZdJyPpIQLHnVJSxkCgYEA4xKIfDj9fyoboRfMABhs
9LCSw3cOZZ4Cn3Dbf0hhN79mcXTyLuhWXW1zmfxIWAgM7A9YBHzJ1uSI9UhAe9pc
GCVQMLeKGOu7jSfprgLvVPs70NxaUiv8ILLvYh9rpRg65SsZGc1VAe6ur49ly1TT
YhYOAdW3DYK0x0TMvUvqTZECgYEAnGi48vn4j6v1J9viyeugsfBfci1Wf2DAfkVQ
qnjvANJ+3Fm75FPG3mRjgKG8jvazvlSHMBuotbjRVDcAxveb8JEyFES9WgE+1AwY
GUEcEZTjnux+lsVtMmZHPvQ5DoMpsviYBYVYmG4WbJwse3HN+D2MQ2WZMMm8Qtu1
bqGyExkCgYEA9a9feZcQuANz+1ZnKl2a30osorE1XBwJR+dJlxqtbNs3vXqSJNC4
M3hbohiVBJfVLPrFq3i8m3lewZyNXQw/1b8w+EriHlDBCAAXfdT2Wqt0KK669c2z
s3Baj1fc55dq4cc8EiwvDNGD39xZBMJJDQ1YwGfvByN6bGjmFm9Cdwg=
-----END RSA PRIVATE KEY-----
"""

// Test that creating an OAuthPayload and JWT generation doesn't crash
let payload = OAuthPayload(
iss: IssuerClaim(value: "test@test-project.iam.gserviceaccount.com"),
scope: "https://www.googleapis.com/auth/cloud-platform",
aud: AudienceClaim(value: "https://oauth2.googleapis.com/token"),
exp: ExpirationClaim(value: Date().addingTimeInterval(3600)),
iat: IssuedAtClaim(value: Date()),
sub: nil
)

// This should not crash with the new JWT Kit 5.x API
XCTAssertNoThrow(try Insecure.RSA.PrivateKey(pem: validPrivateKey.data(using: String.Encoding.utf8) ?? Data()))

let privateKey = try Insecure.RSA.PrivateKey(pem: validPrivateKey.data(using: String.Encoding.utf8) ?? Data())
let keyCollection = JWTKeyCollection()
await keyCollection.add(rsa: privateKey, digestAlgorithm: .sha256)

// This should generate a valid JWT token
let token = try await keyCollection.sign(payload)
XCTAssertFalse(token.isEmpty)

// The token should have 3 parts separated by dots
let tokenParts = token.components(separatedBy: ".")
XCTAssertEqual(tokenParts.count, 3)
}

// var checkoutPath: String {
// if let path = ProcessInfo.processInfo.environment["PROJECT_PATH"] {
// return path
Expand Down Expand Up @@ -46,4 +108,4 @@ final class CredentialTests: XCTestCase {
// ("testLoadApplicationDefaultCredentials", testLoadApplicationDefaultCredentials),
// ("testLoadServiceAccount", testLoadServiceAccountCredentials)
// ]
}
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.18.0"),
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.13.0")
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.2.0")
],
targets: [
.target(
Expand Down
2 changes: 1 addition & 1 deletion Storage/Sources/API/StorageObjectAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ public final class GoogleCloudStorageObjectAPI: StorageObjectAPI {

var headers: HTTPHeaders = ["Content-Type": contentType]

if body.length == nil {
if body.contentLength == nil {
headers.add(name: "Transfer-Encoding", value: "chunked")
}

Expand Down