OAuthProvider.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import CommonCrypto
  15. import Foundation
  16. /// Utility class for constructing OAuth Sign In credentials.
  17. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  18. @objc(FIROAuthProvider) open class OAuthProvider: NSObject, FederatedAuthProvider {
  19. @objc public static let id = "OAuth"
  20. /// Array used to configure the OAuth scopes.
  21. @objc open var scopes: [String]?
  22. /// Dictionary used to configure the OAuth custom parameters.
  23. @objc open var customParameters: [String: String]?
  24. /// The provider ID indicating the specific OAuth provider this OAuthProvider instance represents.
  25. @objc public let providerID: String
  26. /// An instance of OAuthProvider corresponding to the given provider ID.
  27. /// - Parameters:
  28. /// - providerID: The provider ID of the IDP for which this auth provider instance will be
  29. /// configured.
  30. /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID.
  31. #if !FIREBASE_CI
  32. @available(
  33. swift,
  34. deprecated: 0.01,
  35. message: "Use `provider(providerID: AuthProviderID) -> OAuthProvider` instead."
  36. )
  37. #endif // !FIREBASE_CI
  38. @objc(providerWithProviderID:) open class func provider(providerID: String) -> OAuthProvider {
  39. return OAuthProvider(providerID: providerID, auth: Auth.auth())
  40. }
  41. /// An instance of OAuthProvider corresponding to the given provider ID.
  42. /// - Parameters:
  43. /// - providerID: The provider ID of the IDP for which this auth provider instance will be
  44. /// configured.
  45. /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID.
  46. public class func provider(providerID: AuthProviderID) -> OAuthProvider {
  47. return OAuthProvider(providerID: providerID.rawValue, auth: Auth.auth())
  48. }
  49. /// An instance of OAuthProvider corresponding to the given provider ID and auth instance.
  50. /// - Parameters:
  51. /// - providerID: The provider ID of the IDP for which this auth provider instance will be
  52. /// configured.
  53. /// - auth: The auth instance to be associated with the OAuthProvider instance.
  54. /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID.
  55. #if !FIREBASE_CI
  56. @available(
  57. swift,
  58. deprecated: 0.01,
  59. message: "Use `provider(providerID: AuthProviderID, auth: Auth) -> OAuthProvider` instead."
  60. )
  61. #endif // !FIREBASE_CI
  62. @objc(providerWithProviderID:auth:) open class func provider(providerID: String,
  63. auth: Auth) -> OAuthProvider {
  64. return OAuthProvider(providerID: providerID, auth: auth)
  65. }
  66. /// An instance of OAuthProvider corresponding to the given provider ID and auth instance.
  67. /// - Parameters:
  68. /// - providerID: The provider ID of the IDP for which this auth provider instance will be
  69. /// configured.
  70. /// - auth: The auth instance to be associated with the OAuthProvider instance.
  71. /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID.
  72. public class func provider(providerID: AuthProviderID, auth: Auth) -> OAuthProvider {
  73. return OAuthProvider(providerID: providerID.rawValue, auth: auth)
  74. }
  75. /// Initializes an `OAuthProvider`.
  76. /// - Parameter providerID: The provider ID of the IDP for which this auth provider instance will
  77. /// be configured.
  78. /// - Parameter auth: The auth instance to be associated with the OAuthProvider instance.
  79. public init(providerID: String, auth: Auth = Auth.auth()) {
  80. if auth.requestConfiguration.emulatorHostAndPort == nil {
  81. if providerID == FacebookAuthProvider.id {
  82. fatalError("Sign in with Facebook is not supported via generic IDP; the Facebook TOS " +
  83. "dictate that you must use the Facebook iOS SDK for Facebook login.")
  84. }
  85. if providerID == AuthProviderID.apple.rawValue {
  86. fatalError("Sign in with Apple is not supported via generic IDP; You must use the Apple SDK" +
  87. " for Sign in with Apple.")
  88. }
  89. }
  90. self.auth = auth
  91. self.providerID = providerID
  92. scopes = [""]
  93. customParameters = [:]
  94. if let clientID = auth.app?.options.clientID {
  95. let reverseClientIDScheme = clientID.components(separatedBy: ".").reversed()
  96. .joined(separator: ".")
  97. if let urlTypes = auth.mainBundleUrlTypes,
  98. AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: reverseClientIDScheme,
  99. urlTypes: urlTypes) {
  100. callbackScheme = reverseClientIDScheme
  101. usingClientIDScheme = true
  102. return
  103. }
  104. }
  105. usingClientIDScheme = false
  106. if let appID = auth.app?.options.googleAppID {
  107. callbackScheme = "app-\(appID.replacingOccurrences(of: ":", with: "-"))"
  108. } else {
  109. fatalError("Missing googleAppID for constructing callbackScheme")
  110. }
  111. }
  112. /// Initializes an `OAuthProvider`.
  113. /// - Parameter providerID: The provider ID of the IDP for which this auth provider instance will
  114. /// be configured.
  115. /// - Parameter auth: The auth instance to be associated with the OAuthProvider instance.
  116. public convenience init(providerID: AuthProviderID, auth: Auth = Auth.auth()) {
  117. self.init(providerID: providerID.rawValue, auth: auth)
  118. }
  119. /// Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID, ID
  120. /// token, and access token.
  121. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  122. /// - Parameter idToken: The IDToken associated with the Auth credential being created.
  123. /// - Parameter accessToken: The access token associated with the Auth credential be created, if
  124. /// available.
  125. /// - Returns: An AuthCredential for the specified provider ID, ID token and access token.
  126. #if !FIREBASE_CI
  127. @available(
  128. swift,
  129. deprecated: 0.01,
  130. message: "Use `credential(providerID: AuthProviderID, idToken: String, accessToken: String? = nil) -> OAuthCredential` instead."
  131. )
  132. #endif // !FIREBASE_CI
  133. @objc(credentialWithProviderID:IDToken:accessToken:)
  134. public static func credential(withProviderID providerID: String,
  135. idToken: String,
  136. accessToken: String?) -> OAuthCredential {
  137. return OAuthCredential(withProviderID: providerID, idToken: idToken, accessToken: accessToken)
  138. }
  139. /// Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID, ID
  140. /// token, and access token.
  141. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  142. /// - Parameter idToken: The IDToken associated with the Auth credential being created.
  143. /// - Parameter accessToken: The access token associated with the Auth credential be created, if
  144. /// available.
  145. /// - Returns: An AuthCredential for the specified provider ID, ID token and access token.
  146. public static func credential(providerID: AuthProviderID,
  147. idToken: String,
  148. accessToken: String? = nil) -> OAuthCredential {
  149. return OAuthCredential(
  150. withProviderID: providerID.rawValue,
  151. idToken: idToken,
  152. accessToken: accessToken
  153. )
  154. }
  155. /// Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID, ID
  156. /// token, and access token.
  157. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  158. /// - Parameter accessToken: The access token associated with the Auth credential be created, if
  159. /// available.
  160. /// - Returns: An AuthCredential for the specified provider ID, ID token and access token.
  161. #if !FIREBASE_CI
  162. @available(
  163. swift,
  164. deprecated: 0.01,
  165. message: "Use `credential(providerID: AuthProviderID, accessToken: String) -> OAuthCredential` instead."
  166. )
  167. #endif // !FIREBASE_CI
  168. @objc(credentialWithProviderID:accessToken:)
  169. public static func credential(withProviderID providerID: String,
  170. accessToken: String) -> OAuthCredential {
  171. return OAuthCredential(withProviderID: providerID, accessToken: accessToken)
  172. }
  173. /// Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID using
  174. /// an ID token.
  175. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  176. /// - Parameter accessToken: The access token associated with the Auth credential be created
  177. /// - Returns: An AuthCredential.
  178. public static func credential(providerID: AuthProviderID,
  179. accessToken: String) -> OAuthCredential {
  180. return OAuthCredential(withProviderID: providerID.rawValue, accessToken: accessToken)
  181. }
  182. /// Creates an `AuthCredential` for that OAuth 2 provider identified by provider ID, ID
  183. /// token, raw nonce, and access token.
  184. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  185. /// - Parameter idToken: The IDToken associated with the Auth credential being created.
  186. /// - Parameter rawNonce: The raw nonce associated with the Auth credential being created.
  187. /// - Parameter accessToken: The access token associated with the Auth credential be created.
  188. /// - Returns: An AuthCredential for the specified provider ID, ID token and access token.
  189. #if !FIREBASE_CI
  190. @available(
  191. swift,
  192. deprecated: 0.01,
  193. message: "Use `credential(providerID: AuthProviderID, idToken: String, rawNonce: String, accessToken: String? = nil) -> OAuthCredential` instead."
  194. )
  195. #endif // !FIREBASE_CI
  196. @objc(credentialWithProviderID:IDToken:rawNonce:accessToken:)
  197. public static func credential(withProviderID providerID: String, idToken: String,
  198. rawNonce: String,
  199. accessToken: String) -> OAuthCredential {
  200. return OAuthCredential(
  201. withProviderID: providerID,
  202. idToken: idToken,
  203. rawNonce: rawNonce,
  204. accessToken: accessToken
  205. )
  206. }
  207. /// Creates an `AuthCredential` for that OAuth 2 provider identified by providerID using
  208. /// an ID token and raw nonce.
  209. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  210. /// - Parameter idToken: The IDToken associated with the Auth credential being created.
  211. /// - Parameter rawNonce: The raw nonce associated with the Auth credential being created.
  212. /// - Returns: An AuthCredential.
  213. #if !FIREBASE_CI
  214. @available(
  215. swift,
  216. deprecated: 0.01,
  217. message: "Use `credential(providerID: AuthProviderID, idToken: String, rawNonce: String, accessToken: String? = nil) -> OAuthCredential` instead."
  218. )
  219. #endif // !FIREBASE_CI
  220. @objc(credentialWithProviderID:IDToken:rawNonce:)
  221. public static func credential(withProviderID providerID: String, idToken: String,
  222. rawNonce: String) -> OAuthCredential {
  223. return OAuthCredential(withProviderID: providerID, idToken: idToken, rawNonce: rawNonce)
  224. }
  225. /// Creates an `AuthCredential` for that OAuth 2 provider identified by provider ID, ID
  226. /// token, raw nonce, and access token.
  227. /// - Parameter providerID: The provider ID associated with the Auth credential being created.
  228. /// - Parameter idToken: The IDToken associated with the Auth credential being created.
  229. /// - Parameter rawNonce: The raw nonce associated with the Auth credential being created.
  230. /// - Parameter accessToken: The access token associated with the Auth credential be created, if
  231. /// available.
  232. /// - Returns: An AuthCredential for the specified provider ID, ID token and access token.
  233. public static func credential(providerID: AuthProviderID, idToken: String,
  234. rawNonce: String,
  235. accessToken: String? = nil) -> OAuthCredential {
  236. return OAuthCredential(
  237. withProviderID: providerID.rawValue,
  238. idToken: idToken,
  239. rawNonce: rawNonce,
  240. accessToken: accessToken
  241. )
  242. }
  243. #if os(iOS)
  244. /// Used to obtain an auth credential via a mobile web flow.
  245. ///
  246. /// This method is available on iOS only.
  247. /// - Parameter uiDelegate: An optional UI delegate used to present the mobile web flow.
  248. /// - Parameter completion: Optionally; a block which is invoked asynchronously on the main
  249. /// thread when the mobile web flow is completed.
  250. open func getCredentialWith(_ uiDelegate: AuthUIDelegate?,
  251. completion: ((AuthCredential?, Error?) -> Void)? = nil) {
  252. guard let urlTypes = auth.mainBundleUrlTypes,
  253. AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
  254. urlTypes: urlTypes) else {
  255. fatalError(
  256. "Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
  257. )
  258. }
  259. kAuthGlobalWorkQueue.async { [weak self] in
  260. guard let self = self else { return }
  261. let eventID = AuthWebUtils.randomString(withLength: 10)
  262. let sessionID = AuthWebUtils.randomString(withLength: 10)
  263. let callbackOnMainThread: ((AuthCredential?, Error?) -> Void) = { credential, error in
  264. if let completion {
  265. DispatchQueue.main.async {
  266. completion(credential, error)
  267. }
  268. }
  269. }
  270. Task {
  271. do {
  272. guard let headfulLiteURL = try await self.getHeadfulLiteUrl(eventID: eventID,
  273. sessionID: sessionID) else {
  274. fatalError(
  275. "FirebaseAuth Internal Error: Both error and headfulLiteURL return are nil"
  276. )
  277. }
  278. let callbackMatcher: (URL?) -> Bool = { callbackURL in
  279. AuthWebUtils.isExpectedCallbackURL(callbackURL,
  280. eventID: eventID,
  281. authType: "signInWithRedirect",
  282. callbackScheme: self.callbackScheme)
  283. }
  284. self.auth.authURLPresenter.present(headfulLiteURL,
  285. uiDelegate: uiDelegate,
  286. callbackMatcher: callbackMatcher) { callbackURL, error in
  287. if let error {
  288. callbackOnMainThread(nil, error)
  289. return
  290. }
  291. guard let callbackURL else {
  292. fatalError("FirebaseAuth Internal Error: Both error and callbackURL return are nil")
  293. }
  294. let (oAuthResponseURLString, error) = self.oAuthResponseForURL(url: callbackURL)
  295. if let error {
  296. callbackOnMainThread(nil, error)
  297. return
  298. }
  299. guard let oAuthResponseURLString else {
  300. fatalError(
  301. "FirebaseAuth Internal Error: Both error and oAuthResponseURLString return are nil"
  302. )
  303. }
  304. let credential = OAuthCredential(withProviderID: self.providerID,
  305. sessionID: sessionID,
  306. OAuthResponseURLString: oAuthResponseURLString)
  307. callbackOnMainThread(credential, nil)
  308. }
  309. } catch {
  310. callbackOnMainThread(nil, error)
  311. }
  312. }
  313. }
  314. }
  315. /// Used to obtain an auth credential via a mobile web flow.
  316. /// This method is available on iOS only.
  317. /// - Parameter uiDelegate: An optional UI delegate used to present the mobile web flow.
  318. /// - Parameter completionHandler: Optionally; a block which is invoked
  319. /// asynchronously on the main thread when the mobile web flow is
  320. /// completed.
  321. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
  322. @objc(getCredentialWithUIDelegate:completion:)
  323. @MainActor
  324. open func credential(with uiDelegate: AuthUIDelegate?) async throws -> AuthCredential {
  325. return try await withCheckedThrowingContinuation { continuation in
  326. getCredentialWith(uiDelegate) { credential, error in
  327. if let credential = credential {
  328. continuation.resume(returning: credential)
  329. } else {
  330. continuation.resume(throwing: error!) // TODO: Change to ?? and generate unknown error
  331. }
  332. }
  333. }
  334. }
  335. #endif
  336. /// Creates an `AuthCredential` for the Sign in with Apple OAuth 2 provider identified by ID
  337. /// token, raw nonce, and full name.This method is specific to the Sign in with Apple OAuth 2
  338. /// provider as this provider requires the full name to be passed explicitly.
  339. /// - Parameter idToken: The IDToken associated with the Sign in with Apple Auth credential being
  340. /// created.
  341. /// - Parameter rawNonce: The raw nonce associated with the Sign in with Apple Auth credential
  342. /// being created.
  343. /// - Parameter fullName: The full name associated with the Sign in with Apple Auth credential
  344. /// being created.
  345. /// - Returns: An AuthCredential.
  346. @objc(appleCredentialWithIDToken:rawNonce:fullName:)
  347. public static func appleCredential(withIDToken idToken: String,
  348. rawNonce: String?,
  349. fullName: PersonNameComponents?) -> OAuthCredential {
  350. return OAuthCredential(withProviderID: AuthProviderID.apple.rawValue,
  351. idToken: idToken,
  352. rawNonce: rawNonce,
  353. fullName: fullName)
  354. }
  355. // MARK: - Private Methods
  356. /// Parses the redirected URL and returns a string representation of the OAuth response URL.
  357. /// - Parameter url: The url to be parsed for an OAuth response URL.
  358. /// - Returns: The OAuth response if successful.
  359. private func oAuthResponseForURL(url: URL) -> (String?, Error?) {
  360. var urlQueryItems = AuthWebUtils.dictionary(withHttpArgumentsString: url.query)
  361. if let item = urlQueryItems["deep_link_id"],
  362. let deepLinkURL = URL(string: item) {
  363. urlQueryItems = AuthWebUtils.dictionary(withHttpArgumentsString: deepLinkURL.query)
  364. if let queryItemLink = urlQueryItems["link"] {
  365. return (queryItemLink, nil)
  366. }
  367. }
  368. if let errorData = urlQueryItems["firebaseError"]?.data(using: .utf8) {
  369. do {
  370. let error = try JSONSerialization.jsonObject(with: errorData) as? [String: Any]
  371. let code = (error?["code"] as? String) ?? "missing code"
  372. let message = (error?["message"] as? String) ?? "missing message"
  373. return (nil, AuthErrorUtils.urlResponseError(code: code, message: message))
  374. } catch {
  375. return (nil, AuthErrorUtils.JSONSerializationError(underlyingError: error))
  376. }
  377. }
  378. return (nil, AuthErrorUtils.webSignInUserInteractionFailure(
  379. reason: "SignIn failed with unparseable firebaseError"
  380. ))
  381. }
  382. /// Constructs a URL used for opening a headful-lite flow using a given event
  383. /// ID and session ID.
  384. /// - Parameter eventID: The event ID used for this purpose.
  385. /// - Parameter sessionID: The session ID used when completing the headful lite flow.
  386. /// - Returns: A url.
  387. private func getHeadfulLiteUrl(eventID: String,
  388. sessionID: String) async throws -> URL? {
  389. let authDomain = try await AuthWebUtils
  390. .fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration, backend: auth.backend)
  391. let bundleID = Bundle.main.bundleIdentifier
  392. let clientID = auth.app?.options.clientID
  393. let appID = auth.app?.options.googleAppID
  394. let apiKey = auth.requestConfiguration.apiKey
  395. let tenantID = auth.tenantID
  396. let appCheck = auth.requestConfiguration.appCheck
  397. // TODO: Should we fail if these strings are empty? Only ibi was explicit in ObjC.
  398. var urlArguments = ["apiKey": apiKey,
  399. "authType": "signInWithRedirect",
  400. "ibi": bundleID ?? "",
  401. "sessionId": hash(forString: sessionID),
  402. "v": AuthBackend.authUserAgent(),
  403. "eventId": eventID,
  404. "providerId": providerID]
  405. if usingClientIDScheme {
  406. urlArguments["clientId"] = clientID
  407. } else {
  408. urlArguments["appId"] = appID
  409. }
  410. if let tenantID {
  411. urlArguments["tid"] = tenantID
  412. }
  413. if let scopes, scopes.count > 0 {
  414. urlArguments["scopes"] = scopes.joined(separator: ",")
  415. }
  416. if let customParameters, customParameters.count > 0 {
  417. do {
  418. let customParametersJSONData = try JSONSerialization
  419. .data(withJSONObject: customParameters)
  420. let rawJson = String(decoding: customParametersJSONData, as: UTF8.self)
  421. urlArguments["customParameters"] = rawJson
  422. } catch {
  423. throw AuthErrorUtils.JSONSerializationError(underlyingError: error)
  424. }
  425. }
  426. if let languageCode = auth.requestConfiguration.languageCode {
  427. urlArguments["hl"] = languageCode
  428. }
  429. let argumentsString = httpArgumentsString(forArgsDictionary: urlArguments)
  430. var urlString: String
  431. if (auth.requestConfiguration.emulatorHostAndPort) != nil {
  432. urlString = "http://\(authDomain)/emulator/auth/handler?\(argumentsString)"
  433. } else {
  434. urlString = "https://\(authDomain)/__/auth/handler?\(argumentsString)"
  435. }
  436. guard let percentEncoded = urlString.addingPercentEncoding(
  437. withAllowedCharacters: CharacterSet.urlFragmentAllowed
  438. ) else {
  439. fatalError("Internal Auth Error: failed to percent encode a string")
  440. }
  441. var components = URLComponents(string: percentEncoded)
  442. if let appCheck {
  443. let tokenResult = await appCheck.getToken(forcingRefresh: false)
  444. if let error = tokenResult.error {
  445. AuthLog.logWarning(code: "I-AUT000018",
  446. message: "Error getting App Check token; using placeholder " +
  447. "token instead. Error: \(error)")
  448. }
  449. let appCheckTokenFragment = "fac=\(tokenResult.token)"
  450. components?.fragment = appCheckTokenFragment
  451. }
  452. return components?.url
  453. }
  454. /// Returns the SHA256 hash representation of a given string object.
  455. /// - Parameter string: The string for which a SHA256 hash is desired.
  456. /// - Returns: An hexadecimal string representation of the SHA256 hash.
  457. private func hash(forString string: String) -> String {
  458. guard let sessionIdData = string.data(using: .utf8) as? NSData else {
  459. fatalError("FirebaseAuth Internal error: Failed to create hash for sessionID")
  460. }
  461. let digestLength = Int(CC_SHA256_DIGEST_LENGTH)
  462. var hash = [UInt8](repeating: 0, count: digestLength)
  463. CC_SHA256(sessionIdData.bytes, UInt32(sessionIdData.length), &hash)
  464. let dataHash = NSData(bytes: hash, length: digestLength)
  465. var bytes = [UInt8](repeating: 0, count: digestLength)
  466. dataHash.getBytes(&bytes, length: digestLength)
  467. var hexString = ""
  468. for byte in bytes {
  469. hexString += String(format: "%02x", UInt8(byte))
  470. }
  471. return hexString
  472. }
  473. private func httpArgumentsString(forArgsDictionary argsDictionary: [String: String]) -> String {
  474. var argsString: [String] = []
  475. for (key, value) in argsDictionary {
  476. let keyString = AuthWebUtils.string(byUnescapingFromURLArgument: key)
  477. let valueString = AuthWebUtils.string(byUnescapingFromURLArgument: value.description)
  478. argsString.append("\(keyString)=\(valueString)")
  479. }
  480. return argsString.joined(separator: "&")
  481. }
  482. private let auth: Auth
  483. private let callbackScheme: String
  484. private let usingClientIDScheme: Bool
  485. }