Skip to content

Commit 84ce6f2

Browse files
authored
fix(auth): incorrect error when error occurs during PKCE flow (#592)
1 parent 1460660 commit 84ce6f2

File tree

2 files changed

+85
-29
lines changed

2 files changed

+85
-29
lines changed

Sources/Auth/AuthClient.swift

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -756,33 +756,32 @@ public final class AuthClient: Sendable {
756756
/// Gets the session data from a OAuth2 callback URL.
757757
@discardableResult
758758
public func session(from url: URL) async throws -> Session {
759-
logger?.debug("received \(url)")
759+
logger?.debug("Received URL: \(url)")
760760

761761
let params = extractParams(from: url)
762762

763-
if configuration.flowType == .implicit, !isImplicitGrantFlow(params: params) {
764-
throw AuthError.implicitGrantRedirect(message: "Not a valid implicit grant flow url: \(url)")
765-
}
766-
767-
if configuration.flowType == .pkce, !isPKCEFlow(params: params) {
768-
throw AuthError.pkceGrantCodeExchange(message: "Not a valid PKCE flow url: \(url)")
769-
}
770-
771-
if isPKCEFlow(params: params) {
772-
guard let code = params["code"] else {
773-
throw AuthError.pkceGrantCodeExchange(message: "No code detected.")
763+
switch configuration.flowType {
764+
case .implicit:
765+
guard isImplicitGrantFlow(params: params) else {
766+
throw AuthError.implicitGrantRedirect(
767+
message: "Not a valid implicit grant flow URL: \(url)")
774768
}
769+
return try await handleImplicitGrantFlow(params: params)
775770

776-
let session = try await exchangeCodeForSession(authCode: code)
777-
return session
771+
case .pkce:
772+
guard isPKCEFlow(params: params) else {
773+
throw AuthError.pkceGrantCodeExchange(message: "Not a valid PKCE flow URL: \(url)")
774+
}
775+
return try await handlePKCEFlow(params: params)
778776
}
777+
}
779778

780-
if params["error"] != nil || params["error_description"] != nil || params["error_code"] != nil {
781-
throw AuthError.pkceGrantCodeExchange(
782-
message: params["error_description"] ?? "Error in URL with unspecified error_description.",
783-
error: params["error"] ?? "unspecified_error",
784-
code: params["error_code"] ?? "unspecified_code"
785-
)
779+
private func handleImplicitGrantFlow(params: [String: String]) async throws -> Session {
780+
precondition(configuration.flowType == .implicit, "Method only allowed for implicit flow.")
781+
782+
if let errorDescription = params["error_description"] {
783+
throw AuthError.implicitGrantRedirect(
784+
message: errorDescription.replacingOccurrences(of: "+", with: " "))
786785
}
787786

788787
guard
@@ -827,6 +826,25 @@ public final class AuthClient: Sendable {
827826
return session
828827
}
829828

829+
private func handlePKCEFlow(params: [String: String]) async throws -> Session {
830+
precondition(configuration.flowType == .pkce, "Method only allowed for PKCE flow.")
831+
832+
if params["error"] != nil || params["error_description"] != nil || params["error_code"] != nil {
833+
throw AuthError.pkceGrantCodeExchange(
834+
message: params["error_description"]?.replacingOccurrences(of: "+", with: " ")
835+
?? "Error in URL with unspecified error_description.",
836+
error: params["error"] ?? "unspecified_error",
837+
code: params["error_code"] ?? "unspecified_code"
838+
)
839+
}
840+
841+
guard let code = params["code"] else {
842+
throw AuthError.pkceGrantCodeExchange(message: "No code detected.")
843+
}
844+
845+
return try await exchangeCodeForSession(authCode: code)
846+
}
847+
830848
/// Sets the session data from the current session. If the current session is expired, setSession
831849
/// will take care of refreshing it to obtain a new session.
832850
///
@@ -1304,7 +1322,8 @@ public final class AuthClient: Sendable {
13041322

13051323
private func isPKCEFlow(params: [String: String]) -> Bool {
13061324
let currentCodeVerifier = codeVerifierStorage.get()
1307-
return params["code"] != nil && currentCodeVerifier != nil
1325+
return params["code"] != nil || params["error_description"] != nil || params["error"] != nil
1326+
|| params["error_code"] != nil && currentCodeVerifier != nil
13081327
}
13091328

13101329
private func getURLForProvider(

Tests/AuthTests/AuthClientTests.swift

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
// Created by Guilherme Souza on 23/10/23.
66
//
77

8-
@testable import Auth
98
import ConcurrencyExtras
109
import CustomDump
11-
@testable import Helpers
1210
import InlineSnapshotTesting
1311
import TestHelpers
1412
import XCTest
1513

14+
@testable import Auth
15+
@testable import Helpers
16+
1617
#if canImport(FoundationNetworking)
1718
import FoundationNetworking
1819
#endif
@@ -126,7 +127,9 @@ final class AuthClientTests: XCTestCase {
126127
message: "",
127128
errorCode: .unknown,
128129
underlyingData: Data(),
129-
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 404, httpVersion: nil, headerFields: nil)!
130+
underlyingResponse: HTTPURLResponse(
131+
url: URL(string: "http://localhost")!, statusCode: 404, httpVersion: nil,
132+
headerFields: nil)!
130133
)
131134
}
132135

@@ -157,7 +160,9 @@ final class AuthClientTests: XCTestCase {
157160
message: "",
158161
errorCode: .invalidCredentials,
159162
underlyingData: Data(),
160-
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 401, httpVersion: nil, headerFields: nil)!
163+
underlyingResponse: HTTPURLResponse(
164+
url: URL(string: "http://localhost")!, statusCode: 401, httpVersion: nil,
165+
headerFields: nil)!
161166
)
162167
}
163168

@@ -188,7 +193,9 @@ final class AuthClientTests: XCTestCase {
188193
message: "",
189194
errorCode: .invalidCredentials,
190195
underlyingData: Data(),
191-
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 403, httpVersion: nil, headerFields: nil)!
196+
underlyingResponse: HTTPURLResponse(
197+
url: URL(string: "http://localhost")!, statusCode: 403, httpVersion: nil,
198+
headerFields: nil)!
192199
)
193200
}
194201

@@ -277,13 +284,17 @@ final class AuthClientTests: XCTestCase {
277284
response,
278285
OAuthResponse(
279286
provider: .github,
280-
url: URL(string: "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt")!
287+
url: URL(
288+
string:
289+
"https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt"
290+
)!
281291
)
282292
)
283293
}
284294

285295
func testLinkIdentity() async throws {
286-
let url = "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt"
296+
let url =
297+
"https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt"
287298
let sut = makeSUT { _ in
288299
.stub(
289300
"""
@@ -312,7 +323,8 @@ final class AuthClientTests: XCTestCase {
312323
fromFileName: "list-users-response",
313324
headers: [
314325
"X-Total-Count": "669",
315-
"Link": "</admin/users?page=2&per_page=>; rel=\"next\", </admin/users?page=14&per_page=>; rel=\"last\"",
326+
"Link":
327+
"</admin/users?page=2&per_page=>; rel=\"next\", </admin/users?page=14&per_page=>; rel=\"last\"",
316328
]
317329
)
318330
}
@@ -340,6 +352,31 @@ final class AuthClientTests: XCTestCase {
340352
XCTAssertEqual(response.lastPage, 14)
341353
}
342354

355+
func testSessionFromURL_withError() async throws {
356+
sut = makeSUT()
357+
358+
Dependencies[sut.clientID].codeVerifierStorage.set("code-verifier")
359+
360+
let url = URL(
361+
string:
362+
"https://my.redirect.com?error=server_error&error_code=422&error_description=Identity+is+already+linked+to+another+user#error=server_error&error_code=422&error_description=Identity+is+already+linked+to+another+user"
363+
)!
364+
365+
do {
366+
try await sut.session(from: url)
367+
XCTFail("Expect failure")
368+
} catch {
369+
expectNoDifference(
370+
error as? AuthError,
371+
AuthError.pkceGrantCodeExchange(
372+
message: "Identity is already linked to another user",
373+
error: "server_error",
374+
code: "422"
375+
)
376+
)
377+
}
378+
}
379+
343380
private func makeSUT(
344381
fetch: ((URLRequest) async throws -> HTTPResponse)? = nil
345382
) -> AuthClient {

0 commit comments

Comments
 (0)