From 8554e56e4baa8b31431d69858282e05a125d22d2 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Thu, 12 May 2022 22:33:02 +0300 Subject: [PATCH] Registry: use issued_at field in the token response message (#65) * Registry: use issued_at field in the token response message * Configure JSONDecoder instead of the Decodable itself --- Sources/tart/OCI/Registry.swift | 34 ++++++++++++++++----- Tests/TartTests/TokenResponseTests.swift | 38 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 Tests/TartTests/TokenResponseTests.swift diff --git a/Sources/tart/OCI/Registry.swift b/Sources/tart/OCI/Registry.swift index db662bc..88ae52e 100644 --- a/Sources/tart/OCI/Registry.swift +++ b/Sources/tart/OCI/Registry.swift @@ -8,11 +8,31 @@ enum RegistryError: Error { } struct TokenResponse: Decodable { - let creationTime = Date() - + let defaultIssuedAt = Date() + let defaultExpiresIn = 60 + var token: String - var expires_in: Int? - + var expiresIn: Int? + var issuedAt: Date? + + static func parse(fromData: Data) throws -> Self { + let decoder = JSONDecoder() + + decoder.keyDecodingStrategy = .convertFromSnakeCase + + // RFC3339 date formatter from Apple's documentation[1] + // + // [1]: https://developer.apple.com/documentation/foundation/dateformatter + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + decoder.dateDecodingStrategy = .formatted(dateFormatter) + + return try decoder.decode(TokenResponse.self, from: fromData) + } + var tokenExpiresAt: Date { get { // Tokens can expire and expire_in field is used to determine when: @@ -22,8 +42,8 @@ struct TokenResponse: Decodable { // >a token should never be returned with less than 60 seconds to live. // // [1]: https://docs.docker.com/registry/spec/auth/token/#requesting-a-token - - creationTime + TimeInterval(expires_in ?? 60) + + (issuedAt ?? defaultIssuedAt) + TimeInterval(expiresIn ?? defaultExpiresIn) } } @@ -233,7 +253,7 @@ class Registry { + "while retrieving an authentication token", details: String(decoding: tokenResponseRaw, as: UTF8.self)) } - currentAuthToken = try JSONDecoder().decode(TokenResponse.self, from: tokenResponseRaw) + currentAuthToken = try TokenResponse.parse(fromData: tokenResponseRaw) } private func authAwareRequest(request: URLRequest) async throws -> (Data, HTTPURLResponse) { diff --git a/Tests/TartTests/TokenResponseTests.swift b/Tests/TartTests/TokenResponseTests.swift new file mode 100644 index 0000000..eaf2672 --- /dev/null +++ b/Tests/TartTests/TokenResponseTests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import tart + +final class TokenResponseTests: XCTestCase { + func testBasic() throws { + let tokenResponseRaw = Data("{\"token\":\"some token\"}".utf8) + let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw) + + XCTAssertEqual(tokenResponse.token, "some token") + + let expectedTokenExpiresAtRange = Date()...Date().addingTimeInterval(60) + XCTAssertTrue(expectedTokenExpiresAtRange.contains(tokenResponse.tokenExpiresAt)) + + XCTAssertTrue(tokenResponse.isValid) + } + + func testExpirationBasic() throws { + let tokenResponseRaw = Data("{\"token\":\"some token\",\"expires_in\":2}".utf8) + let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw) + + XCTAssertEqual(tokenResponse.expiresIn, 2) + + let expectedTokenExpiresAtRange = Date()...Date().addingTimeInterval(2) + XCTAssertTrue(expectedTokenExpiresAtRange.contains(tokenResponse.tokenExpiresAt)) + + XCTAssertTrue(tokenResponse.isValid) + _ = XCTWaiter.wait(for: [expectation(description: "Wait 3 seconds for the token to become invalid")], timeout: 2) + XCTAssertFalse(tokenResponse.isValid) + } + + func testExpirationWithIssuedAt() throws { + let tokenResponseRaw = Data("{\"token\":\"some token\",\"expires_in\":3600,\"issued_at\":\"1970-01-01T00:00:00Z\"}".utf8) + let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw) + + XCTAssertEqual(Date(timeIntervalSince1970: 3600), tokenResponse.tokenExpiresAt) + XCTAssertFalse(tokenResponse.isValid) + } +}