Skip to content

Commit

Permalink
Registry: use issued_at field in the token response message (#65)
Browse files Browse the repository at this point in the history
* Registry: use issued_at field in the token response message

* Configure JSONDecoder instead of the Decodable itself
  • Loading branch information
edigaryev authored May 12, 2022
1 parent 3fdcc5c commit 8554e56
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 7 deletions.
34 changes: 27 additions & 7 deletions Sources/tart/OCI/Registry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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) {
Expand Down
38 changes: 38 additions & 0 deletions Tests/TartTests/TokenResponseTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 8554e56

Please sign in to comment.