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

feat(auth): add support for error codes and refactor AuthError #518

Merged
merged 10 commits into from
Sep 6, 2024
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
Loading