OAuthProviderTests.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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 XCTest
  16. @testable import FirebaseAuth
  17. import FirebaseCore
  18. #if os(iOS)
  19. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  20. class OAuthProviderTests: RPCBaseTests {
  21. static let kFakeAuthorizedDomain = "test.firebaseapp.com"
  22. private let kFakeAccessToken = "fakeAccessToken"
  23. private let kFakeIDToken = "fakeIDToken"
  24. private let kFakeProviderID = "fakeProviderID"
  25. static let kFakeAPIKey = "asdfghjkl"
  26. static let kFakeEmulatorHost = "emulatorhost"
  27. static let kFakeEmulatorPort = 12345
  28. static let kFakeClientID = "123456.apps.googleusercontent.com"
  29. static let kFakeOAuthResponseURL = "fakeOAuthResponseURL"
  30. static let kFakeFirebaseAppID = "1:123456789:ios:123abc456def"
  31. static let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
  32. static let kFakeTenantID = "tenantID"
  33. static let kFakeReverseClientID = "com.googleusercontent.apps.123456"
  34. // Switches for testing different OAuth test flows
  35. // TODO: Consider using an enum for these instead.
  36. static var testTenantID = false
  37. static var testCancel = false
  38. static var testErrorString = false
  39. static var testInternalError = false
  40. static var testInvalidClientID = false
  41. static var testUnknownError = false
  42. static var testAppID = false
  43. static var testEmulator = false
  44. static var testAppCheck = false
  45. static var auth: Auth?
  46. /** @fn testObtainingOAuthCredentialNoIDToken
  47. @brief Tests the correct creation of an OAuthCredential without an IDToken.
  48. */
  49. func testObtainingOAuthCredentialNoIDToken() throws {
  50. initApp(#function)
  51. let credential = OAuthProvider.credential(withProviderID: kFakeProviderID,
  52. accessToken: kFakeAccessToken)
  53. XCTAssertEqual(credential.accessToken, kFakeAccessToken)
  54. XCTAssertEqual(credential.provider, kFakeProviderID)
  55. XCTAssertNil(credential.idToken)
  56. }
  57. /** @fn testObtainingOAuthCredentialWithFullName
  58. @brief Tests the correct creation of an OAuthCredential with a fullName.
  59. */
  60. func testObtainingOAuthCredentialWithFullName() throws {
  61. let kFakeGivenName = "Paul"
  62. let kFakeFamilyName = "B"
  63. var fullName = PersonNameComponents()
  64. fullName.givenName = kFakeGivenName
  65. fullName.familyName = kFakeFamilyName
  66. initApp(#function)
  67. let credential = OAuthProvider.appleCredential(withIDToken: kFakeIDToken, rawNonce: nil,
  68. fullName: fullName)
  69. XCTAssertEqual(credential.fullName, fullName)
  70. XCTAssertEqual(credential.provider, "apple.com")
  71. XCTAssertEqual(credential.idToken, kFakeIDToken)
  72. XCTAssertNil(credential.accessToken)
  73. }
  74. /** @fn testObtainingOAuthCredentialWithIDToken
  75. @brief Tests the correct creation of an OAuthCredential with an IDToken
  76. */
  77. func testObtainingOAuthCredentialWithIDToken() throws {
  78. initApp(#function)
  79. let credential = OAuthProvider.credential(withProviderID: kFakeProviderID,
  80. idToken: kFakeIDToken,
  81. accessToken: kFakeAccessToken)
  82. XCTAssertEqual(credential.accessToken, kFakeAccessToken)
  83. XCTAssertEqual(credential.provider, kFakeProviderID)
  84. XCTAssertEqual(credential.idToken, kFakeIDToken)
  85. }
  86. /** @fn testObtainingOAuthProvider
  87. @brief Tests the correct creation of an FIROAuthProvider instance.
  88. */
  89. func testObtainingOAuthProvider() throws {
  90. initApp(#function)
  91. let provider = OAuthProvider(providerID: kFakeProviderID, auth: OAuthProviderTests.auth!)
  92. XCTAssertEqual(provider.providerID, kFakeProviderID)
  93. }
  94. /** @fn testGetCredentialWithUIDelegateWithClientID
  95. @brief Tests a successful invocation of @c getCredentialWithUIDelegate
  96. */
  97. func testGetCredentialWithUIDelegateWithClientID() throws {
  98. initApp(#function)
  99. try testOAuthFlow(description: #function)
  100. }
  101. /** @fn testGetCredentialWithUIDelegateWithTenantID
  102. @brief Tests a successful invocation of @c getCredentialWithUIDelegate:completion:
  103. */
  104. func testGetCredentialWithUIDelegateWithTenantID() throws {
  105. initApp(#function)
  106. // Update tenantID on workqueue to enable _protectedDataDidBecomeAvailableObserver to finish
  107. // init.
  108. kAuthGlobalWorkQueue.sync {
  109. OAuthProviderTests.auth?.tenantID = OAuthProviderTests.kFakeTenantID
  110. }
  111. OAuthProviderTests.testTenantID = true
  112. try testOAuthFlow(description: #function)
  113. OAuthProviderTests.auth?.tenantID = nil
  114. OAuthProviderTests.testTenantID = false
  115. }
  116. /** @fn testGetCredentialWithUIDelegateUserCancellationWithClientID
  117. @brief Tests an unsuccessful invocation of @c testGetCredentialWithUIDelegateUserCancellationWithClientID due to user
  118. cancelation.
  119. */
  120. func testGetCredentialWithUIDelegateUserCancellationWithClientID() throws {
  121. initApp(#function)
  122. OAuthProviderTests.testCancel = true
  123. try testOAuthFlow(description: #function)
  124. OAuthProviderTests.testCancel = false
  125. }
  126. /** @fn testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID
  127. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to a
  128. failed network request within the web context.
  129. */
  130. func testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID() throws {
  131. initApp(#function)
  132. OAuthProviderTests.testErrorString = true
  133. try testOAuthFlow(description: #function)
  134. OAuthProviderTests.testErrorString = false
  135. }
  136. /** @fn testGetCredentialWithUIDelegateInternalErrorWithClientID
  137. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
  138. internal error within the web context.
  139. */
  140. func testGetCredentialWithUIDelegateInternalErrorWithClientID() throws {
  141. initApp(#function)
  142. OAuthProviderTests.testInternalError = true
  143. try testOAuthFlow(description: #function)
  144. OAuthProviderTests.testInternalError = false
  145. }
  146. /** @fn testGetCredentialWithUIDelegateInvalidClientID
  147. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
  148. use of an invalid client ID.
  149. */
  150. func testGetCredentialWithUIDelegateInvalidClientID() throws {
  151. initApp(#function)
  152. OAuthProviderTests.testInvalidClientID = true
  153. try testOAuthFlow(description: #function)
  154. OAuthProviderTests.testInvalidClientID = false
  155. }
  156. /** @fn testGetCredentialWithUIDelegateUnknownErrorWithClientID
  157. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
  158. unknown error.
  159. */
  160. func testGetCredentialWithUIDelegateUnknownErrorWithClientID() throws {
  161. initApp(#function)
  162. OAuthProviderTests.testUnknownError = true
  163. try testOAuthFlow(description: #function)
  164. OAuthProviderTests.testUnknownError = false
  165. }
  166. /** @fn testGetCredentialWithUIDelegateWithFirebaseAppID
  167. @brief Tests a successful invocation of @c getCredentialWithUIDelegate
  168. */
  169. func testGetCredentialWithUIDelegateWithFirebaseAppID() throws {
  170. initApp(#function, useAppID: true, omitClientID: true,
  171. scheme: OAuthProviderTests.kFakeEncodedFirebaseAppID)
  172. OAuthProviderTests.testAppID = true
  173. try testOAuthFlow(description: #function)
  174. OAuthProviderTests.testAppID = false
  175. }
  176. /** @fn testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent
  177. @brief Tests a successful invocation of @c getCredentialWithUIDelegate when the
  178. client ID is present in the plist file, but the encoded app ID is the registered custom URL
  179. scheme.
  180. */
  181. func testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent() throws {
  182. initApp(#function, useAppID: true, scheme: OAuthProviderTests.kFakeEncodedFirebaseAppID)
  183. OAuthProviderTests.testAppID = true
  184. try testOAuthFlow(description: #function)
  185. OAuthProviderTests.testAppID = false
  186. }
  187. /** @fn testGetCredentialWithUIDelegateUseEmulator
  188. @brief Tests a successful invocation of @c getCredentialWithUIDelegate when using the emulator.
  189. */
  190. func testGetCredentialWithUIDelegateUseEmulator() throws {
  191. initApp(#function, useAppID: true)
  192. OAuthProviderTests.auth?.requestConfiguration.emulatorHostAndPort =
  193. "\(OAuthProviderTests.kFakeEmulatorHost):\(OAuthProviderTests.kFakeEmulatorPort)"
  194. OAuthProviderTests.testEmulator = true
  195. try testOAuthFlow(description: #function)
  196. OAuthProviderTests.testEmulator = false
  197. }
  198. /** @fn testGetCredentialWithUIDelegateWithAppCheckToken
  199. @brief Tests a successful invocation of @c getCredentialWithUIDelegate
  200. */
  201. func testGetCredentialWithUIDelegateWithAppCheckToken() throws {
  202. let fakeAppCheck = FakeAppCheck()
  203. initApp(#function, useAppID: true)
  204. OAuthProviderTests.auth?.requestConfiguration.appCheck = fakeAppCheck
  205. OAuthProviderTests.testAppCheck = true
  206. try testOAuthFlow(description: #function)
  207. OAuthProviderTests.testAppCheck = false
  208. }
  209. /** @fn testOAuthCredentialCoding
  210. @brief Tests successful archiving and unarchiving of @c GoogleAuthCredential.
  211. */
  212. func testOAuthCredentialCoding() throws {
  213. let kAccessToken = "accessToken"
  214. let kIDToken = "idToken"
  215. let kRawNonce = "nonce"
  216. let kSecret = "sEcret"
  217. let kFullName = PersonNameComponents()
  218. let kPendingToken = "pendingToken"
  219. let credential = OAuthCredential(withProviderID: "dummyProvider",
  220. idToken: kIDToken,
  221. rawNonce: kRawNonce,
  222. accessToken: kAccessToken,
  223. secret: kSecret,
  224. fullName: kFullName,
  225. pendingToken: kPendingToken)
  226. XCTAssertTrue(OAuthCredential.supportsSecureCoding)
  227. let data = try NSKeyedArchiver.archivedData(
  228. withRootObject: credential,
  229. requiringSecureCoding: true
  230. )
  231. let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
  232. ofClasses: [OAuthCredential.self, NSPersonNameComponents.self], from: data
  233. ) as? OAuthCredential)
  234. XCTAssertEqual(unarchivedCredential.idToken, kIDToken)
  235. XCTAssertEqual(unarchivedCredential.rawNonce, kRawNonce)
  236. XCTAssertEqual(unarchivedCredential.accessToken, kAccessToken)
  237. XCTAssertEqual(unarchivedCredential.secret, kSecret)
  238. XCTAssertEqual(unarchivedCredential.fullName, kFullName)
  239. XCTAssertEqual(unarchivedCredential.pendingToken, kPendingToken)
  240. XCTAssertEqual(unarchivedCredential.provider, OAuthProvider.id)
  241. }
  242. private func initApp(_ functionName: String, useAppID: Bool = false, omitClientID: Bool = false,
  243. scheme: String = OAuthProviderTests.kFakeReverseClientID) {
  244. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  245. gcmSenderID: "00000000000000000-00000000000-000000000")
  246. options.apiKey = OAuthProviderTests.kFakeAPIKey
  247. options.projectID = "myProjectID"
  248. if useAppID {
  249. options.googleAppID = OAuthProviderTests.kFakeFirebaseAppID
  250. }
  251. if !omitClientID {
  252. options.clientID = OAuthProviderTests.kFakeClientID
  253. }
  254. let strippedName = functionName.replacingOccurrences(of: "(", with: "")
  255. .replacingOccurrences(of: ")", with: "")
  256. FirebaseApp.configure(name: strippedName, options: options)
  257. OAuthProviderTests.auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!)
  258. OAuthProviderTests.auth?.mainBundleUrlTypes =
  259. [["CFBundleURLSchemes": [scheme]]]
  260. }
  261. private func testOAuthFlow(description: String,
  262. with fakeAppCheck: FakeAppCheck? = nil) throws {
  263. let expectation = self.expectation(description: description)
  264. let provider = OAuthProvider(providerID: kFakeProviderID, auth: OAuthProviderTests.auth!)
  265. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  266. OAuthProviderTests.auth?.authURLPresenter = FakePresenter()
  267. // 1. Setup fakes and parameters for getCredential.
  268. if !OAuthProviderTests.testEmulator {
  269. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  270. rpcIssuer?.projectConfigRequester = { request in
  271. // 3. Validate the created Request instance.
  272. XCTAssertEqual(request.apiKey, OAuthProviderTests.kFakeAPIKey)
  273. XCTAssertEqual(request.endpoint, "getProjectConfig")
  274. // 4. Fulfill the expectation.
  275. projectConfigExpectation.fulfill()
  276. kAuthGlobalWorkQueue.async {
  277. do {
  278. // 5. Send the response from the fake backend.
  279. try self.rpcIssuer?
  280. .respond(withJSON: ["authorizedDomains": [OAuthProviderTests
  281. .kFakeAuthorizedDomain]])
  282. } catch {
  283. XCTFail("Failure sending response: \(error)")
  284. }
  285. }
  286. }
  287. }
  288. class FakePresenter: NSObject, AuthWebViewControllerDelegate {
  289. func webViewController(_ webViewController: AuthWebViewController,
  290. canHandle URL: URL) -> Bool {
  291. XCTFail("Do not call")
  292. return false
  293. }
  294. func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
  295. XCTFail("Do not call")
  296. }
  297. func webViewController(_ webViewController: AuthWebViewController,
  298. didFailWithError error: Error) {
  299. XCTFail("Do not call")
  300. }
  301. func present(_ presentURL: URL, uiDelegate UIDelegate: AuthUIDelegate?,
  302. callbackMatcher: @escaping (URL?) -> Bool,
  303. completion: @escaping (URL?, Error?) -> Void) {
  304. // 6. Verify flow triggers present in the FakePresenter class with the right parameters.
  305. if OAuthProviderTests.testEmulator {
  306. XCTAssertEqual(presentURL.scheme, "http")
  307. XCTAssertEqual(presentURL.host, OAuthProviderTests.kFakeEmulatorHost)
  308. XCTAssertEqual(presentURL.port, OAuthProviderTests.kFakeEmulatorPort)
  309. XCTAssertEqual(presentURL.path, "/emulator/auth/handler")
  310. } else {
  311. XCTAssertEqual(presentURL.scheme, "https")
  312. XCTAssertEqual(presentURL.host, OAuthProviderTests.kFakeAuthorizedDomain)
  313. XCTAssertEqual(presentURL.path, "/__/auth/handler")
  314. }
  315. let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
  316. XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
  317. if OAuthProviderTests.testAppID {
  318. XCTAssertEqual(params["appId"], OAuthProviderTests.kFakeFirebaseAppID)
  319. } else {
  320. XCTAssertEqual(params["clientId"], OAuthProviderTests.kFakeClientID)
  321. }
  322. XCTAssertEqual(params["apiKey"], OAuthProviderTests.kFakeAPIKey)
  323. XCTAssertEqual(params["authType"], "signInWithRedirect")
  324. XCTAssertNotNil(params["v"])
  325. if OAuthProviderTests.testTenantID {
  326. XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
  327. } else {
  328. XCTAssertNil(params["tid"])
  329. }
  330. let appCheckToken = presentURL.fragment
  331. let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
  332. XCTAssertEqual(appCheckToken, verifyAppCheckToken)
  333. // 7. Test callbackMatcher
  334. let kFakeRedirectStart = OAuthProviderTests
  335. .testAppID ? "app-1-123456789-ios-123abc456def" :
  336. OAuthProviderTests.kFakeReverseClientID
  337. let kFakeRedirectURLBase = kFakeRedirectStart + "://firebaseauth/" +
  338. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3F"
  339. var kFakeRedirectURLRest = "authType%3DsignInWithRedirect%26link%3D"
  340. if OAuthProviderTests.testInternalError {
  341. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253" +
  342. "A%2522auth%252Finternal-error%2522%252C%2522message%2522%253A%2522Internal%2520" +
  343. "error%2520.%2522%257D%26authType%3DsignInWithRedirect"
  344. } else if OAuthProviderTests.testInvalidClientID {
  345. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253A%2522auth" +
  346. "%252Finvalid-oauth-client-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%" +
  347. "2520ID%2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%" +
  348. "2520specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
  349. } else if OAuthProviderTests.testErrorString {
  350. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2" +
  351. "522%253A%2522auth%252Fnetwork-request-failed%2522%252C%2522message%2522%253A%2522The%" +
  352. "2520network%2520request%2520failed%2520.%2522%257D%26authType%3DsignInWithRedirect"
  353. } else if OAuthProviderTests.testUnknownError {
  354. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253A%2522auth%2" +
  355. "52Funknown-error-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%" +
  356. "2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520" +
  357. "specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
  358. }
  359. var redirectURL = "\(kFakeRedirectURLBase)\(kFakeRedirectURLRest)"
  360. // Add fake OAuthResponse to callback.
  361. if !OAuthProviderTests.testErrorString, !OAuthProviderTests.testInternalError,
  362. !OAuthProviderTests.testInvalidClientID, !OAuthProviderTests.testUnknownError {
  363. redirectURL += OAuthProviderTests.kFakeOAuthResponseURL
  364. }
  365. // Verify that the URL is rejected by the callback matcher without the event ID.
  366. XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
  367. // Verify that the URL is accepted by the callback matcher with the matching event ID.
  368. let redirectWithEventID =
  369. "\(redirectURL)%26eventId%3D\(params["eventId"] ?? "missingEventID")"
  370. let originalComponents = URLComponents(string: redirectWithEventID)!
  371. XCTAssertTrue(callbackMatcher(originalComponents.url))
  372. var components = originalComponents
  373. components.query = "https"
  374. XCTAssertFalse(callbackMatcher(components.url))
  375. components = originalComponents
  376. components.host = "badhost"
  377. XCTAssertFalse(callbackMatcher(components.url))
  378. components = originalComponents
  379. components.path = "badpath"
  380. XCTAssertFalse(callbackMatcher(components.url))
  381. components = originalComponents
  382. components.query = "badquery"
  383. XCTAssertFalse(callbackMatcher(components.url))
  384. // 8. Do the callback to the original call.
  385. kAuthGlobalWorkQueue.async {
  386. if OAuthProviderTests.testCancel {
  387. completion(nil, AuthErrorUtils.webContextCancelledError(message: nil))
  388. } else {
  389. completion(originalComponents.url, nil)
  390. }
  391. }
  392. }
  393. }
  394. // 2. Request the credential.
  395. provider.getCredentialWith(nil) { credential, error in
  396. // 9. After the response triggers the callback, verify the values in the callback credential
  397. XCTAssertTrue(Thread.isMainThread)
  398. if OAuthProviderTests.testCancel {
  399. XCTAssertNil(credential)
  400. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webContextCancelled.rawValue)
  401. } else if OAuthProviderTests.testErrorString {
  402. XCTAssertNil(credential)
  403. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webNetworkRequestFailed.rawValue)
  404. } else if OAuthProviderTests.testInternalError {
  405. XCTAssertNil(credential)
  406. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webInternalError.rawValue)
  407. } else if OAuthProviderTests.testInvalidClientID {
  408. XCTAssertNil(credential)
  409. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.invalidClientID.rawValue)
  410. } else if OAuthProviderTests.testUnknownError {
  411. XCTAssertNil(credential)
  412. XCTAssertEqual(
  413. (error as? NSError)?.code,
  414. AuthErrorCode.webSignInUserInteractionFailure.rawValue
  415. )
  416. } else {
  417. XCTAssertNil(error)
  418. let oAuthCredential = credential as? OAuthCredential
  419. XCTAssertEqual(
  420. oAuthCredential?.OAuthResponseURLString,
  421. OAuthProviderTests.kFakeOAuthResponseURL
  422. )
  423. }
  424. expectation.fulfill()
  425. }
  426. waitForExpectations(timeout: 5)
  427. }
  428. }
  429. #endif