Skip to content

Commit

Permalink
docs: improve docs for AuthError.APIError
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev committed Dec 14, 2023
1 parent ec07c95 commit 1b58372
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 14 deletions.
15 changes: 12 additions & 3 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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
)
Expand All @@ -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
Expand All @@ -566,6 +574,7 @@ public actor AuthClient {
accessToken: accessToken,
tokenType: "bearer",
expiresIn: expiresAt.timeIntervalSince(now),
expiresAt: expiresAt.timeIntervalSince1970,
refreshToken: refreshToken,
user: user
)
Expand Down
24 changes: 22 additions & 2 deletions Sources/Auth/AuthError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions Sources/Auth/Internal/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ struct Dependencies: Sendable {
var sessionStorage: SessionStorage
var sessionRefresher: SessionRefresher
var codeVerifierStorage: CodeVerifierStorage
var currentDate: @Sendable () -> Date = { Date() }
}
39 changes: 33 additions & 6 deletions Sources/Auth/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,43 +47,64 @@ 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(
providerToken: String? = nil,
providerRefreshToken: String? = nil,
accessToken: String,
tokenType: String,
expiresIn: Double,
expiresIn: TimeInterval,
expiresAt: TimeInterval?,
refreshToken: String,
weakPassword: WeakPassword? = nil,
user: User
) {
self.providerToken = providerToken
self.providerRefreshToken = providerRefreshToken
self.accessToken = accessToken
self.tokenType = tokenType
self.expiresIn = expiresIn
self.expiresAt = expiresAt
self.refreshToken = refreshToken
self.weakPassword = weakPassword
self.user = user
}

static let empty = Session(
accessToken: "",
tokenType: "",
expiresIn: 0,
expiresAt: nil,
refreshToken: "",
user: User(
id: UUID(),
Expand Down Expand Up @@ -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]
}
10 changes: 8 additions & 2 deletions Tests/AuthTests/AuthResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/AuthTests/MockHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
2 changes: 2 additions & 0 deletions Tests/AuthTests/Mocks/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ extension Session {
accessToken: "accesstoken",
tokenType: "bearer",
expiresIn: 120,
expiresAt: Date().addingTimeInterval(120).timeIntervalSince1970,
refreshToken: "refreshtoken",
user: User(fromMockNamed: "user")
)
Expand All @@ -105,6 +106,7 @@ extension Session {
accessToken: "accesstoken",
tokenType: "bearer",
expiresIn: 60,
expiresAt: Date().addingTimeInterval(60).timeIntervalSince1970,
refreshToken: "refreshtoken",
user: User(fromMockNamed: "user")
)
Expand Down
4 changes: 4 additions & 0 deletions Tests/AuthTests/RequestsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")
)
Expand Down

0 comments on commit 1b58372

Please sign in to comment.