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

[Auth] Convert *Response classes to structs #14012

Merged
merged 10 commits into from
Nov 4, 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
177 changes: 60 additions & 117 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
ncooke3 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -22,82 +22,61 @@ import Foundation
#endif

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
protocol AuthBackendRPCIssuer {
/// Asynchronously send a HTTP request.
/// - Parameter request: The request to be made.
/// - Parameter body: Request body.
/// - Parameter contentType: Content type of the body.
/// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously
/// on the auth global work queue in the future.
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
body: Data?,
contentType: String) async -> (Data?, Error?)
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthBackendRPCIssuerImplementation: AuthBackendRPCIssuer {
let fetcherService: GTMSessionFetcherService

init() {
fetcherService = GTMSessionFetcherService()
fetcherService.userAgent = AuthBackend.authUserAgent()
fetcherService.callbackQueue = kAuthGlobalWorkQueue

// Avoid reusing the session to prevent
// https://github.com/firebase/firebase-ios-sdk/issues/1261
fetcherService.reuseSession = false
}

func asyncCallToURL<T: AuthRPCRequest>(with request: T,
body: Data?,
contentType: String) async -> (Data?, Error?) {
let requestConfiguration = request.requestConfiguration()
let request = await AuthBackend.request(withURL: request.requestURL(),
contentType: contentType,
requestConfiguration: requestConfiguration)
let fetcher = fetcherService.fetcher(with: request)
if let _ = requestConfiguration.emulatorHostAndPort {
fetcher.allowLocalhostRequest = true
fetcher.allowedInsecureSchemes = ["http"]
}
fetcher.bodyData = body

return await withUnsafeContinuation { continuation in
fetcher.beginFetch { data, error in
continuation.resume(returning: (data, error))
}
}
}
protocol AuthBackendProtocol {
func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthBackend {
class AuthBackend: AuthBackendProtocol {
static func authUserAgent() -> String {
return "FirebaseAuth.iOS/\(FirebaseVersion()) \(GTMFetcherStandardUserAgentString(nil))"
}

private static var realRPCBackend = AuthBackendRPCImplementation()
private static var gBackendImplementation = realRPCBackend
static func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
return try await shared.call(with: request)
}

class func setTestRPCIssuer(issuer: AuthBackendRPCIssuer) {
gBackendImplementation.rpcIssuer = issuer
static func setTestRPCIssuer(issuer: AuthBackendRPCIssuer) {
shared.rpcIssuer = issuer
}

class func resetRPCIssuer() {
gBackendImplementation.rpcIssuer = realRPCBackend.rpcIssuer
static func resetRPCIssuer() {
shared.rpcIssuer = AuthBackendRPCIssuer()
}

class func implementation() -> AuthBackendImplementation {
return gBackendImplementation
private static let shared: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer())

private var rpcIssuer: any AuthBackendRPCIssuerProtocol

init(rpcIssuer: any AuthBackendRPCIssuerProtocol) {
self.rpcIssuer = rpcIssuer
}

class func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
return try await implementation().call(with: request)
/// Calls the RPC using HTTP request.
/// Possible error responses:
/// * See FIRAuthInternalErrorCodeRPCRequestEncodingError
/// * See FIRAuthInternalErrorCodeJSONSerializationError
/// * See FIRAuthInternalErrorCodeNetworkError
/// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse
/// * See FIRAuthInternalErrorCodeUnexpectedResponse
/// * See FIRAuthInternalErrorCodeRPCResponseDecodingError
/// - Parameter request: The request.
/// - Returns: The response.
func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
let response = try await callInternal(with: request)
if let auth = request.requestConfiguration().auth,
let mfaError = Self.generateMFAError(response: response, auth: auth) {
throw mfaError
} else if let error = Self.phoneCredentialInUse(response: response) {
throw error
} else {
return response
}
}

class func request(withURL url: URL,
contentType: String,
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
static func request(withURL url: URL,
contentType: String,
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
// Kick off tasks for the async header values.
async let heartbeatsHeaderValue = requestConfiguration.heartbeatLogger?.asyncHeaderValue()
async let appCheckTokenHeaderValue = requestConfiguration.appCheck?
Expand Down Expand Up @@ -132,41 +111,11 @@ class AuthBackend {
}
return request
}
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
protocol AuthBackendImplementation {
func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
private class AuthBackendRPCImplementation: AuthBackendImplementation {
var rpcIssuer: AuthBackendRPCIssuer = AuthBackendRPCIssuerImplementation()

/// Calls the RPC using HTTP request.
/// Possible error responses:
/// * See FIRAuthInternalErrorCodeRPCRequestEncodingError
/// * See FIRAuthInternalErrorCodeJSONSerializationError
/// * See FIRAuthInternalErrorCodeNetworkError
/// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse
/// * See FIRAuthInternalErrorCodeUnexpectedResponse
/// * See FIRAuthInternalErrorCodeRPCResponseDecodingError
/// - Parameter request: The request.
/// - Returns: The response.
fileprivate func call<T: AuthRPCRequest>(with request: T) async throws -> T.Response {
let response = try await callInternal(with: request)
if let auth = request.requestConfiguration().auth,
let mfaError = Self.generateMFAError(response: response, auth: auth) {
throw mfaError
} else if let error = Self.phoneCredentialInUse(response: response) {
throw error
} else {
return response
}
}

#if os(iOS)
private class func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {
private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {
#if !os(iOS)
return nil
#else
if let mfaResponse = response as? AuthMFAResponse,
mfaResponse.idToken == nil,
let enrollments = mfaResponse.mfaInfo {
Expand All @@ -189,17 +138,15 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
} else {
return nil
}
}
#else
private class func generateMFAError(response: AuthRPCResponse, auth: Auth?) -> Error? {
return nil
}
#endif
#endif // !os(iOS)
}

#if os(iOS)
// Check whether or not the successful response is actually the special case phone
// auth flow that returns a temporary proof and phone number.
private class func phoneCredentialInUse(response: AuthRPCResponse) -> Error? {
// Check whether or not the successful response is actually the special case phone
// auth flow that returns a temporary proof and phone number.
private static func phoneCredentialInUse(response: AuthRPCResponse) -> Error? {
#if !os(iOS)
return nil
#else
if let phoneAuthResponse = response as? VerifyPhoneNumberResponse,
let phoneNumber = phoneAuthResponse.phoneNumber,
phoneNumber.count > 0,
Expand All @@ -214,12 +161,8 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
} else {
return nil
}
}
#else
private class func phoneCredentialInUse(response: AuthRPCResponse) -> Error? {
return nil
}
#endif
#endif // !os(iOS)
}

/// Calls the RPC using HTTP request.
///
Expand Down Expand Up @@ -308,7 +251,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
}
dictionary = decodedDictionary

let response = T.Response()
var response = T.Response()

// At this point we either have an error with successfully decoded
// details in the body, or we have a response which must pass further
Expand All @@ -318,7 +261,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
if error != nil {
if let errorDictionary = dictionary["error"] as? [String: AnyHashable] {
if let errorMessage = errorDictionary["message"] as? String {
if let clientError = AuthBackendRPCImplementation.clientError(
if let clientError = Self.clientError(
withServerErrorMessage: errorMessage,
errorDictionary: errorDictionary,
response: response,
Expand Down Expand Up @@ -351,7 +294,7 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
if let verifyAssertionRequest = request as? VerifyAssertionRequest {
if verifyAssertionRequest.returnIDPCredential {
if let errorMessage = dictionary["errorMessage"] as? String {
if let clientError = AuthBackendRPCImplementation.clientError(
if let clientError = Self.clientError(
withServerErrorMessage: errorMessage,
errorDictionary: dictionary,
response: response,
Expand All @@ -365,10 +308,10 @@ private class AuthBackendRPCImplementation: AuthBackendImplementation {
return response
}

private class func clientError(withServerErrorMessage serverErrorMessage: String,
errorDictionary: [String: Any],
response: AuthRPCResponse,
error: Error?) -> Error? {
private static func clientError(withServerErrorMessage serverErrorMessage: String,
errorDictionary: [String: Any],
response: AuthRPCResponse,
error: Error?) -> Error? {
let split = serverErrorMessage.split(separator: ":")
let shortErrorMessage = split.first?.trimmingCharacters(in: .whitespacesAndNewlines)
let serverDetailErrorMessage = String(split.count > 1 ? split[1] : "")
Expand Down
71 changes: 71 additions & 0 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackendRPCIssuer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import FirebaseCore
import FirebaseCoreExtension
import Foundation
#if COCOAPODS
import GTMSessionFetcher
#else
import GTMSessionFetcherCore
#endif

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
protocol AuthBackendRPCIssuerProtocol {
/// Asynchronously send a HTTP request.
/// - Parameter request: The request to be made.
/// - Parameter body: Request body.
/// - Parameter contentType: Content type of the body.
/// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously
/// on the auth global work queue in the future.
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
body: Data?,
contentType: String) async -> (Data?, Error?)
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthBackendRPCIssuer: AuthBackendRPCIssuerProtocol {
let fetcherService: GTMSessionFetcherService

init() {
fetcherService = GTMSessionFetcherService()
fetcherService.userAgent = AuthBackend.authUserAgent()
fetcherService.callbackQueue = kAuthGlobalWorkQueue

// Avoid reusing the session to prevent
// https://github.com/firebase/firebase-ios-sdk/issues/1261
fetcherService.reuseSession = false
}

func asyncCallToURL<T: AuthRPCRequest>(with request: T,
body: Data?,
contentType: String) async -> (Data?, Error?) {
let requestConfiguration = request.requestConfiguration()
let request = await AuthBackend.request(withURL: request.requestURL(),
contentType: contentType,
requestConfiguration: requestConfiguration)
let fetcher = fetcherService.fetcher(with: request)
if let _ = requestConfiguration.emulatorHostAndPort {
fetcher.allowLocalhostRequest = true
fetcher.allowedInsecureSchemes = ["http"]
}
fetcher.bodyData = body

return await withUnsafeContinuation { continuation in
fetcher.beginFetch { data, error in
continuation.resume(returning: (data, error))
}
}
}
}
4 changes: 2 additions & 2 deletions FirebaseAuth/Sources/Swift/Backend/AuthRPCResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@

import Foundation

protocol AuthRPCResponse {
protocol AuthRPCResponse: Sendable {
/// Bare initializer for a response.
init()

/// Sets the response instance from the decoded JSON response.
/// - Parameter dictionary: The dictionary decoded from HTTP JSON response.
/// - Parameter error: An out field for an error which occurred constructing the request.
/// - Returns: Whether the operation was successful or not.
func setFields(dictionary: [String: AnyHashable]) throws
mutating func setFields(dictionary: [String: AnyHashable]) throws

/// This optional method allows response classes to create client errors given a short error
/// message and a detail error message from the server.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import Foundation

/// Represents the parameters for the createAuthUri endpoint.
/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri

class CreateAuthURIResponse: AuthRPCResponse {
struct CreateAuthURIResponse: AuthRPCResponse {
/// The URI used by the IDP to authenticate the user.
var authURI: String?

Expand All @@ -36,10 +35,7 @@ class CreateAuthURIResponse: AuthRPCResponse {
/// A list of sign-in methods available for the passed identifier.
var signinMethods: [String] = []

/// Bare initializer.
required init() {}

func setFields(dictionary: [String: AnyHashable]) throws {
mutating func setFields(dictionary: [String: AnyHashable]) throws {
providerID = dictionary["providerId"] as? String
authURI = dictionary["authUri"] as? String
registered = dictionary["registered"] as? Bool ?? false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import Foundation
/// Represents the response from the deleteAccount endpoint.
///
/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount
class DeleteAccountResponse: AuthRPCResponse {
required init() {}

func setFields(dictionary: [String: AnyHashable]) throws {}
struct DeleteAccountResponse: AuthRPCResponse {
mutating func setFields(dictionary: [String: AnyHashable]) throws {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
import Foundation

/// Represents the response from the emailLinkSignin endpoint.
class EmailLinkSignInResponse: AuthRPCResponse, AuthMFAResponse {
required init() {}

struct EmailLinkSignInResponse: AuthRPCResponse, AuthMFAResponse {
/// The ID token in the email link sign-in response.
private(set) var idToken: String?

Expand All @@ -42,7 +40,7 @@ class EmailLinkSignInResponse: AuthRPCResponse, AuthMFAResponse {
/// Info on which multi-factor authentication providers are enabled.
private(set) var mfaInfo: [AuthProtoMFAEnrollment]?

func setFields(dictionary: [String: AnyHashable]) throws {
mutating func setFields(dictionary: [String: AnyHashable]) throws {
email = dictionary["email"] as? String
idToken = dictionary["idToken"] as? String
isNewUser = dictionary["isNewUser"] as? Bool ?? false
Expand Down
Loading
Loading