GSI - Employe Self Service Mobile
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1608 lines
59 KiB

2 months ago
  1. import { _getProvider, getApp, _registerComponent, registerVersion } from '@firebase/app';
  2. import { Component } from '@firebase/component';
  3. import { Deferred, ErrorFactory, isIndexedDBAvailable, uuidv4, getGlobal, base64, issuedAtTime, calculateBackoffMillis, getModularInstance } from '@firebase/util';
  4. import { Logger } from '@firebase/logger';
  5. /**
  6. * @license
  7. * Copyright 2020 Google LLC
  8. *
  9. * Licensed under the Apache License, Version 2.0 (the "License");
  10. * you may not use this file except in compliance with the License.
  11. * You may obtain a copy of the License at
  12. *
  13. * http://www.apache.org/licenses/LICENSE-2.0
  14. *
  15. * Unless required by applicable law or agreed to in writing, software
  16. * distributed under the License is distributed on an "AS IS" BASIS,
  17. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. * See the License for the specific language governing permissions and
  19. * limitations under the License.
  20. */
  21. const APP_CHECK_STATES = new Map();
  22. const DEFAULT_STATE = {
  23. activated: false,
  24. tokenObservers: []
  25. };
  26. const DEBUG_STATE = {
  27. initialized: false,
  28. enabled: false
  29. };
  30. /**
  31. * Gets a reference to the state object.
  32. */
  33. function getStateReference(app) {
  34. return APP_CHECK_STATES.get(app) || Object.assign({}, DEFAULT_STATE);
  35. }
  36. /**
  37. * Set once on initialization. The map should hold the same reference to the
  38. * same object until this entry is deleted.
  39. */
  40. function setInitialState(app, state) {
  41. APP_CHECK_STATES.set(app, state);
  42. return APP_CHECK_STATES.get(app);
  43. }
  44. function getDebugState() {
  45. return DEBUG_STATE;
  46. }
  47. /**
  48. * @license
  49. * Copyright 2020 Google LLC
  50. *
  51. * Licensed under the Apache License, Version 2.0 (the "License");
  52. * you may not use this file except in compliance with the License.
  53. * You may obtain a copy of the License at
  54. *
  55. * http://www.apache.org/licenses/LICENSE-2.0
  56. *
  57. * Unless required by applicable law or agreed to in writing, software
  58. * distributed under the License is distributed on an "AS IS" BASIS,
  59. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  60. * See the License for the specific language governing permissions and
  61. * limitations under the License.
  62. */
  63. const BASE_ENDPOINT = 'https://content-firebaseappcheck.googleapis.com/v1';
  64. const EXCHANGE_RECAPTCHA_TOKEN_METHOD = 'exchangeRecaptchaV3Token';
  65. const EXCHANGE_RECAPTCHA_ENTERPRISE_TOKEN_METHOD = 'exchangeRecaptchaEnterpriseToken';
  66. const EXCHANGE_DEBUG_TOKEN_METHOD = 'exchangeDebugToken';
  67. const TOKEN_REFRESH_TIME = {
  68. /**
  69. * The offset time before token natural expiration to run the refresh.
  70. * This is currently 5 minutes.
  71. */
  72. OFFSET_DURATION: 5 * 60 * 1000,
  73. /**
  74. * This is the first retrial wait after an error. This is currently
  75. * 30 seconds.
  76. */
  77. RETRIAL_MIN_WAIT: 30 * 1000,
  78. /**
  79. * This is the maximum retrial wait, currently 16 minutes.
  80. */
  81. RETRIAL_MAX_WAIT: 16 * 60 * 1000
  82. };
  83. /**
  84. * One day in millis, for certain error code backoffs.
  85. */
  86. const ONE_DAY = 24 * 60 * 60 * 1000;
  87. /**
  88. * @license
  89. * Copyright 2020 Google LLC
  90. *
  91. * Licensed under the Apache License, Version 2.0 (the "License");
  92. * you may not use this file except in compliance with the License.
  93. * You may obtain a copy of the License at
  94. *
  95. * http://www.apache.org/licenses/LICENSE-2.0
  96. *
  97. * Unless required by applicable law or agreed to in writing, software
  98. * distributed under the License is distributed on an "AS IS" BASIS,
  99. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  100. * See the License for the specific language governing permissions and
  101. * limitations under the License.
  102. */
  103. /**
  104. * Port from auth proactiverefresh.js
  105. *
  106. */
  107. // TODO: move it to @firebase/util?
  108. // TODO: allow to config whether refresh should happen in the background
  109. class Refresher {
  110. constructor(operation, retryPolicy, getWaitDuration, lowerBound, upperBound) {
  111. this.operation = operation;
  112. this.retryPolicy = retryPolicy;
  113. this.getWaitDuration = getWaitDuration;
  114. this.lowerBound = lowerBound;
  115. this.upperBound = upperBound;
  116. this.pending = null;
  117. this.nextErrorWaitInterval = lowerBound;
  118. if (lowerBound > upperBound) {
  119. throw new Error('Proactive refresh lower bound greater than upper bound!');
  120. }
  121. }
  122. start() {
  123. this.nextErrorWaitInterval = this.lowerBound;
  124. this.process(true).catch(() => {
  125. /* we don't care about the result */
  126. });
  127. }
  128. stop() {
  129. if (this.pending) {
  130. this.pending.reject('cancelled');
  131. this.pending = null;
  132. }
  133. }
  134. isRunning() {
  135. return !!this.pending;
  136. }
  137. async process(hasSucceeded) {
  138. this.stop();
  139. try {
  140. this.pending = new Deferred();
  141. await sleep(this.getNextRun(hasSucceeded));
  142. // Why do we resolve a promise, then immediate wait for it?
  143. // We do it to make the promise chain cancellable.
  144. // We can call stop() which rejects the promise before the following line execute, which makes
  145. // the code jump to the catch block.
  146. // TODO: unit test this
  147. this.pending.resolve();
  148. await this.pending.promise;
  149. this.pending = new Deferred();
  150. await this.operation();
  151. this.pending.resolve();
  152. await this.pending.promise;
  153. this.process(true).catch(() => {
  154. /* we don't care about the result */
  155. });
  156. }
  157. catch (error) {
  158. if (this.retryPolicy(error)) {
  159. this.process(false).catch(() => {
  160. /* we don't care about the result */
  161. });
  162. }
  163. else {
  164. this.stop();
  165. }
  166. }
  167. }
  168. getNextRun(hasSucceeded) {
  169. if (hasSucceeded) {
  170. // If last operation succeeded, reset next error wait interval and return
  171. // the default wait duration.
  172. this.nextErrorWaitInterval = this.lowerBound;
  173. // Return typical wait duration interval after a successful operation.
  174. return this.getWaitDuration();
  175. }
  176. else {
  177. // Get next error wait interval.
  178. const currentErrorWaitInterval = this.nextErrorWaitInterval;
  179. // Double interval for next consecutive error.
  180. this.nextErrorWaitInterval *= 2;
  181. // Make sure next wait interval does not exceed the maximum upper bound.
  182. if (this.nextErrorWaitInterval > this.upperBound) {
  183. this.nextErrorWaitInterval = this.upperBound;
  184. }
  185. return currentErrorWaitInterval;
  186. }
  187. }
  188. }
  189. function sleep(ms) {
  190. return new Promise(resolve => {
  191. setTimeout(resolve, ms);
  192. });
  193. }
  194. /**
  195. * @license
  196. * Copyright 2020 Google LLC
  197. *
  198. * Licensed under the Apache License, Version 2.0 (the "License");
  199. * you may not use this file except in compliance with the License.
  200. * You may obtain a copy of the License at
  201. *
  202. * http://www.apache.org/licenses/LICENSE-2.0
  203. *
  204. * Unless required by applicable law or agreed to in writing, software
  205. * distributed under the License is distributed on an "AS IS" BASIS,
  206. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  207. * See the License for the specific language governing permissions and
  208. * limitations under the License.
  209. */
  210. const ERRORS = {
  211. ["already-initialized" /* AppCheckError.ALREADY_INITIALIZED */]: 'You have already called initializeAppCheck() for FirebaseApp {$appName} with ' +
  212. 'different options. To avoid this error, call initializeAppCheck() with the ' +
  213. 'same options as when it was originally called. This will return the ' +
  214. 'already initialized instance.',
  215. ["use-before-activation" /* AppCheckError.USE_BEFORE_ACTIVATION */]: 'App Check is being used before initializeAppCheck() is called for FirebaseApp {$appName}. ' +
  216. 'Call initializeAppCheck() before instantiating other Firebase services.',
  217. ["fetch-network-error" /* AppCheckError.FETCH_NETWORK_ERROR */]: 'Fetch failed to connect to a network. Check Internet connection. ' +
  218. 'Original error: {$originalErrorMessage}.',
  219. ["fetch-parse-error" /* AppCheckError.FETCH_PARSE_ERROR */]: 'Fetch client could not parse response.' +
  220. ' Original error: {$originalErrorMessage}.',
  221. ["fetch-status-error" /* AppCheckError.FETCH_STATUS_ERROR */]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
  222. ["storage-open" /* AppCheckError.STORAGE_OPEN */]: 'Error thrown when opening storage. Original error: {$originalErrorMessage}.',
  223. ["storage-get" /* AppCheckError.STORAGE_GET */]: 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.',
  224. ["storage-set" /* AppCheckError.STORAGE_WRITE */]: 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.',
  225. ["recaptcha-error" /* AppCheckError.RECAPTCHA_ERROR */]: 'ReCAPTCHA error.',
  226. ["throttled" /* AppCheckError.THROTTLED */]: `Requests throttled due to {$httpStatus} error. Attempts allowed again after {$time}`
  227. };
  228. const ERROR_FACTORY = new ErrorFactory('appCheck', 'AppCheck', ERRORS);
  229. /**
  230. * @license
  231. * Copyright 2020 Google LLC
  232. *
  233. * Licensed under the Apache License, Version 2.0 (the "License");
  234. * you may not use this file except in compliance with the License.
  235. * You may obtain a copy of the License at
  236. *
  237. * http://www.apache.org/licenses/LICENSE-2.0
  238. *
  239. * Unless required by applicable law or agreed to in writing, software
  240. * distributed under the License is distributed on an "AS IS" BASIS,
  241. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  242. * See the License for the specific language governing permissions and
  243. * limitations under the License.
  244. */
  245. function getRecaptcha(isEnterprise = false) {
  246. var _a;
  247. if (isEnterprise) {
  248. return (_a = self.grecaptcha) === null || _a === void 0 ? void 0 : _a.enterprise;
  249. }
  250. return self.grecaptcha;
  251. }
  252. function ensureActivated(app) {
  253. if (!getStateReference(app).activated) {
  254. throw ERROR_FACTORY.create("use-before-activation" /* AppCheckError.USE_BEFORE_ACTIVATION */, {
  255. appName: app.name
  256. });
  257. }
  258. }
  259. function getDurationString(durationInMillis) {
  260. const totalSeconds = Math.round(durationInMillis / 1000);
  261. const days = Math.floor(totalSeconds / (3600 * 24));
  262. const hours = Math.floor((totalSeconds - days * 3600 * 24) / 3600);
  263. const minutes = Math.floor((totalSeconds - days * 3600 * 24 - hours * 3600) / 60);
  264. const seconds = totalSeconds - days * 3600 * 24 - hours * 3600 - minutes * 60;
  265. let result = '';
  266. if (days) {
  267. result += pad(days) + 'd:';
  268. }
  269. if (hours) {
  270. result += pad(hours) + 'h:';
  271. }
  272. result += pad(minutes) + 'm:' + pad(seconds) + 's';
  273. return result;
  274. }
  275. function pad(value) {
  276. if (value === 0) {
  277. return '00';
  278. }
  279. return value >= 10 ? value.toString() : '0' + value;
  280. }
  281. /**
  282. * @license
  283. * Copyright 2020 Google LLC
  284. *
  285. * Licensed under the Apache License, Version 2.0 (the "License");
  286. * you may not use this file except in compliance with the License.
  287. * You may obtain a copy of the License at
  288. *
  289. * http://www.apache.org/licenses/LICENSE-2.0
  290. *
  291. * Unless required by applicable law or agreed to in writing, software
  292. * distributed under the License is distributed on an "AS IS" BASIS,
  293. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  294. * See the License for the specific language governing permissions and
  295. * limitations under the License.
  296. */
  297. async function exchangeToken({ url, body }, heartbeatServiceProvider) {
  298. const headers = {
  299. 'Content-Type': 'application/json'
  300. };
  301. // If heartbeat service exists, add heartbeat header string to the header.
  302. const heartbeatService = heartbeatServiceProvider.getImmediate({
  303. optional: true
  304. });
  305. if (heartbeatService) {
  306. const heartbeatsHeader = await heartbeatService.getHeartbeatsHeader();
  307. if (heartbeatsHeader) {
  308. headers['X-Firebase-Client'] = heartbeatsHeader;
  309. }
  310. }
  311. const options = {
  312. method: 'POST',
  313. body: JSON.stringify(body),
  314. headers
  315. };
  316. let response;
  317. try {
  318. response = await fetch(url, options);
  319. }
  320. catch (originalError) {
  321. throw ERROR_FACTORY.create("fetch-network-error" /* AppCheckError.FETCH_NETWORK_ERROR */, {
  322. originalErrorMessage: originalError === null || originalError === void 0 ? void 0 : originalError.message
  323. });
  324. }
  325. if (response.status !== 200) {
  326. throw ERROR_FACTORY.create("fetch-status-error" /* AppCheckError.FETCH_STATUS_ERROR */, {
  327. httpStatus: response.status
  328. });
  329. }
  330. let responseBody;
  331. try {
  332. // JSON parsing throws SyntaxError if the response body isn't a JSON string.
  333. responseBody = await response.json();
  334. }
  335. catch (originalError) {
  336. throw ERROR_FACTORY.create("fetch-parse-error" /* AppCheckError.FETCH_PARSE_ERROR */, {
  337. originalErrorMessage: originalError === null || originalError === void 0 ? void 0 : originalError.message
  338. });
  339. }
  340. // Protobuf duration format.
  341. // https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/Duration
  342. const match = responseBody.ttl.match(/^([\d.]+)(s)$/);
  343. if (!match || !match[2] || isNaN(Number(match[1]))) {
  344. throw ERROR_FACTORY.create("fetch-parse-error" /* AppCheckError.FETCH_PARSE_ERROR */, {
  345. originalErrorMessage: `ttl field (timeToLive) is not in standard Protobuf Duration ` +
  346. `format: ${responseBody.ttl}`
  347. });
  348. }
  349. const timeToLiveAsNumber = Number(match[1]) * 1000;
  350. const now = Date.now();
  351. return {
  352. token: responseBody.token,
  353. expireTimeMillis: now + timeToLiveAsNumber,
  354. issuedAtTimeMillis: now
  355. };
  356. }
  357. function getExchangeRecaptchaV3TokenRequest(app, reCAPTCHAToken) {
  358. const { projectId, appId, apiKey } = app.options;
  359. return {
  360. url: `${BASE_ENDPOINT}/projects/${projectId}/apps/${appId}:${EXCHANGE_RECAPTCHA_TOKEN_METHOD}?key=${apiKey}`,
  361. body: {
  362. 'recaptcha_v3_token': reCAPTCHAToken
  363. }
  364. };
  365. }
  366. function getExchangeRecaptchaEnterpriseTokenRequest(app, reCAPTCHAToken) {
  367. const { projectId, appId, apiKey } = app.options;
  368. return {
  369. url: `${BASE_ENDPOINT}/projects/${projectId}/apps/${appId}:${EXCHANGE_RECAPTCHA_ENTERPRISE_TOKEN_METHOD}?key=${apiKey}`,
  370. body: {
  371. 'recaptcha_enterprise_token': reCAPTCHAToken
  372. }
  373. };
  374. }
  375. function getExchangeDebugTokenRequest(app, debugToken) {
  376. const { projectId, appId, apiKey } = app.options;
  377. return {
  378. url: `${BASE_ENDPOINT}/projects/${projectId}/apps/${appId}:${EXCHANGE_DEBUG_TOKEN_METHOD}?key=${apiKey}`,
  379. body: {
  380. // eslint-disable-next-line
  381. debug_token: debugToken
  382. }
  383. };
  384. }
  385. /**
  386. * @license
  387. * Copyright 2020 Google LLC
  388. *
  389. * Licensed under the Apache License, Version 2.0 (the "License");
  390. * you may not use this file except in compliance with the License.
  391. * You may obtain a copy of the License at
  392. *
  393. * http://www.apache.org/licenses/LICENSE-2.0
  394. *
  395. * Unless required by applicable law or agreed to in writing, software
  396. * distributed under the License is distributed on an "AS IS" BASIS,
  397. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  398. * See the License for the specific language governing permissions and
  399. * limitations under the License.
  400. */
  401. const DB_NAME = 'firebase-app-check-database';
  402. const DB_VERSION = 1;
  403. const STORE_NAME = 'firebase-app-check-store';
  404. const DEBUG_TOKEN_KEY = 'debug-token';
  405. let dbPromise = null;
  406. function getDBPromise() {
  407. if (dbPromise) {
  408. return dbPromise;
  409. }
  410. dbPromise = new Promise((resolve, reject) => {
  411. try {
  412. const request = indexedDB.open(DB_NAME, DB_VERSION);
  413. request.onsuccess = event => {
  414. resolve(event.target.result);
  415. };
  416. request.onerror = event => {
  417. var _a;
  418. reject(ERROR_FACTORY.create("storage-open" /* AppCheckError.STORAGE_OPEN */, {
  419. originalErrorMessage: (_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message
  420. }));
  421. };
  422. request.onupgradeneeded = event => {
  423. const db = event.target.result;
  424. // We don't use 'break' in this switch statement, the fall-through
  425. // behavior is what we want, because if there are multiple versions between
  426. // the old version and the current version, we want ALL the migrations
  427. // that correspond to those versions to run, not only the last one.
  428. // eslint-disable-next-line default-case
  429. switch (event.oldVersion) {
  430. case 0:
  431. db.createObjectStore(STORE_NAME, {
  432. keyPath: 'compositeKey'
  433. });
  434. }
  435. };
  436. }
  437. catch (e) {
  438. reject(ERROR_FACTORY.create("storage-open" /* AppCheckError.STORAGE_OPEN */, {
  439. originalErrorMessage: e === null || e === void 0 ? void 0 : e.message
  440. }));
  441. }
  442. });
  443. return dbPromise;
  444. }
  445. function readTokenFromIndexedDB(app) {
  446. return read(computeKey(app));
  447. }
  448. function writeTokenToIndexedDB(app, token) {
  449. return write(computeKey(app), token);
  450. }
  451. function writeDebugTokenToIndexedDB(token) {
  452. return write(DEBUG_TOKEN_KEY, token);
  453. }
  454. function readDebugTokenFromIndexedDB() {
  455. return read(DEBUG_TOKEN_KEY);
  456. }
  457. async function write(key, value) {
  458. const db = await getDBPromise();
  459. const transaction = db.transaction(STORE_NAME, 'readwrite');
  460. const store = transaction.objectStore(STORE_NAME);
  461. const request = store.put({
  462. compositeKey: key,
  463. value
  464. });
  465. return new Promise((resolve, reject) => {
  466. request.onsuccess = _event => {
  467. resolve();
  468. };
  469. transaction.onerror = event => {
  470. var _a;
  471. reject(ERROR_FACTORY.create("storage-set" /* AppCheckError.STORAGE_WRITE */, {
  472. originalErrorMessage: (_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message
  473. }));
  474. };
  475. });
  476. }
  477. async function read(key) {
  478. const db = await getDBPromise();
  479. const transaction = db.transaction(STORE_NAME, 'readonly');
  480. const store = transaction.objectStore(STORE_NAME);
  481. const request = store.get(key);
  482. return new Promise((resolve, reject) => {
  483. request.onsuccess = event => {
  484. const result = event.target.result;
  485. if (result) {
  486. resolve(result.value);
  487. }
  488. else {
  489. resolve(undefined);
  490. }
  491. };
  492. transaction.onerror = event => {
  493. var _a;
  494. reject(ERROR_FACTORY.create("storage-get" /* AppCheckError.STORAGE_GET */, {
  495. originalErrorMessage: (_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message
  496. }));
  497. };
  498. });
  499. }
  500. function computeKey(app) {
  501. return `${app.options.appId}-${app.name}`;
  502. }
  503. /**
  504. * @license
  505. * Copyright 2020 Google LLC
  506. *
  507. * Licensed under the Apache License, Version 2.0 (the "License");
  508. * you may not use this file except in compliance with the License.
  509. * You may obtain a copy of the License at
  510. *
  511. * http://www.apache.org/licenses/LICENSE-2.0
  512. *
  513. * Unless required by applicable law or agreed to in writing, software
  514. * distributed under the License is distributed on an "AS IS" BASIS,
  515. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  516. * See the License for the specific language governing permissions and
  517. * limitations under the License.
  518. */
  519. const logger = new Logger('@firebase/app-check');
  520. /**
  521. * @license
  522. * Copyright 2020 Google LLC
  523. *
  524. * Licensed under the Apache License, Version 2.0 (the "License");
  525. * you may not use this file except in compliance with the License.
  526. * You may obtain a copy of the License at
  527. *
  528. * http://www.apache.org/licenses/LICENSE-2.0
  529. *
  530. * Unless required by applicable law or agreed to in writing, software
  531. * distributed under the License is distributed on an "AS IS" BASIS,
  532. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  533. * See the License for the specific language governing permissions and
  534. * limitations under the License.
  535. */
  536. /**
  537. * Always resolves. In case of an error reading from indexeddb, resolve with undefined
  538. */
  539. async function readTokenFromStorage(app) {
  540. if (isIndexedDBAvailable()) {
  541. let token = undefined;
  542. try {
  543. token = await readTokenFromIndexedDB(app);
  544. }
  545. catch (e) {
  546. // swallow the error and return undefined
  547. logger.warn(`Failed to read token from IndexedDB. Error: ${e}`);
  548. }
  549. return token;
  550. }
  551. return undefined;
  552. }
  553. /**
  554. * Always resolves. In case of an error writing to indexeddb, print a warning and resolve the promise
  555. */
  556. function writeTokenToStorage(app, token) {
  557. if (isIndexedDBAvailable()) {
  558. return writeTokenToIndexedDB(app, token).catch(e => {
  559. // swallow the error and resolve the promise
  560. logger.warn(`Failed to write token to IndexedDB. Error: ${e}`);
  561. });
  562. }
  563. return Promise.resolve();
  564. }
  565. async function readOrCreateDebugTokenFromStorage() {
  566. /**
  567. * Theoretically race condition can happen if we read, then write in 2 separate transactions.
  568. * But it won't happen here, because this function will be called exactly once.
  569. */
  570. let existingDebugToken = undefined;
  571. try {
  572. existingDebugToken = await readDebugTokenFromIndexedDB();
  573. }
  574. catch (_e) {
  575. // failed to read from indexeddb. We assume there is no existing debug token, and generate a new one.
  576. }
  577. if (!existingDebugToken) {
  578. // create a new debug token
  579. const newToken = uuidv4();
  580. // We don't need to block on writing to indexeddb
  581. // In case persistence failed, a new debug token will be generated everytime the page is refreshed.
  582. // It renders the debug token useless because you have to manually register(whitelist) the new token in the firebase console again and again.
  583. // If you see this error trying to use debug token, it probably means you are using a browser that doesn't support indexeddb.
  584. // You should switch to a different browser that supports indexeddb
  585. writeDebugTokenToIndexedDB(newToken).catch(e => logger.warn(`Failed to persist debug token to IndexedDB. Error: ${e}`));
  586. return newToken;
  587. }
  588. else {
  589. return existingDebugToken;
  590. }
  591. }
  592. /**
  593. * @license
  594. * Copyright 2020 Google LLC
  595. *
  596. * Licensed under the Apache License, Version 2.0 (the "License");
  597. * you may not use this file except in compliance with the License.
  598. * You may obtain a copy of the License at
  599. *
  600. * http://www.apache.org/licenses/LICENSE-2.0
  601. *
  602. * Unless required by applicable law or agreed to in writing, software
  603. * distributed under the License is distributed on an "AS IS" BASIS,
  604. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  605. * See the License for the specific language governing permissions and
  606. * limitations under the License.
  607. */
  608. function isDebugMode() {
  609. const debugState = getDebugState();
  610. return debugState.enabled;
  611. }
  612. async function getDebugToken() {
  613. const state = getDebugState();
  614. if (state.enabled && state.token) {
  615. return state.token.promise;
  616. }
  617. else {
  618. // should not happen!
  619. throw Error(`
  620. Can't get debug token in production mode.
  621. `);
  622. }
  623. }
  624. function initializeDebugMode() {
  625. const globals = getGlobal();
  626. const debugState = getDebugState();
  627. // Set to true if this function has been called, whether or not
  628. // it enabled debug mode.
  629. debugState.initialized = true;
  630. if (typeof globals.FIREBASE_APPCHECK_DEBUG_TOKEN !== 'string' &&
  631. globals.FIREBASE_APPCHECK_DEBUG_TOKEN !== true) {
  632. return;
  633. }
  634. debugState.enabled = true;
  635. const deferredToken = new Deferred();
  636. debugState.token = deferredToken;
  637. if (typeof globals.FIREBASE_APPCHECK_DEBUG_TOKEN === 'string') {
  638. deferredToken.resolve(globals.FIREBASE_APPCHECK_DEBUG_TOKEN);
  639. }
  640. else {
  641. deferredToken.resolve(readOrCreateDebugTokenFromStorage());
  642. }
  643. }
  644. /**
  645. * @license
  646. * Copyright 2020 Google LLC
  647. *
  648. * Licensed under the Apache License, Version 2.0 (the "License");
  649. * you may not use this file except in compliance with the License.
  650. * You may obtain a copy of the License at
  651. *
  652. * http://www.apache.org/licenses/LICENSE-2.0
  653. *
  654. * Unless required by applicable law or agreed to in writing, software
  655. * distributed under the License is distributed on an "AS IS" BASIS,
  656. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  657. * See the License for the specific language governing permissions and
  658. * limitations under the License.
  659. */
  660. // Initial hardcoded value agreed upon across platforms for initial launch.
  661. // Format left open for possible dynamic error values and other fields in the future.
  662. const defaultTokenErrorData = { error: 'UNKNOWN_ERROR' };
  663. /**
  664. * Stringify and base64 encode token error data.
  665. *
  666. * @param tokenError Error data, currently hardcoded.
  667. */
  668. function formatDummyToken(tokenErrorData) {
  669. return base64.encodeString(JSON.stringify(tokenErrorData),
  670. /* webSafe= */ false);
  671. }
  672. /**
  673. * This function always resolves.
  674. * The result will contain an error field if there is any error.
  675. * In case there is an error, the token field in the result will be populated with a dummy value
  676. */
  677. async function getToken$2(appCheck, forceRefresh = false) {
  678. const app = appCheck.app;
  679. ensureActivated(app);
  680. const state = getStateReference(app);
  681. /**
  682. * First check if there is a token in memory from a previous `getToken()` call.
  683. */
  684. let token = state.token;
  685. let error = undefined;
  686. /**
  687. * If an invalid token was found in memory, clear token from
  688. * memory and unset the local variable `token`.
  689. */
  690. if (token && !isValid(token)) {
  691. state.token = undefined;
  692. token = undefined;
  693. }
  694. /**
  695. * If there is no valid token in memory, try to load token from indexedDB.
  696. */
  697. if (!token) {
  698. // cachedTokenPromise contains the token found in IndexedDB or undefined if not found.
  699. const cachedToken = await state.cachedTokenPromise;
  700. if (cachedToken) {
  701. if (isValid(cachedToken)) {
  702. token = cachedToken;
  703. }
  704. else {
  705. // If there was an invalid token in the indexedDB cache, clear it.
  706. await writeTokenToStorage(app, undefined);
  707. }
  708. }
  709. }
  710. // Return the cached token (from either memory or indexedDB) if it's valid
  711. if (!forceRefresh && token && isValid(token)) {
  712. return {
  713. token: token.token
  714. };
  715. }
  716. // Only set to true if this `getToken()` call is making the actual
  717. // REST call to the exchange endpoint, versus waiting for an already
  718. // in-flight call (see debug and regular exchange endpoint paths below)
  719. let shouldCallListeners = false;
  720. /**
  721. * DEBUG MODE
  722. * If debug mode is set, and there is no cached token, fetch a new App
  723. * Check token using the debug token, and return it directly.
  724. */
  725. if (isDebugMode()) {
  726. // Avoid making another call to the exchange endpoint if one is in flight.
  727. if (!state.exchangeTokenPromise) {
  728. state.exchangeTokenPromise = exchangeToken(getExchangeDebugTokenRequest(app, await getDebugToken()), appCheck.heartbeatServiceProvider).finally(() => {
  729. // Clear promise when settled - either resolved or rejected.
  730. state.exchangeTokenPromise = undefined;
  731. });
  732. shouldCallListeners = true;
  733. }
  734. const tokenFromDebugExchange = await state.exchangeTokenPromise;
  735. // Write debug token to indexedDB.
  736. await writeTokenToStorage(app, tokenFromDebugExchange);
  737. // Write debug token to state.
  738. state.token = tokenFromDebugExchange;
  739. return { token: tokenFromDebugExchange.token };
  740. }
  741. /**
  742. * There are no valid tokens in memory or indexedDB and we are not in
  743. * debug mode.
  744. * Request a new token from the exchange endpoint.
  745. */
  746. try {
  747. // Avoid making another call to the exchange endpoint if one is in flight.
  748. if (!state.exchangeTokenPromise) {
  749. // state.provider is populated in initializeAppCheck()
  750. // ensureActivated() at the top of this function checks that
  751. // initializeAppCheck() has been called.
  752. state.exchangeTokenPromise = state.provider.getToken().finally(() => {
  753. // Clear promise when settled - either resolved or rejected.
  754. state.exchangeTokenPromise = undefined;
  755. });
  756. shouldCallListeners = true;
  757. }
  758. token = await getStateReference(app).exchangeTokenPromise;
  759. }
  760. catch (e) {
  761. if (e.code === `appCheck/${"throttled" /* AppCheckError.THROTTLED */}`) {
  762. // Warn if throttled, but do not treat it as an error.
  763. logger.warn(e.message);
  764. }
  765. else {
  766. // `getToken()` should never throw, but logging error text to console will aid debugging.
  767. logger.error(e);
  768. }
  769. // Always save error to be added to dummy token.
  770. error = e;
  771. }
  772. let interopTokenResult;
  773. if (!token) {
  774. // If token is undefined, there must be an error.
  775. // Return a dummy token along with the error.
  776. interopTokenResult = makeDummyTokenResult(error);
  777. }
  778. else if (error) {
  779. if (isValid(token)) {
  780. // It's also possible a valid token exists, but there's also an error.
  781. // (Such as if the token is almost expired, tries to refresh, and
  782. // the exchange request fails.)
  783. // We add a special error property here so that the refresher will
  784. // count this as a failed attempt and use the backoff instead of
  785. // retrying repeatedly with no delay, but any 3P listeners will not
  786. // be hindered in getting the still-valid token.
  787. interopTokenResult = {
  788. token: token.token,
  789. internalError: error
  790. };
  791. }
  792. else {
  793. // No invalid tokens should make it to this step. Memory and cached tokens
  794. // are checked. Other tokens are from fresh exchanges. But just in case.
  795. interopTokenResult = makeDummyTokenResult(error);
  796. }
  797. }
  798. else {
  799. interopTokenResult = {
  800. token: token.token
  801. };
  802. // write the new token to the memory state as well as the persistent storage.
  803. // Only do it if we got a valid new token
  804. state.token = token;
  805. await writeTokenToStorage(app, token);
  806. }
  807. if (shouldCallListeners) {
  808. notifyTokenListeners(app, interopTokenResult);
  809. }
  810. return interopTokenResult;
  811. }
  812. function addTokenListener(appCheck, type, listener, onError) {
  813. const { app } = appCheck;
  814. const state = getStateReference(app);
  815. const tokenObserver = {
  816. next: listener,
  817. error: onError,
  818. type
  819. };
  820. state.tokenObservers = [...state.tokenObservers, tokenObserver];
  821. // Invoke the listener async immediately if there is a valid token
  822. // in memory.
  823. if (state.token && isValid(state.token)) {
  824. const validToken = state.token;
  825. Promise.resolve()
  826. .then(() => {
  827. listener({ token: validToken.token });
  828. initTokenRefresher(appCheck);
  829. })
  830. .catch(() => {
  831. /* we don't care about exceptions thrown in listeners */
  832. });
  833. }
  834. /**
  835. * Wait for any cached token promise to resolve before starting the token
  836. * refresher. The refresher checks to see if there is an existing token
  837. * in state and calls the exchange endpoint if not. We should first let the
  838. * IndexedDB check have a chance to populate state if it can.
  839. *
  840. * Listener call isn't needed here because cachedTokenPromise will call any
  841. * listeners that exist when it resolves.
  842. */
  843. // state.cachedTokenPromise is always populated in `activate()`.
  844. void state.cachedTokenPromise.then(() => initTokenRefresher(appCheck));
  845. }
  846. function removeTokenListener(app, listener) {
  847. const state = getStateReference(app);
  848. const newObservers = state.tokenObservers.filter(tokenObserver => tokenObserver.next !== listener);
  849. if (newObservers.length === 0 &&
  850. state.tokenRefresher &&
  851. state.tokenRefresher.isRunning()) {
  852. state.tokenRefresher.stop();
  853. }
  854. state.tokenObservers = newObservers;
  855. }
  856. /**
  857. * Logic to create and start refresher as needed.
  858. */
  859. function initTokenRefresher(appCheck) {
  860. const { app } = appCheck;
  861. const state = getStateReference(app);
  862. // Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
  863. // is not true.
  864. let refresher = state.tokenRefresher;
  865. if (!refresher) {
  866. refresher = createTokenRefresher(appCheck);
  867. state.tokenRefresher = refresher;
  868. }
  869. if (!refresher.isRunning() && state.isTokenAutoRefreshEnabled) {
  870. refresher.start();
  871. }
  872. }
  873. function createTokenRefresher(appCheck) {
  874. const { app } = appCheck;
  875. return new Refresher(
  876. // Keep in mind when this fails for any reason other than the ones
  877. // for which we should retry, it will effectively stop the proactive refresh.
  878. async () => {
  879. const state = getStateReference(app);
  880. // If there is no token, we will try to load it from storage and use it
  881. // If there is a token, we force refresh it because we know it's going to expire soon
  882. let result;
  883. if (!state.token) {
  884. result = await getToken$2(appCheck);
  885. }
  886. else {
  887. result = await getToken$2(appCheck, true);
  888. }
  889. /**
  890. * getToken() always resolves. In case the result has an error field defined, it means
  891. * the operation failed, and we should retry.
  892. */
  893. if (result.error) {
  894. throw result.error;
  895. }
  896. /**
  897. * A special `internalError` field reflects that there was an error
  898. * getting a new token from the exchange endpoint, but there's still a
  899. * previous token that's valid for now and this should be passed to 2P/3P
  900. * requests for a token. But we want this callback (`this.operation` in
  901. * `Refresher`) to throw in order to kick off the Refresher's retry
  902. * backoff. (Setting `hasSucceeded` to false.)
  903. */
  904. if (result.internalError) {
  905. throw result.internalError;
  906. }
  907. }, () => {
  908. return true;
  909. }, () => {
  910. const state = getStateReference(app);
  911. if (state.token) {
  912. // issuedAtTime + (50% * total TTL) + 5 minutes
  913. let nextRefreshTimeMillis = state.token.issuedAtTimeMillis +
  914. (state.token.expireTimeMillis - state.token.issuedAtTimeMillis) *
  915. 0.5 +
  916. 5 * 60 * 1000;
  917. // Do not allow refresh time to be past (expireTime - 5 minutes)
  918. const latestAllowableRefresh = state.token.expireTimeMillis - 5 * 60 * 1000;
  919. nextRefreshTimeMillis = Math.min(nextRefreshTimeMillis, latestAllowableRefresh);
  920. return Math.max(0, nextRefreshTimeMillis - Date.now());
  921. }
  922. else {
  923. return 0;
  924. }
  925. }, TOKEN_REFRESH_TIME.RETRIAL_MIN_WAIT, TOKEN_REFRESH_TIME.RETRIAL_MAX_WAIT);
  926. }
  927. function notifyTokenListeners(app, token) {
  928. const observers = getStateReference(app).tokenObservers;
  929. for (const observer of observers) {
  930. try {
  931. if (observer.type === "EXTERNAL" /* ListenerType.EXTERNAL */ && token.error != null) {
  932. // If this listener was added by a 3P call, send any token error to
  933. // the supplied error handler. A 3P observer always has an error
  934. // handler.
  935. observer.error(token.error);
  936. }
  937. else {
  938. // If the token has no error field, always return the token.
  939. // If this is a 2P listener, return the token, whether or not it
  940. // has an error field.
  941. observer.next(token);
  942. }
  943. }
  944. catch (e) {
  945. // Errors in the listener function itself are always ignored.
  946. }
  947. }
  948. }
  949. function isValid(token) {
  950. return token.expireTimeMillis - Date.now() > 0;
  951. }
  952. function makeDummyTokenResult(error) {
  953. return {
  954. token: formatDummyToken(defaultTokenErrorData),
  955. error
  956. };
  957. }
  958. /**
  959. * @license
  960. * Copyright 2020 Google LLC
  961. *
  962. * Licensed under the Apache License, Version 2.0 (the "License");
  963. * you may not use this file except in compliance with the License.
  964. * You may obtain a copy of the License at
  965. *
  966. * http://www.apache.org/licenses/LICENSE-2.0
  967. *
  968. * Unless required by applicable law or agreed to in writing, software
  969. * distributed under the License is distributed on an "AS IS" BASIS,
  970. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  971. * See the License for the specific language governing permissions and
  972. * limitations under the License.
  973. */
  974. /**
  975. * AppCheck Service class.
  976. */
  977. class AppCheckService {
  978. constructor(app, heartbeatServiceProvider) {
  979. this.app = app;
  980. this.heartbeatServiceProvider = heartbeatServiceProvider;
  981. }
  982. _delete() {
  983. const { tokenObservers } = getStateReference(this.app);
  984. for (const tokenObserver of tokenObservers) {
  985. removeTokenListener(this.app, tokenObserver.next);
  986. }
  987. return Promise.resolve();
  988. }
  989. }
  990. function factory(app, heartbeatServiceProvider) {
  991. return new AppCheckService(app, heartbeatServiceProvider);
  992. }
  993. function internalFactory(appCheck) {
  994. return {
  995. getToken: forceRefresh => getToken$2(appCheck, forceRefresh),
  996. addTokenListener: listener => addTokenListener(appCheck, "INTERNAL" /* ListenerType.INTERNAL */, listener),
  997. removeTokenListener: listener => removeTokenListener(appCheck.app, listener)
  998. };
  999. }
  1000. const name = "@firebase/app-check";
  1001. const version = "0.6.1";
  1002. /**
  1003. * @license
  1004. * Copyright 2020 Google LLC
  1005. *
  1006. * Licensed under the Apache License, Version 2.0 (the "License");
  1007. * you may not use this file except in compliance with the License.
  1008. * You may obtain a copy of the License at
  1009. *
  1010. * http://www.apache.org/licenses/LICENSE-2.0
  1011. *
  1012. * Unless required by applicable law or agreed to in writing, software
  1013. * distributed under the License is distributed on an "AS IS" BASIS,
  1014. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  1015. * See the License for the specific language governing permissions and
  1016. * limitations under the License.
  1017. */
  1018. const RECAPTCHA_URL = 'https://www.google.com/recaptcha/api.js';
  1019. const RECAPTCHA_ENTERPRISE_URL = 'https://www.google.com/recaptcha/enterprise.js';
  1020. function initializeV3(app, siteKey) {
  1021. const initialized = new Deferred();
  1022. const state = getStateReference(app);
  1023. state.reCAPTCHAState = { initialized };
  1024. const divId = makeDiv(app);
  1025. const grecaptcha = getRecaptcha(false);
  1026. if (!grecaptcha) {
  1027. loadReCAPTCHAV3Script(() => {
  1028. const grecaptcha = getRecaptcha(false);
  1029. if (!grecaptcha) {
  1030. // it shouldn't happen.
  1031. throw new Error('no recaptcha');
  1032. }
  1033. queueWidgetRender(app, siteKey, grecaptcha, divId, initialized);
  1034. });
  1035. }
  1036. else {
  1037. queueWidgetRender(app, siteKey, grecaptcha, divId, initialized);
  1038. }
  1039. return initialized.promise;
  1040. }
  1041. function initializeEnterprise(app, siteKey) {
  1042. const initialized = new Deferred();
  1043. const state = getStateReference(app);
  1044. state.reCAPTCHAState = { initialized };
  1045. const divId = makeDiv(app);
  1046. const grecaptcha = getRecaptcha(true);
  1047. if (!grecaptcha) {
  1048. loadReCAPTCHAEnterpriseScript(() => {
  1049. const grecaptcha = getRecaptcha(true);
  1050. if (!grecaptcha) {
  1051. // it shouldn't happen.
  1052. throw new Error('no recaptcha');
  1053. }
  1054. queueWidgetRender(app, siteKey, grecaptcha, divId, initialized);
  1055. });
  1056. }
  1057. else {
  1058. queueWidgetRender(app, siteKey, grecaptcha, divId, initialized);
  1059. }
  1060. return initialized.promise;
  1061. }
  1062. /**
  1063. * Add listener to render the widget and resolve the promise when
  1064. * the grecaptcha.ready() event fires.
  1065. */
  1066. function queueWidgetRender(app, siteKey, grecaptcha, container, initialized) {
  1067. grecaptcha.ready(() => {
  1068. // Invisible widgets allow us to set a different siteKey for each widget,
  1069. // so we use them to support multiple apps
  1070. renderInvisibleWidget(app, siteKey, grecaptcha, container);
  1071. initialized.resolve(grecaptcha);
  1072. });
  1073. }
  1074. /**
  1075. * Add invisible div to page.
  1076. */
  1077. function makeDiv(app) {
  1078. const divId = `fire_app_check_${app.name}`;
  1079. const invisibleDiv = document.createElement('div');
  1080. invisibleDiv.id = divId;
  1081. invisibleDiv.style.display = 'none';
  1082. document.body.appendChild(invisibleDiv);
  1083. return divId;
  1084. }
  1085. async function getToken$1(app) {
  1086. ensureActivated(app);
  1087. // ensureActivated() guarantees that reCAPTCHAState is set
  1088. const reCAPTCHAState = getStateReference(app).reCAPTCHAState;
  1089. const recaptcha = await reCAPTCHAState.initialized.promise;
  1090. return new Promise((resolve, _reject) => {
  1091. // Updated after initialization is complete.
  1092. const reCAPTCHAState = getStateReference(app).reCAPTCHAState;
  1093. recaptcha.ready(() => {
  1094. resolve(
  1095. // widgetId is guaranteed to be available if reCAPTCHAState.initialized.promise resolved.
  1096. recaptcha.execute(reCAPTCHAState.widgetId, {
  1097. action: 'fire_app_check'
  1098. }));
  1099. });
  1100. });
  1101. }
  1102. /**
  1103. *
  1104. * @param app
  1105. * @param container - Id of a HTML element.
  1106. */
  1107. function renderInvisibleWidget(app, siteKey, grecaptcha, container) {
  1108. const widgetId = grecaptcha.render(container, {
  1109. sitekey: siteKey,
  1110. size: 'invisible'
  1111. });
  1112. const state = getStateReference(app);
  1113. state.reCAPTCHAState = Object.assign(Object.assign({}, state.reCAPTCHAState), { // state.reCAPTCHAState is set in the initialize()
  1114. widgetId });
  1115. }
  1116. function loadReCAPTCHAV3Script(onload) {
  1117. const script = document.createElement('script');
  1118. script.src = RECAPTCHA_URL;
  1119. script.onload = onload;
  1120. document.head.appendChild(script);
  1121. }
  1122. function loadReCAPTCHAEnterpriseScript(onload) {
  1123. const script = document.createElement('script');
  1124. script.src = RECAPTCHA_ENTERPRISE_URL;
  1125. script.onload = onload;
  1126. document.head.appendChild(script);
  1127. }
  1128. /**
  1129. * @license
  1130. * Copyright 2021 Google LLC
  1131. *
  1132. * Licensed under the Apache License, Version 2.0 (the "License");
  1133. * you may not use this file except in compliance with the License.
  1134. * You may obtain a copy of the License at
  1135. *
  1136. * http://www.apache.org/licenses/LICENSE-2.0
  1137. *
  1138. * Unless required by applicable law or agreed to in writing, software
  1139. * distributed under the License is distributed on an "AS IS" BASIS,
  1140. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  1141. * See the License for the specific language governing permissions and
  1142. * limitations under the License.
  1143. */
  1144. /**
  1145. * App Check provider that can obtain a reCAPTCHA V3 token and exchange it
  1146. * for an App Check token.
  1147. *
  1148. * @public
  1149. */
  1150. class ReCaptchaV3Provider {
  1151. /**
  1152. * Create a ReCaptchaV3Provider instance.
  1153. * @param siteKey - ReCAPTCHA V3 siteKey.
  1154. */
  1155. constructor(_siteKey) {
  1156. this._siteKey = _siteKey;
  1157. /**
  1158. * Throttle requests on certain error codes to prevent too many retries
  1159. * in a short time.
  1160. */
  1161. this._throttleData = null;
  1162. }
  1163. /**
  1164. * Returns an App Check token.
  1165. * @internal
  1166. */
  1167. async getToken() {
  1168. var _a, _b;
  1169. throwIfThrottled(this._throttleData);
  1170. // Top-level `getToken()` has already checked that App Check is initialized
  1171. // and therefore this._app and this._heartbeatServiceProvider are available.
  1172. const attestedClaimsToken = await getToken$1(this._app).catch(_e => {
  1173. // reCaptcha.execute() throws null which is not very descriptive.
  1174. throw ERROR_FACTORY.create("recaptcha-error" /* AppCheckError.RECAPTCHA_ERROR */);
  1175. });
  1176. let result;
  1177. try {
  1178. result = await exchangeToken(getExchangeRecaptchaV3TokenRequest(this._app, attestedClaimsToken), this._heartbeatServiceProvider);
  1179. }
  1180. catch (e) {
  1181. if ((_a = e.code) === null || _a === void 0 ? void 0 : _a.includes("fetch-status-error" /* AppCheckError.FETCH_STATUS_ERROR */)) {
  1182. this._throttleData = setBackoff(Number((_b = e.customData) === null || _b === void 0 ? void 0 : _b.httpStatus), this._throttleData);
  1183. throw ERROR_FACTORY.create("throttled" /* AppCheckError.THROTTLED */, {
  1184. time: getDurationString(this._throttleData.allowRequestsAfter - Date.now()),
  1185. httpStatus: this._throttleData.httpStatus
  1186. });
  1187. }
  1188. else {
  1189. throw e;
  1190. }
  1191. }
  1192. // If successful, clear throttle data.
  1193. this._throttleData = null;
  1194. return result;
  1195. }
  1196. /**
  1197. * @internal
  1198. */
  1199. initialize(app) {
  1200. this._app = app;
  1201. this._heartbeatServiceProvider = _getProvider(app, 'heartbeat');
  1202. initializeV3(app, this._siteKey).catch(() => {
  1203. /* we don't care about the initialization result */
  1204. });
  1205. }
  1206. /**
  1207. * @internal
  1208. */
  1209. isEqual(otherProvider) {
  1210. if (otherProvider instanceof ReCaptchaV3Provider) {
  1211. return this._siteKey === otherProvider._siteKey;
  1212. }
  1213. else {
  1214. return false;
  1215. }
  1216. }
  1217. }
  1218. /**
  1219. * App Check provider that can obtain a reCAPTCHA Enterprise token and exchange it
  1220. * for an App Check token.
  1221. *
  1222. * @public
  1223. */
  1224. class ReCaptchaEnterpriseProvider {
  1225. /**
  1226. * Create a ReCaptchaEnterpriseProvider instance.
  1227. * @param siteKey - reCAPTCHA Enterprise score-based site key.
  1228. */
  1229. constructor(_siteKey) {
  1230. this._siteKey = _siteKey;
  1231. /**
  1232. * Throttle requests on certain error codes to prevent too many retries
  1233. * in a short time.
  1234. */
  1235. this._throttleData = null;
  1236. }
  1237. /**
  1238. * Returns an App Check token.
  1239. * @internal
  1240. */
  1241. async getToken() {
  1242. var _a, _b;
  1243. throwIfThrottled(this._throttleData);
  1244. // Top-level `getToken()` has already checked that App Check is initialized
  1245. // and therefore this._app and this._heartbeatServiceProvider are available.
  1246. const attestedClaimsToken = await getToken$1(this._app).catch(_e => {
  1247. // reCaptcha.execute() throws null which is not very descriptive.
  1248. throw ERROR_FACTORY.create("recaptcha-error" /* AppCheckError.RECAPTCHA_ERROR */);
  1249. });
  1250. let result;
  1251. try {
  1252. result = await exchangeToken(getExchangeRecaptchaEnterpriseTokenRequest(this._app, attestedClaimsToken), this._heartbeatServiceProvider);
  1253. }
  1254. catch (e) {
  1255. if ((_a = e.code) === null || _a === void 0 ? void 0 : _a.includes("fetch-status-error" /* AppCheckError.FETCH_STATUS_ERROR */)) {
  1256. this._throttleData = setBackoff(Number((_b = e.customData) === null || _b === void 0 ? void 0 : _b.httpStatus), this._throttleData);
  1257. throw ERROR_FACTORY.create("throttled" /* AppCheckError.THROTTLED */, {
  1258. time: getDurationString(this._throttleData.allowRequestsAfter - Date.now()),
  1259. httpStatus: this._throttleData.httpStatus
  1260. });
  1261. }
  1262. else {
  1263. throw e;
  1264. }
  1265. }
  1266. // If successful, clear throttle data.
  1267. this._throttleData = null;
  1268. return result;
  1269. }
  1270. /**
  1271. * @internal
  1272. */
  1273. initialize(app) {
  1274. this._app = app;
  1275. this._heartbeatServiceProvider = _getProvider(app, 'heartbeat');
  1276. initializeEnterprise(app, this._siteKey).catch(() => {
  1277. /* we don't care about the initialization result */
  1278. });
  1279. }
  1280. /**
  1281. * @internal
  1282. */
  1283. isEqual(otherProvider) {
  1284. if (otherProvider instanceof ReCaptchaEnterpriseProvider) {
  1285. return this._siteKey === otherProvider._siteKey;
  1286. }
  1287. else {
  1288. return false;
  1289. }
  1290. }
  1291. }
  1292. /**
  1293. * Custom provider class.
  1294. * @public
  1295. */
  1296. class CustomProvider {
  1297. constructor(_customProviderOptions) {
  1298. this._customProviderOptions = _customProviderOptions;
  1299. }
  1300. /**
  1301. * @internal
  1302. */
  1303. async getToken() {
  1304. // custom provider
  1305. const customToken = await this._customProviderOptions.getToken();
  1306. // Try to extract IAT from custom token, in case this token is not
  1307. // being newly issued. JWT timestamps are in seconds since epoch.
  1308. const issuedAtTimeSeconds = issuedAtTime(customToken.token);
  1309. // Very basic validation, use current timestamp as IAT if JWT
  1310. // has no `iat` field or value is out of bounds.
  1311. const issuedAtTimeMillis = issuedAtTimeSeconds !== null &&
  1312. issuedAtTimeSeconds < Date.now() &&
  1313. issuedAtTimeSeconds > 0
  1314. ? issuedAtTimeSeconds * 1000
  1315. : Date.now();
  1316. return Object.assign(Object.assign({}, customToken), { issuedAtTimeMillis });
  1317. }
  1318. /**
  1319. * @internal
  1320. */
  1321. initialize(app) {
  1322. this._app = app;
  1323. }
  1324. /**
  1325. * @internal
  1326. */
  1327. isEqual(otherProvider) {
  1328. if (otherProvider instanceof CustomProvider) {
  1329. return (this._customProviderOptions.getToken.toString() ===
  1330. otherProvider._customProviderOptions.getToken.toString());
  1331. }
  1332. else {
  1333. return false;
  1334. }
  1335. }
  1336. }
  1337. /**
  1338. * Set throttle data to block requests until after a certain time
  1339. * depending on the failed request's status code.
  1340. * @param httpStatus - Status code of failed request.
  1341. * @param throttleData - `ThrottleData` object containing previous throttle
  1342. * data state.
  1343. * @returns Data about current throttle state and expiration time.
  1344. */
  1345. function setBackoff(httpStatus, throttleData) {
  1346. /**
  1347. * Block retries for 1 day for the following error codes:
  1348. *
  1349. * 404: Likely malformed URL.
  1350. *
  1351. * 403:
  1352. * - Attestation failed
  1353. * - Wrong API key
  1354. * - Project deleted
  1355. */
  1356. if (httpStatus === 404 || httpStatus === 403) {
  1357. return {
  1358. backoffCount: 1,
  1359. allowRequestsAfter: Date.now() + ONE_DAY,
  1360. httpStatus
  1361. };
  1362. }
  1363. else {
  1364. /**
  1365. * For all other error codes, the time when it is ok to retry again
  1366. * is based on exponential backoff.
  1367. */
  1368. const backoffCount = throttleData ? throttleData.backoffCount : 0;
  1369. const backoffMillis = calculateBackoffMillis(backoffCount, 1000, 2);
  1370. return {
  1371. backoffCount: backoffCount + 1,
  1372. allowRequestsAfter: Date.now() + backoffMillis,
  1373. httpStatus
  1374. };
  1375. }
  1376. }
  1377. function throwIfThrottled(throttleData) {
  1378. if (throttleData) {
  1379. if (Date.now() - throttleData.allowRequestsAfter <= 0) {
  1380. // If before, throw.
  1381. throw ERROR_FACTORY.create("throttled" /* AppCheckError.THROTTLED */, {
  1382. time: getDurationString(throttleData.allowRequestsAfter - Date.now()),
  1383. httpStatus: throttleData.httpStatus
  1384. });
  1385. }
  1386. }
  1387. }
  1388. /**
  1389. * @license
  1390. * Copyright 2020 Google LLC
  1391. *
  1392. * Licensed under the Apache License, Version 2.0 (the "License");
  1393. * you may not use this file except in compliance with the License.
  1394. * You may obtain a copy of the License at
  1395. *
  1396. * http://www.apache.org/licenses/LICENSE-2.0
  1397. *
  1398. * Unless required by applicable law or agreed to in writing, software
  1399. * distributed under the License is distributed on an "AS IS" BASIS,
  1400. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  1401. * See the License for the specific language governing permissions and
  1402. * limitations under the License.
  1403. */
  1404. /**
  1405. * Activate App Check for the given app. Can be called only once per app.
  1406. * @param app - the {@link @firebase/app#FirebaseApp} to activate App Check for
  1407. * @param options - App Check initialization options
  1408. * @public
  1409. */
  1410. function initializeAppCheck(app = getApp(), options) {
  1411. app = getModularInstance(app);
  1412. const provider = _getProvider(app, 'app-check');
  1413. // Ensure initializeDebugMode() is only called once.
  1414. if (!getDebugState().initialized) {
  1415. initializeDebugMode();
  1416. }
  1417. // Log a message containing the debug token when `initializeAppCheck()`
  1418. // is called in debug mode.
  1419. if (isDebugMode()) {
  1420. // Do not block initialization to get the token for the message.
  1421. void getDebugToken().then(token =>
  1422. // Not using logger because I don't think we ever want this accidentally hidden.
  1423. console.log(`App Check debug token: ${token}. You will need to add it to your app's App Check settings in the Firebase console for it to work.`));
  1424. }
  1425. if (provider.isInitialized()) {
  1426. const existingInstance = provider.getImmediate();
  1427. const initialOptions = provider.getOptions();
  1428. if (initialOptions.isTokenAutoRefreshEnabled ===
  1429. options.isTokenAutoRefreshEnabled &&
  1430. initialOptions.provider.isEqual(options.provider)) {
  1431. return existingInstance;
  1432. }
  1433. else {
  1434. throw ERROR_FACTORY.create("already-initialized" /* AppCheckError.ALREADY_INITIALIZED */, {
  1435. appName: app.name
  1436. });
  1437. }
  1438. }
  1439. const appCheck = provider.initialize({ options });
  1440. _activate(app, options.provider, options.isTokenAutoRefreshEnabled);
  1441. // If isTokenAutoRefreshEnabled is false, do not send any requests to the
  1442. // exchange endpoint without an explicit call from the user either directly
  1443. // or through another Firebase library (storage, functions, etc.)
  1444. if (getStateReference(app).isTokenAutoRefreshEnabled) {
  1445. // Adding a listener will start the refresher and fetch a token if needed.
  1446. // This gets a token ready and prevents a delay when an internal library
  1447. // requests the token.
  1448. // Listener function does not need to do anything, its base functionality
  1449. // of calling getToken() already fetches token and writes it to memory/storage.
  1450. addTokenListener(appCheck, "INTERNAL" /* ListenerType.INTERNAL */, () => { });
  1451. }
  1452. return appCheck;
  1453. }
  1454. /**
  1455. * Activate App Check
  1456. * @param app - Firebase app to activate App Check for.
  1457. * @param provider - reCAPTCHA v3 provider or
  1458. * custom token provider.
  1459. * @param isTokenAutoRefreshEnabled - If true, the SDK automatically
  1460. * refreshes App Check tokens as needed. If undefined, defaults to the
  1461. * value of `app.automaticDataCollectionEnabled`, which defaults to
  1462. * false and can be set in the app config.
  1463. */
  1464. function _activate(app, provider, isTokenAutoRefreshEnabled) {
  1465. // Create an entry in the APP_CHECK_STATES map. Further changes should
  1466. // directly mutate this object.
  1467. const state = setInitialState(app, Object.assign({}, DEFAULT_STATE));
  1468. state.activated = true;
  1469. state.provider = provider; // Read cached token from storage if it exists and store it in memory.
  1470. state.cachedTokenPromise = readTokenFromStorage(app).then(cachedToken => {
  1471. if (cachedToken && isValid(cachedToken)) {
  1472. state.token = cachedToken;
  1473. // notify all listeners with the cached token
  1474. notifyTokenListeners(app, { token: cachedToken.token });
  1475. }
  1476. return cachedToken;
  1477. });
  1478. // Use value of global `automaticDataCollectionEnabled` (which
  1479. // itself defaults to false if not specified in config) if
  1480. // `isTokenAutoRefreshEnabled` param was not provided by user.
  1481. state.isTokenAutoRefreshEnabled =
  1482. isTokenAutoRefreshEnabled === undefined
  1483. ? app.automaticDataCollectionEnabled
  1484. : isTokenAutoRefreshEnabled;
  1485. state.provider.initialize(app);
  1486. }
  1487. /**
  1488. * Set whether App Check will automatically refresh tokens as needed.
  1489. *
  1490. * @param appCheckInstance - The App Check service instance.
  1491. * @param isTokenAutoRefreshEnabled - If true, the SDK automatically
  1492. * refreshes App Check tokens as needed. This overrides any value set
  1493. * during `initializeAppCheck()`.
  1494. * @public
  1495. */
  1496. function setTokenAutoRefreshEnabled(appCheckInstance, isTokenAutoRefreshEnabled) {
  1497. const app = appCheckInstance.app;
  1498. const state = getStateReference(app);
  1499. // This will exist if any product libraries have called
  1500. // `addTokenListener()`
  1501. if (state.tokenRefresher) {
  1502. if (isTokenAutoRefreshEnabled === true) {
  1503. state.tokenRefresher.start();
  1504. }
  1505. else {
  1506. state.tokenRefresher.stop();
  1507. }
  1508. }
  1509. state.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
  1510. }
  1511. /**
  1512. * Get the current App Check token. Attaches to the most recent
  1513. * in-flight request if one is present. Returns null if no token
  1514. * is present and no token requests are in-flight.
  1515. *
  1516. * @param appCheckInstance - The App Check service instance.
  1517. * @param forceRefresh - If true, will always try to fetch a fresh token.
  1518. * If false, will use a cached token if found in storage.
  1519. * @public
  1520. */
  1521. async function getToken(appCheckInstance, forceRefresh) {
  1522. const result = await getToken$2(appCheckInstance, forceRefresh);
  1523. if (result.error) {
  1524. throw result.error;
  1525. }
  1526. return { token: result.token };
  1527. }
  1528. /**
  1529. * Wraps `addTokenListener`/`removeTokenListener` methods in an `Observer`
  1530. * pattern for public use.
  1531. */
  1532. function onTokenChanged(appCheckInstance, onNextOrObserver, onError,
  1533. /**
  1534. * NOTE: Although an `onCompletion` callback can be provided, it will
  1535. * never be called because the token stream is never-ending.
  1536. * It is added only for API consistency with the observer pattern, which
  1537. * we follow in JS APIs.
  1538. */
  1539. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  1540. onCompletion) {
  1541. let nextFn = () => { };
  1542. let errorFn = () => { };
  1543. if (onNextOrObserver.next != null) {
  1544. nextFn = onNextOrObserver.next.bind(onNextOrObserver);
  1545. }
  1546. else {
  1547. nextFn = onNextOrObserver;
  1548. }
  1549. if (onNextOrObserver.error != null) {
  1550. errorFn = onNextOrObserver.error.bind(onNextOrObserver);
  1551. }
  1552. else if (onError) {
  1553. errorFn = onError;
  1554. }
  1555. addTokenListener(appCheckInstance, "EXTERNAL" /* ListenerType.EXTERNAL */, nextFn, errorFn);
  1556. return () => removeTokenListener(appCheckInstance.app, nextFn);
  1557. }
  1558. /**
  1559. * Firebase App Check
  1560. *
  1561. * @packageDocumentation
  1562. */
  1563. const APP_CHECK_NAME = 'app-check';
  1564. const APP_CHECK_NAME_INTERNAL = 'app-check-internal';
  1565. function registerAppCheck() {
  1566. // The public interface
  1567. _registerComponent(new Component(APP_CHECK_NAME, container => {
  1568. // getImmediate for FirebaseApp will always succeed
  1569. const app = container.getProvider('app').getImmediate();
  1570. const heartbeatServiceProvider = container.getProvider('heartbeat');
  1571. return factory(app, heartbeatServiceProvider);
  1572. }, "PUBLIC" /* ComponentType.PUBLIC */)
  1573. .setInstantiationMode("EXPLICIT" /* InstantiationMode.EXPLICIT */)
  1574. /**
  1575. * Initialize app-check-internal after app-check is initialized to make AppCheck available to
  1576. * other Firebase SDKs
  1577. */
  1578. .setInstanceCreatedCallback((container, _identifier, _appcheckService) => {
  1579. container.getProvider(APP_CHECK_NAME_INTERNAL).initialize();
  1580. }));
  1581. // The internal interface used by other Firebase products
  1582. _registerComponent(new Component(APP_CHECK_NAME_INTERNAL, container => {
  1583. const appCheck = container.getProvider('app-check').getImmediate();
  1584. return internalFactory(appCheck);
  1585. }, "PUBLIC" /* ComponentType.PUBLIC */).setInstantiationMode("EXPLICIT" /* InstantiationMode.EXPLICIT */));
  1586. registerVersion(name, version);
  1587. }
  1588. registerAppCheck();
  1589. export { CustomProvider, ReCaptchaEnterpriseProvider, ReCaptchaV3Provider, getToken, initializeAppCheck, onTokenChanged, setTokenAutoRefreshEnabled };
  1590. //# sourceMappingURL=index.esm2017.js.map