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

fix(auth): incorrect error when error occurs during PKCE flow #592

Merged
merged 1 commit into from
Nov 6, 2024
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
61 changes: 40 additions & 21 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@
method: .post,
query: [URLQueryItem(name: "grant_type", value: "password")],
body: configuration.encoder.encode(
UserCredentials(

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, TVOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, TVOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, WATCHOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 302 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, WATCHOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.
email: email,
password: password,
gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:))
Expand All @@ -326,7 +326,7 @@
method: .post,
query: [URLQueryItem(name: "grant_type", value: "password")],
body: configuration.encoder.encode(
UserCredentials(

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, TVOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, TVOS, 16.0)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.

Check warning on line 329 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, WATCHOS, 15.4)

'UserCredentials' is deprecated: Access to UserCredentials will be removed on the next major release.
password: password,
phone: phone,
gotrueMetaSecurity: captchaToken.map(AuthMetaSecurity.init(captchaToken:))
Expand Down Expand Up @@ -756,33 +756,32 @@
/// Gets the session data from a OAuth2 callback URL.
@discardableResult
public func session(from url: URL) async throws -> Session {
logger?.debug("received \(url)")
logger?.debug("Received URL: \(url)")

let params = extractParams(from: url)

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

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

if isPKCEFlow(params: params) {
guard let code = params["code"] else {
throw AuthError.pkceGrantCodeExchange(message: "No code detected.")
switch configuration.flowType {
case .implicit:
guard isImplicitGrantFlow(params: params) else {
throw AuthError.implicitGrantRedirect(
message: "Not a valid implicit grant flow URL: \(url)")
}
return try await handleImplicitGrantFlow(params: params)

let session = try await exchangeCodeForSession(authCode: code)
return session
case .pkce:
guard isPKCEFlow(params: params) else {
throw AuthError.pkceGrantCodeExchange(message: "Not a valid PKCE flow URL: \(url)")
}
return try await handlePKCEFlow(params: params)
}
}

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"
)
private func handleImplicitGrantFlow(params: [String: String]) async throws -> Session {
precondition(configuration.flowType == .implicit, "Method only allowed for implicit flow.")

if let errorDescription = params["error_description"] {
throw AuthError.implicitGrantRedirect(
message: errorDescription.replacingOccurrences(of: "+", with: " "))
}

guard
Expand Down Expand Up @@ -827,6 +826,25 @@
return session
}

private func handlePKCEFlow(params: [String: String]) async throws -> Session {
precondition(configuration.flowType == .pkce, "Method only allowed for PKCE flow.")

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

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

return try await exchangeCodeForSession(authCode: code)
}

/// Sets the session data from the current session. If the current session is expired, setSession
/// will take care of refreshing it to obtain a new session.
///
Expand Down Expand Up @@ -1304,7 +1322,8 @@

private func isPKCEFlow(params: [String: String]) -> Bool {
let currentCodeVerifier = codeVerifierStorage.get()
return params["code"] != nil && currentCodeVerifier != nil
return params["code"] != nil || params["error_description"] != nil || params["error"] != nil
|| params["error_code"] != nil && currentCodeVerifier != nil
}

private func getURLForProvider(
Expand Down Expand Up @@ -1381,7 +1400,7 @@
final class DefaultPresentationContextProvider: NSObject,
ASWebAuthenticationPresentationContextProviding
{
func presentationAnchor(for _: ASWebAuthenticationSession) -> ASPresentationAnchor {

Check warning on line 1403 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 15.4)

main actor-isolated instance method 'presentationAnchor(for:)' cannot be used to satisfy nonisolated protocol requirement

Check warning on line 1403 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 15.4)

main actor-isolated instance method 'presentationAnchor(for:)' cannot be used to satisfy nonisolated protocol requirement

Check warning on line 1403 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 15.4)

main actor-isolated instance method 'presentationAnchor(for:)' cannot be used to satisfy nonisolated protocol requirement

Check warning on line 1403 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 15.4)

main actor-isolated instance method 'presentationAnchor(for:)' cannot be used to satisfy nonisolated protocol requirement

Check warning on line 1403 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 15.4)

main actor-isolated instance method 'presentationAnchor(for:)' cannot be used to satisfy nonisolated protocol requirement

Check warning on line 1403 in Sources/Auth/AuthClient.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 15.4)

main actor-isolated instance method 'presentationAnchor(for:)' cannot be used to satisfy nonisolated protocol requirement
ASPresentationAnchor()
}
}
Expand Down
53 changes: 45 additions & 8 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
// Created by Guilherme Souza on 23/10/23.
//

@testable import Auth
import ConcurrencyExtras
import CustomDump
@testable import Helpers
import InlineSnapshotTesting
import TestHelpers
import XCTest

@testable import Auth
@testable import Helpers

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
Expand Down Expand Up @@ -126,7 +127,9 @@ final class AuthClientTests: XCTestCase {
message: "",
errorCode: .unknown,
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 404, httpVersion: nil, headerFields: nil)!
underlyingResponse: HTTPURLResponse(
url: URL(string: "http://localhost")!, statusCode: 404, httpVersion: nil,
headerFields: nil)!
)
}

Expand Down Expand Up @@ -157,7 +160,9 @@ final class AuthClientTests: XCTestCase {
message: "",
errorCode: .invalidCredentials,
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 401, httpVersion: nil, headerFields: nil)!
underlyingResponse: HTTPURLResponse(
url: URL(string: "http://localhost")!, statusCode: 401, httpVersion: nil,
headerFields: nil)!
)
}

Expand Down Expand Up @@ -188,7 +193,9 @@ final class AuthClientTests: XCTestCase {
message: "",
errorCode: .invalidCredentials,
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 403, httpVersion: nil, headerFields: nil)!
underlyingResponse: HTTPURLResponse(
url: URL(string: "http://localhost")!, statusCode: 403, httpVersion: nil,
headerFields: nil)!
)
}

Expand Down Expand Up @@ -277,13 +284,17 @@ final class AuthClientTests: XCTestCase {
response,
OAuthResponse(
provider: .github,
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")!
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"
)!
)
)
}

func testLinkIdentity() async throws {
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"
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"
let sut = makeSUT { _ in
.stub(
"""
Expand Down Expand Up @@ -312,7 +323,8 @@ final class AuthClientTests: XCTestCase {
fromFileName: "list-users-response",
headers: [
"X-Total-Count": "669",
"Link": "</admin/users?page=2&per_page=>; rel=\"next\", </admin/users?page=14&per_page=>; rel=\"last\"",
"Link":
"</admin/users?page=2&per_page=>; rel=\"next\", </admin/users?page=14&per_page=>; rel=\"last\"",
]
)
}
Expand Down Expand Up @@ -340,6 +352,31 @@ final class AuthClientTests: XCTestCase {
XCTAssertEqual(response.lastPage, 14)
}

func testSessionFromURL_withError() async throws {
sut = makeSUT()

Dependencies[sut.clientID].codeVerifierStorage.set("code-verifier")

let url = URL(
string:
"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"
)!

do {
try await sut.session(from: url)
XCTFail("Expect failure")
} catch {
expectNoDifference(
error as? AuthError,
AuthError.pkceGrantCodeExchange(
message: "Identity is already linked to another user",
error: "server_error",
code: "422"
)
)
}
}

private func makeSUT(
fetch: ((URLRequest) async throws -> HTTPResponse)? = nil
) -> AuthClient {
Expand Down
Loading