Skip to content

Commit

Permalink
[Auth] Convert *Response classes to structs (#14012)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncooke3 authored Nov 4, 2024
1 parent 15eb852 commit cdaa05f
Show file tree
Hide file tree
Showing 41 changed files with 363 additions and 401 deletions.
177 changes: 60 additions & 117 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
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

0 comments on commit cdaa05f

Please sign in to comment.