diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 9b1a545d..141e06f9 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -599,17 +599,17 @@ public final class AuthClient: Sendable { /// Gets the session data from a OAuth2 callback URL. @discardableResult public func session(from url: URL) async throws -> Session { - if configuration.flowType == .implicit, !isImplicitGrantFlow(url: url) { + let params = extractParams(from: url) + + if configuration.flowType == .implicit, !isImplicitGrantFlow(params: params) { throw AuthError.invalidImplicitGrantFlowURL } - if configuration.flowType == .pkce, !isPKCEFlow(url: url) { + if configuration.flowType == .pkce, !isPKCEFlow(params: params) { throw AuthError.pkce(.invalidPKCEFlowURL) } - let params = extractParams(from: url) - - if isPKCEFlow(url: url) { + if isPKCEFlow(params: params) { guard let code = params["code"] else { throw AuthError.pkce(.codeVerifierNotFound) } @@ -1120,15 +1120,13 @@ public final class AuthClient: Sendable { return (codeChallenge, codeChallengeMethod) } - private func isImplicitGrantFlow(url: URL) -> Bool { - let fragments = extractParams(from: url) - return fragments["access_token"] != nil || fragments["error_description"] != nil + private func isImplicitGrantFlow(params: [String: String]) -> Bool { + params["access_token"] != nil || params["error_description"] != nil } - private func isPKCEFlow(url: URL) -> Bool { - let fragments = extractParams(from: url) + private func isPKCEFlow(params: [String: String]) -> Bool { let currentCodeVerifier = codeVerifierStorage.get() - return fragments["code"] != nil && currentCodeVerifier != nil + return params["code"] != nil && currentCodeVerifier != nil } private func getURLForProvider( diff --git a/Sources/Auth/AuthError.swift b/Sources/Auth/AuthError.swift index 1a3e41b2..f6424c6f 100644 --- a/Sources/Auth/AuthError.swift +++ b/Sources/Auth/AuthError.swift @@ -5,11 +5,15 @@ public enum AuthError: LocalizedError, Sendable, Equatable { case malformedJWT case sessionNotFound case api(APIError) + + /// Error thrown during PKCE flow. case pkce(PKCEFailureReason) + case invalidImplicitGrantFlowURL case missingURL case invalidRedirectScheme + /// An error returned by the API. public struct APIError: Error, Decodable, Sendable, Equatable { /// A basic message describing the problem with the request. Usually missing if /// ``AuthError/APIError/error`` is present. @@ -39,21 +43,32 @@ public enum AuthError: LocalizedError, Sendable, Equatable { } public enum PKCEFailureReason: Sendable { + /// Code verifier not found in the URL. case codeVerifierNotFound + + /// Not a valid PKCE flow URL. case invalidPKCEFlowURL } public var errorDescription: String? { switch self { case let .api(error): error.errorDescription ?? error.msg ?? error.error - case .missingExpClaim: "Missing expiration claim on access token." + case .missingExpClaim: "Missing expiration claim in the access token." case .malformedJWT: "A malformed JWT received." case .sessionNotFound: "Unable to get a valid session." - case .pkce(.codeVerifierNotFound): "A code verifier wasn't found in PKCE flow." - case .pkce(.invalidPKCEFlowURL): "Not a valid PKCE flow url." + case let .pkce(reason): reason.errorDescription case .invalidImplicitGrantFlowURL: "Not a valid implicit grant flow url." case .missingURL: "Missing URL." case .invalidRedirectScheme: "Invalid redirect scheme." } } } + +extension AuthError.PKCEFailureReason { + var errorDescription: String { + switch self { + case .codeVerifierNotFound: "A code verifier wasn't found in PKCE flow." + case .invalidPKCEFlowURL: "Not a valid PKCE flow url." + } + } +} diff --git a/Sources/Auth/Internal/Contants.swift b/Sources/Auth/Internal/Contants.swift new file mode 100644 index 00000000..308b544a --- /dev/null +++ b/Sources/Auth/Internal/Contants.swift @@ -0,0 +1,10 @@ +// +// Contants.swift +// +// +// Created by Guilherme Souza on 22/05/24. +// + +import Foundation + +let EXPIRY_MARGIN: TimeInterval = 30 diff --git a/Sources/Auth/Internal/SessionManager.swift b/Sources/Auth/Internal/SessionManager.swift index 12cae5ea..8e504028 100644 --- a/Sources/Auth/Internal/SessionManager.swift +++ b/Sources/Auth/Internal/SessionManager.swift @@ -36,7 +36,7 @@ private actor LiveSessionManager { throw AuthError.sessionNotFound } - if currentSession.isValid { + if !currentSession.isExpired { scheduleNextTokenRefresh(currentSession) return currentSession diff --git a/Sources/Auth/Internal/SessionStorage.swift b/Sources/Auth/Internal/SessionStorage.swift index da435195..97c7d3cc 100644 --- a/Sources/Auth/Internal/SessionStorage.swift +++ b/Sources/Auth/Internal/SessionStorage.swift @@ -19,12 +19,6 @@ struct StoredSession: Codable { } } -extension Session { - var isValid: Bool { - expiresAt - Date().timeIntervalSince1970 > 60 - } -} - extension AuthLocalStorage { func getSession() throws -> Session? { try retrieve(key: "supabase.session").flatMap { diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index 1e0f80cb..0b8eed65 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -15,6 +15,11 @@ public enum AuthChangeEvent: String, Sendable { case mfaChallengeVerified = "MFA_CHALLENGE_VERIFIED" } +@available( + *, + deprecated, + message: "Access to UserCredentials will be removed on the next major release." +) public struct UserCredentials: Codable, Hashable, Sendable { public var email: String? public var password: String? @@ -104,21 +109,13 @@ public struct Session: Codable, Hashable, Sendable { self.user = user } - static let empty = Session( - accessToken: "", - tokenType: "", - expiresIn: 0, - expiresAt: 0, - refreshToken: "", - user: User( - id: UUID(), - appMetadata: [:], - userMetadata: [:], - aud: "", - createdAt: Date(), - updatedAt: Date() - ) - ) + /// Returns `true` if the token is expired or will expire in the next 30 seconds. + /// + /// The 30 second buffer is to account for latency issues. + public var isExpired: Bool { + let expiresAt = Date(timeIntervalSince1970: expiresAt) + return expiresAt.timeIntervalSinceNow < EXPIRY_MARGIN + } } public struct User: Codable, Hashable, Identifiable, Sendable { @@ -513,7 +510,7 @@ public struct Factor: Identifiable, Codable, Hashable, Sendable { public let friendlyName: String? /// Type of factor. Only `totp` supported with this version but may change in future versions. - public let factorType: String + public let factorType: FactorType /// Factor's status. public let status: FactorStatus @@ -718,8 +715,7 @@ public struct ResendMobileResponse: Decodable, Hashable, Sendable { } public struct WeakPassword: Codable, Hashable, Sendable { - /// List of reasons the password is too weak, could be any of `length`, `characters`, or - /// `pwned`. + /// List of reasons the password is too weak, could be any of `length`, `characters`, or `pwned`. public let reasons: [String] } diff --git a/Sources/_Helpers/AnyJSON/AnyJSON.swift b/Sources/_Helpers/AnyJSON/AnyJSON.swift index 70ea165c..afe6ae73 100644 --- a/Sources/_Helpers/AnyJSON/AnyJSON.swift +++ b/Sources/_Helpers/AnyJSON/AnyJSON.swift @@ -22,8 +22,7 @@ public enum AnyJSON: Sendable, Codable, Hashable { /// Returns the underlying Swift value corresponding to the `AnyJSON` instance. /// - /// - Note: For `.object` and `.array` cases, the returned value contains recursively transformed - /// `AnyJSON` instances. + /// - Note: For `.object` and `.array` cases, the returned value contains recursively transformed `AnyJSON` instances. public var value: Any { switch self { case .null: NSNull() diff --git a/Tests/AuthTests/Mocks/Mocks.swift b/Tests/AuthTests/Mocks/Mocks.swift index 86efae9f..ab22ccef 100644 --- a/Tests/AuthTests/Mocks/Mocks.swift +++ b/Tests/AuthTests/Mocks/Mocks.swift @@ -28,8 +28,8 @@ extension Session { static let expiredSession = Session( accessToken: "accesstoken", tokenType: "bearer", - expiresIn: 60, - expiresAt: Date().addingTimeInterval(60).timeIntervalSince1970, + expiresIn: 30, + expiresAt: Date().addingTimeInterval(30).timeIntervalSince1970, refreshToken: "refreshtoken", user: User(fromMockNamed: "user") )