Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ let package = Package(
dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
"Helpers",
"Auth",
Expand Down
50 changes: 22 additions & 28 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,10 @@ public final class AuthClient: Sendable {

/// Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
public func exchangeCodeForSession(authCode: String) async throws -> Session {
guard let codeVerifier = codeVerifierStorage.get() else {
throw AuthError.pkce(.codeVerifierNotFound)
let codeVerifier = codeVerifierStorage.get()

if codeVerifier == nil {
logger?.error("code verifier not found, a code verifier should exist when calling this method.")
}

let session: Session = try await api.execute(
Expand Down Expand Up @@ -519,14 +521,10 @@ public final class AuthClient: Sendable {
queryParams: [(name: String, value: String?)] = [],
launchFlow: @MainActor @Sendable (_ url: URL) async throws -> URL
) async throws -> Session {
guard let redirectTo = (redirectTo ?? configuration.redirectToURL) else {
throw AuthError.invalidRedirectScheme
}

let url = try getOAuthSignInURL(
provider: provider,
scopes: scopes,
redirectTo: redirectTo,
redirectTo: redirectTo ?? configuration.redirectToURL,
queryParams: queryParams
)

Expand Down Expand Up @@ -566,8 +564,9 @@ public final class AuthClient: Sendable {
) { @MainActor url in
try await withCheckedThrowingContinuation { continuation in
guard let callbackScheme = (configuration.redirectToURL ?? redirectTo)?.scheme else {
continuation.resume(throwing: AuthError.invalidRedirectScheme)
return
preconditionFailure(
"Please, provide a valid redirect URL, either thorugh `redirectTo` param, or globally thorugh `AuthClient.Configuration.redirectToURL`."
)
}

#if !os(tvOS) && !os(watchOS)
Expand All @@ -583,7 +582,7 @@ public final class AuthClient: Sendable {
} else if let url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AuthError.missingURL)
fatalError("Expected url or error, but got none.")
}

#if !os(tvOS) && !os(watchOS)
Expand Down Expand Up @@ -674,24 +673,28 @@ public final class AuthClient: Sendable {
let params = extractParams(from: url)

if configuration.flowType == .implicit, !isImplicitGrantFlow(params: params) {
throw AuthError.invalidImplicitGrantFlowURL
throw AuthError.implicitGrantRedirect(message: "Not a valid implicit grant flow url: \(url)")
}

if configuration.flowType == .pkce, !isPKCEFlow(params: params) {
throw AuthError.pkce(.invalidPKCEFlowURL)
throw AuthError.pkceGrantCodeExchange(message: "Not a valid PKCE flow url: \(url)")
}

if isPKCEFlow(params: params) {
guard let code = params["code"] else {
throw AuthError.pkce(.codeVerifierNotFound)
throw AuthError.pkceGrantCodeExchange(message: "No code detected.")
}

let session = try await exchangeCodeForSession(authCode: code)
return session
}

if let errorDescription = params["error_description"] {
throw AuthError.api(.init(errorDescription: errorDescription))
if params["error"] != nil || params["error_description"] != nil || params["error_code"] != nil {
throw AuthError.pkceGrantCodeExchange(
message: params["error_description"] ?? "Error in URL with unspecified error_description.",
error: params["error"] ?? "unspecified_error",
code: params["error_code"] ?? "unspecified_code"
)
}

guard
Expand All @@ -700,7 +703,7 @@ public final class AuthClient: Sendable {
let refreshToken = params["refresh_token"],
let tokenType = params["token_type"]
else {
throw URLError(.badURL)
throw AuthError.implicitGrantRedirect(message: "No session defined in URL")
}

let expiresAt = params["expires_at"].flatMap(TimeInterval.init)
Expand Down Expand Up @@ -753,11 +756,9 @@ public final class AuthClient: Sendable {
var session: Session

let jwt = try decode(jwt: accessToken)
if let exp = jwt["exp"] as? TimeInterval {
if let exp = jwt?["exp"] as? TimeInterval {
expiresAt = Date(timeIntervalSince1970: exp)
hasExpired = expiresAt <= now
} else {
throw AuthError.missingExpClaim
}

if hasExpired {
Expand Down Expand Up @@ -803,16 +804,9 @@ public final class AuthClient: Sendable {
headers: [.init(name: "Authorization", value: "Bearer \(accessToken)")]
)
)
} catch {
} catch let AuthError.api(_, _, _, response) where [404, 403, 401].contains(response.statusCode) {
// ignore 404s since user might not exist anymore
// ignore 401s, and 403s since an invalid or expired JWT should sign out the current session.
let ignoredCodes = Set([404, 403, 401])

if case let AuthError.api(apiError) = error, let code = apiError.code,
!ignoredCodes.contains(code)
{
throw error
}
}
}

Expand Down Expand Up @@ -1169,7 +1163,7 @@ public final class AuthClient: Sendable {
@discardableResult
public func refreshSession(refreshToken: String? = nil) async throws -> Session {
guard let refreshToken = refreshToken ?? currentSession?.refreshToken else {
throw AuthError.sessionNotFound
throw AuthError.sessionMissing
}

return try await sessionManager.refreshSession(refreshToken)
Expand Down
Loading