token-generator.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * @license
  5. * Copyright 2021 Google Inc.
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. */
  19. Object.defineProperty(exports, "__esModule", { value: true });
  20. exports.appCheckErrorFromCryptoSignerError = exports.AppCheckTokenGenerator = void 0;
  21. const validator = require("../utils/validator");
  22. const utils_1 = require("../utils");
  23. const crypto_signer_1 = require("../utils/crypto-signer");
  24. const app_check_api_client_internal_1 = require("./app-check-api-client-internal");
  25. const ONE_MINUTE_IN_SECONDS = 60;
  26. const ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1000;
  27. const ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
  28. // Audience to use for Firebase App Check Custom tokens
  29. const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService';
  30. /**
  31. * Class for generating Firebase App Check tokens.
  32. *
  33. * @internal
  34. */
  35. class AppCheckTokenGenerator {
  36. /**
  37. * The AppCheckTokenGenerator class constructor.
  38. *
  39. * @param signer - The CryptoSigner instance for this token generator.
  40. * @constructor
  41. */
  42. constructor(signer) {
  43. if (!validator.isNonNullObject(signer)) {
  44. throw new app_check_api_client_internal_1.FirebaseAppCheckError('invalid-argument', 'INTERNAL ASSERT: Must provide a CryptoSigner to use AppCheckTokenGenerator.');
  45. }
  46. this.signer = signer;
  47. }
  48. /**
  49. * Creates a new custom token that can be exchanged to an App Check token.
  50. *
  51. * @param appId - The Application ID to use for the generated token.
  52. *
  53. * @returns A Promise fulfilled with a custom token signed with a service account key
  54. * that can be exchanged to an App Check token.
  55. */
  56. createCustomToken(appId, options) {
  57. if (!validator.isNonEmptyString(appId)) {
  58. throw new app_check_api_client_internal_1.FirebaseAppCheckError('invalid-argument', '`appId` must be a non-empty string.');
  59. }
  60. let customOptions = {};
  61. if (typeof options !== 'undefined') {
  62. customOptions = this.validateTokenOptions(options);
  63. }
  64. return this.signer.getAccountId().then((account) => {
  65. const header = {
  66. alg: this.signer.algorithm,
  67. typ: 'JWT',
  68. };
  69. const iat = Math.floor(Date.now() / 1000);
  70. const body = {
  71. iss: account,
  72. sub: account,
  73. app_id: appId,
  74. aud: FIREBASE_APP_CHECK_AUDIENCE,
  75. exp: iat + (ONE_MINUTE_IN_SECONDS * 5),
  76. iat,
  77. ...customOptions,
  78. };
  79. const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`;
  80. return this.signer.sign(Buffer.from(token))
  81. .then((signature) => {
  82. return `${token}.${this.encodeSegment(signature)}`;
  83. });
  84. }).catch((err) => {
  85. throw appCheckErrorFromCryptoSignerError(err);
  86. });
  87. }
  88. encodeSegment(segment) {
  89. const buffer = (segment instanceof Buffer) ? segment : Buffer.from(JSON.stringify(segment));
  90. return (0, utils_1.toWebSafeBase64)(buffer).replace(/=+$/, '');
  91. }
  92. /**
  93. * Checks if a given `AppCheckTokenOptions` object is valid. If successful, returns an object with
  94. * custom properties.
  95. *
  96. * @param options - An options object to be validated.
  97. * @returns A custom object with ttl converted to protobuf Duration string format.
  98. */
  99. validateTokenOptions(options) {
  100. if (!validator.isNonNullObject(options)) {
  101. throw new app_check_api_client_internal_1.FirebaseAppCheckError('invalid-argument', 'AppCheckTokenOptions must be a non-null object.');
  102. }
  103. if (typeof options.ttlMillis !== 'undefined') {
  104. if (!validator.isNumber(options.ttlMillis)) {
  105. throw new app_check_api_client_internal_1.FirebaseAppCheckError('invalid-argument', 'ttlMillis must be a duration in milliseconds.');
  106. }
  107. // ttlMillis must be between 30 minutes and 7 days (inclusive)
  108. if (options.ttlMillis < (ONE_MINUTE_IN_MILLIS * 30) || options.ttlMillis > (ONE_DAY_IN_MILLIS * 7)) {
  109. throw new app_check_api_client_internal_1.FirebaseAppCheckError('invalid-argument', 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).');
  110. }
  111. return { ttl: (0, utils_1.transformMillisecondsToSecondsString)(options.ttlMillis) };
  112. }
  113. return {};
  114. }
  115. }
  116. exports.AppCheckTokenGenerator = AppCheckTokenGenerator;
  117. /**
  118. * Creates a new `FirebaseAppCheckError` by extracting the error code, message and other relevant
  119. * details from a `CryptoSignerError`.
  120. *
  121. * @param err - The Error to convert into a `FirebaseAppCheckError` error
  122. * @returns A Firebase App Check error that can be returned to the user.
  123. */
  124. function appCheckErrorFromCryptoSignerError(err) {
  125. if (!(err instanceof crypto_signer_1.CryptoSignerError)) {
  126. return err;
  127. }
  128. if (err.code === crypto_signer_1.CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) {
  129. const httpError = err.cause;
  130. const errorResponse = httpError.response.data;
  131. if (errorResponse?.error) {
  132. const status = errorResponse.error.status;
  133. const description = errorResponse.error.message || JSON.stringify(httpError.response);
  134. let code = 'unknown-error';
  135. if (status && status in app_check_api_client_internal_1.APP_CHECK_ERROR_CODE_MAPPING) {
  136. code = app_check_api_client_internal_1.APP_CHECK_ERROR_CODE_MAPPING[status];
  137. }
  138. return new app_check_api_client_internal_1.FirebaseAppCheckError(code, `Error returned from server while signing a custom token: ${description}`);
  139. }
  140. return new app_check_api_client_internal_1.FirebaseAppCheckError('internal-error', 'Error returned from server: ' + JSON.stringify(errorResponse) + '.');
  141. }
  142. return new app_check_api_client_internal_1.FirebaseAppCheckError(mapToAppCheckErrorCode(err.code), err.message);
  143. }
  144. exports.appCheckErrorFromCryptoSignerError = appCheckErrorFromCryptoSignerError;
  145. function mapToAppCheckErrorCode(code) {
  146. switch (code) {
  147. case crypto_signer_1.CryptoSignerErrorCode.INVALID_CREDENTIAL:
  148. return 'invalid-credential';
  149. case crypto_signer_1.CryptoSignerErrorCode.INVALID_ARGUMENT:
  150. return 'invalid-argument';
  151. default:
  152. return 'internal-error';
  153. }
  154. }