From 1b58372db3e890df266679adff6073c3f421fba2 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 14 Dec 2023 20:06:13 -0300 Subject: [PATCH] docs: improve docs for AuthError.APIError --- Sources/Auth/AuthClient.swift | 15 +++++++-- Sources/Auth/AuthError.swift | 24 +++++++++++++-- Sources/Auth/Internal/Dependencies.swift | 1 + Sources/Auth/Types.swift | 39 ++++++++++++++++++++---- Tests/AuthTests/AuthResponseTests.swift | 10 ++++-- Tests/AuthTests/MockHelpers.swift | 2 +- Tests/AuthTests/Mocks/Mocks.swift | 2 ++ Tests/AuthTests/RequestsTests.swift | 4 +++ 8 files changed, 83 insertions(+), 14 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index ed95bf02..cdadd3f1 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -71,6 +71,10 @@ public actor AuthClient { Dependencies.current.value!.eventEmitter } + private var currentDate: @Sendable () -> Date { + Dependencies.current.value!.currentDate + } + /// Returns the session, refreshing it if necessary. /// /// If no session can be found, a ``AuthError/sessionNotFound`` error is thrown. @@ -496,13 +500,16 @@ public actor AuthClient { guard let accessToken = params.first(where: { $0.name == "access_token" })?.value, - let expiresIn = params.first(where: { $0.name == "expires_in" })?.value, + let expiresIn = params.first(where: { $0.name == "expires_in" }).map(\.value) + .flatMap(TimeInterval.init), let refreshToken = params.first(where: { $0.name == "refresh_token" })?.value, let tokenType = params.first(where: { $0.name == "token_type" })?.value else { throw URLError(.badURL) } + let expiresAt = params.first(where: { $0.name == "expires_at" }).map(\.value) + .flatMap(TimeInterval.init) let providerToken = params.first(where: { $0.name == "provider_token" })?.value let providerRefreshToken = params.first(where: { $0.name == "provider_refresh_token" })?.value @@ -519,7 +526,8 @@ public actor AuthClient { providerRefreshToken: providerRefreshToken, accessToken: accessToken, tokenType: tokenType, - expiresIn: Double(expiresIn) ?? 0, + expiresIn: expiresIn, + expiresAt: expiresAt ?? currentDate().addingTimeInterval(expiresIn).timeIntervalSince1970, refreshToken: refreshToken, user: user ) @@ -545,7 +553,7 @@ public actor AuthClient { /// - Returns: A new valid session. @discardableResult public func setSession(accessToken: String, refreshToken: String) async throws -> Session { - let now = Date() + let now = currentDate() var expiresAt = now var hasExpired = true var session: Session @@ -566,6 +574,7 @@ public actor AuthClient { accessToken: accessToken, tokenType: "bearer", expiresIn: expiresAt.timeIntervalSince(now), + expiresAt: expiresAt.timeIntervalSince1970, refreshToken: refreshToken, user: user ) diff --git a/Sources/Auth/AuthError.swift b/Sources/Auth/AuthError.swift index 30140c78..26a5b4f2 100644 --- a/Sources/Auth/AuthError.swift +++ b/Sources/Auth/AuthError.swift @@ -9,11 +9,31 @@ public enum AuthError: LocalizedError, Sendable { case invalidImplicitGrantFlowURL public struct APIError: Error, Decodable, Sendable { - public var message: String? + /// A basic message describing the problem with the request. Usually missing if + /// ``AuthError/APIError/error`` is present. public var msg: String? + + /// The HTTP status code. Usually missing if ``AuthError/APIError/error`` is present. public var code: Int? + + /// Certain responses will contain this property with the provided values. + /// + /// Usually one of these: + /// - `invalid_request` + /// - `unauthorized_client` + /// - `access_denied` + /// - `server_error` + /// - `temporarily_unavailable` + /// - `unsupported_otp_type` public var error: String? + + /// Certain responses that have an ``AuthError/APIError/error`` property may have this property + /// which describes the error. public var errorDescription: String? + + /// Only returned when signing up if the password used is too weak. Inspect the + /// ``WeakPassword/reasons`` and ``AuthError/APIError/msg`` property to identify the causes. + public var weakPassword: WeakPassword? } public enum PKCEFailureReason: Sendable { @@ -23,10 +43,10 @@ public enum AuthError: LocalizedError, Sendable { public var errorDescription: String? { switch self { + case let .api(error): return error.errorDescription ?? error.msg ?? error.error case .missingExpClaim: return "Missing expiration claim on access token." case .malformedJWT: return "A malformed JWT received." case .sessionNotFound: return "Unable to get a valid session." - case let .api(error): return error.errorDescription ?? error.message ?? error.msg case .pkce(.codeVerifierNotFound): return "A code verifier wasn't found in PKCE flow." case .pkce(.invalidPKCEFlowURL): return "Not a valid PKCE flow url." case .invalidImplicitGrantFlowURL: diff --git a/Sources/Auth/Internal/Dependencies.swift b/Sources/Auth/Internal/Dependencies.swift index 77afe352..33e042ca 100644 --- a/Sources/Auth/Internal/Dependencies.swift +++ b/Sources/Auth/Internal/Dependencies.swift @@ -11,4 +11,5 @@ struct Dependencies: Sendable { var sessionStorage: SessionStorage var sessionRefresher: SessionRefresher var codeVerifierStorage: CodeVerifierStorage + var currentDate: @Sendable () -> Date = { Date() } } diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index 8d8328f4..768f4bf9 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -47,19 +47,35 @@ public struct Session: Codable, Hashable, Sendable { /// The oauth provider token. If present, this can be used to make external API requests to the /// oauth provider used. public var providerToken: String? + /// The oauth provider refresh token. If present, this can be used to refresh the provider_token /// via the oauth provider's API. Not all oauth providers return a provider refresh token. If the /// provider_refresh_token is missing, please refer to the oauth provider's documentation for /// information on how to obtain the provider refresh token. public var providerRefreshToken: String? - /// The access token jwt. It is recommended to set the JWT_EXPIRY to a shorter expiry value. + + /// A valid JWT that will expire in ``Session/expiresIn`` seconds. + /// It is recommended to set the `JWT_EXPIRY` to a shorter expiry value. public var accessToken: String + + /// What type of token this is. Only `bearer` returned, may change in the future. public var tokenType: String - /// The number of seconds until the token expires (since it was issued). Returned when a login is - /// confirmed. - public var expiresIn: Double - /// A one-time used refresh token that never expires. + + /// Number of seconds after which the ``Session/accessToken`` should be renewed by using the + /// refresh token with the `refresh_token` grant type. + public var expiresIn: TimeInterval + + /// UNIX timestamp after which the ``Session/accessToken`` should be renewed by using the refresh + /// token with the `refresh_token` grant type. + public var expiresAt: TimeInterval? + + /// An opaque string that can be used once to obtain a new access and refresh token. public var refreshToken: String + + /// Only returned on the `/token?grant_type=password` endpoint. When present, it indicates that + /// the password used is weak. Inspect the ``WeakPassword/reasons`` property to identify why. + public var weakPassword: WeakPassword? + public var user: User public init( @@ -67,8 +83,10 @@ public struct Session: Codable, Hashable, Sendable { providerRefreshToken: String? = nil, accessToken: String, tokenType: String, - expiresIn: Double, + expiresIn: TimeInterval, + expiresAt: TimeInterval?, refreshToken: String, + weakPassword: WeakPassword? = nil, user: User ) { self.providerToken = providerToken @@ -76,7 +94,9 @@ public struct Session: Codable, Hashable, Sendable { self.accessToken = accessToken self.tokenType = tokenType self.expiresIn = expiresIn + self.expiresAt = expiresAt self.refreshToken = refreshToken + self.weakPassword = weakPassword self.user = user } @@ -84,6 +104,7 @@ public struct Session: Codable, Hashable, Sendable { accessToken: "", tokenType: "", expiresIn: 0, + expiresAt: nil, refreshToken: "", user: User( id: UUID(), @@ -618,3 +639,9 @@ public struct ResendMobileResponse: Decodable, Hashable, Sendable { self.messageId = messageId } } + +public struct WeakPassword: Codable, Hashable, Sendable { + /// List of reasons the password is too weak, could be any of `length`, `characters`, or + /// `pwned`. + public let reasons: [String] +} diff --git a/Tests/AuthTests/AuthResponseTests.swift b/Tests/AuthTests/AuthResponseTests.swift index d5c2eb06..761ded44 100644 --- a/Tests/AuthTests/AuthResponseTests.swift +++ b/Tests/AuthTests/AuthResponseTests.swift @@ -4,13 +4,19 @@ import XCTest final class AuthResponseTests: XCTestCase { func testSession() throws { - let response = try JSONDecoder.goTrue.decode(AuthResponse.self, from: json(named: "session")) + let response = try AuthClient.Configuration.jsonDecoder.decode( + AuthResponse.self, + from: json(named: "session") + ) XCTAssertNotNil(response.session) XCTAssertEqual(response.user, response.session?.user) } func testUser() throws { - let response = try JSONDecoder.goTrue.decode(AuthResponse.self, from: json(named: "user")) + let response = try AuthClient.Configuration.jsonDecoder.decode( + AuthResponse.self, + from: json(named: "user") + ) XCTAssertNil(response.session) } } diff --git a/Tests/AuthTests/MockHelpers.swift b/Tests/AuthTests/MockHelpers.swift index 70c6d5fa..561341d4 100644 --- a/Tests/AuthTests/MockHelpers.swift +++ b/Tests/AuthTests/MockHelpers.swift @@ -9,6 +9,6 @@ func json(named name: String) -> Data { extension Decodable { init(fromMockNamed name: String) { - self = try! JSONDecoder.goTrue.decode(Self.self, from: json(named: name)) + self = try! AuthClient.Configuration.jsonDecoder.decode(Self.self, from: json(named: name)) } } diff --git a/Tests/AuthTests/Mocks/Mocks.swift b/Tests/AuthTests/Mocks/Mocks.swift index 84ba62fe..2851505e 100644 --- a/Tests/AuthTests/Mocks/Mocks.swift +++ b/Tests/AuthTests/Mocks/Mocks.swift @@ -97,6 +97,7 @@ extension Session { accessToken: "accesstoken", tokenType: "bearer", expiresIn: 120, + expiresAt: Date().addingTimeInterval(120).timeIntervalSince1970, refreshToken: "refreshtoken", user: User(fromMockNamed: "user") ) @@ -105,6 +106,7 @@ extension Session { accessToken: "accesstoken", tokenType: "bearer", expiresIn: 60, + expiresAt: Date().addingTimeInterval(60).timeIntervalSince1970, refreshToken: "refreshtoken", user: User(fromMockNamed: "user") ) diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index 136d240a..32725fd8 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -166,11 +166,14 @@ final class RequestsTests: XCTestCase { return (json(named: "user"), HTTPURLResponse()) }) + let currentDate = Date() + try await withDependencies { $0.sessionManager.update = { _ in } $0.sessionStorage.storeSession = { _ in } $0.codeVerifierStorage.getCodeVerifier = { nil } $0.eventEmitter = .live + $0.currentDate = { currentDate } } operation: { let url = URL( string: @@ -182,6 +185,7 @@ final class RequestsTests: XCTestCase { accessToken: "accesstoken", tokenType: "bearer", expiresIn: 60, + expiresAt: currentDate.addingTimeInterval(60).timeIntervalSince1970, refreshToken: "refreshtoken", user: User(fromMockNamed: "user") )