Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(auth): improve docs for auth #191

Merged
merged 1 commit into from
Dec 15, 2023
Merged
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
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