OAuthProvider.swift 20 KB

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