OAuthProvider.swift 18 KB

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