OAuthProvider.swift 23 KB

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