token-verifier.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * Copyright 2018 Google Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.createSessionCookieVerifier = exports.createAuthBlockingTokenVerifier = exports.createIdTokenVerifier = exports.FirebaseTokenVerifier = exports.SESSION_COOKIE_INFO = exports.AUTH_BLOCKING_TOKEN_INFO = exports.ID_TOKEN_INFO = void 0;
  20. const error_1 = require("../utils/error");
  21. const util = require("../utils/index");
  22. const validator = require("../utils/validator");
  23. const jwt_1 = require("../utils/jwt");
  24. // Audience to use for Firebase Auth Custom tokens
  25. const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
  26. // URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
  27. // Auth ID tokens)
  28. const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
  29. // URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon.
  30. const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys';
  31. const EMULATOR_VERIFIER = new jwt_1.EmulatorSignatureVerifier();
  32. /**
  33. * User facing token information related to the Firebase ID token.
  34. *
  35. * @internal
  36. */
  37. exports.ID_TOKEN_INFO = {
  38. url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens',
  39. verifyApiName: 'verifyIdToken()',
  40. jwtName: 'Firebase ID token',
  41. shortName: 'ID token',
  42. expiredErrorCode: error_1.AuthClientErrorCode.ID_TOKEN_EXPIRED,
  43. };
  44. /**
  45. * User facing token information related to the Firebase Auth Blocking token.
  46. *
  47. * @internal
  48. */
  49. exports.AUTH_BLOCKING_TOKEN_INFO = {
  50. url: 'https://cloud.google.com/identity-platform/docs/blocking-functions',
  51. verifyApiName: '_verifyAuthBlockingToken()',
  52. jwtName: 'Firebase Auth Blocking token',
  53. shortName: 'Auth Blocking token',
  54. expiredErrorCode: error_1.AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED,
  55. };
  56. /**
  57. * User facing token information related to the Firebase session cookie.
  58. *
  59. * @internal
  60. */
  61. exports.SESSION_COOKIE_INFO = {
  62. url: 'https://firebase.google.com/docs/auth/admin/manage-cookies',
  63. verifyApiName: 'verifySessionCookie()',
  64. jwtName: 'Firebase session cookie',
  65. shortName: 'session cookie',
  66. expiredErrorCode: error_1.AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
  67. };
  68. /**
  69. * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies.
  70. *
  71. * @internal
  72. */
  73. class FirebaseTokenVerifier {
  74. constructor(clientCertUrl, issuer, tokenInfo, app) {
  75. this.issuer = issuer;
  76. this.tokenInfo = tokenInfo;
  77. this.app = app;
  78. if (!validator.isURL(clientCertUrl)) {
  79. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.');
  80. }
  81. else if (!validator.isURL(issuer)) {
  82. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.');
  83. }
  84. else if (!validator.isNonNullObject(tokenInfo)) {
  85. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.');
  86. }
  87. else if (!validator.isURL(tokenInfo.url)) {
  88. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.');
  89. }
  90. else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) {
  91. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The JWT verify API name must be a non-empty string.');
  92. }
  93. else if (!validator.isNonEmptyString(tokenInfo.jwtName)) {
  94. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The JWT public full name must be a non-empty string.');
  95. }
  96. else if (!validator.isNonEmptyString(tokenInfo.shortName)) {
  97. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The JWT public short name must be a non-empty string.');
  98. }
  99. else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) {
  100. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'The JWT expiration error code must be a non-null ErrorInfo object.');
  101. }
  102. this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a';
  103. this.signatureVerifier =
  104. jwt_1.PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl, app.options.httpAgent);
  105. // For backward compatibility, the project ID is validated in the verification call.
  106. }
  107. /**
  108. * Verifies the format and signature of a Firebase Auth JWT token.
  109. *
  110. * @param jwtToken - The Firebase Auth JWT token to verify.
  111. * @param isEmulator - Whether to accept Auth Emulator tokens.
  112. * @returns A promise fulfilled with the decoded claims of the Firebase Auth ID token.
  113. */
  114. verifyJWT(jwtToken, isEmulator = false) {
  115. if (!validator.isString(jwtToken)) {
  116. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`);
  117. }
  118. return this.ensureProjectId()
  119. .then((projectId) => {
  120. return this.decodeAndVerify(jwtToken, projectId, isEmulator);
  121. })
  122. .then((decoded) => {
  123. const decodedIdToken = decoded.payload;
  124. decodedIdToken.uid = decodedIdToken.sub;
  125. return decodedIdToken;
  126. });
  127. }
  128. /** @alpha */
  129. // eslint-disable-next-line @typescript-eslint/naming-convention
  130. _verifyAuthBlockingToken(jwtToken, isEmulator, audience) {
  131. if (!validator.isString(jwtToken)) {
  132. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`);
  133. }
  134. return this.ensureProjectId()
  135. .then((projectId) => {
  136. if (typeof audience === 'undefined') {
  137. audience = `${projectId}.cloudfunctions.net/`;
  138. }
  139. return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience);
  140. })
  141. .then((decoded) => {
  142. const decodedAuthBlockingToken = decoded.payload;
  143. decodedAuthBlockingToken.uid = decodedAuthBlockingToken.sub;
  144. return decodedAuthBlockingToken;
  145. });
  146. }
  147. ensureProjectId() {
  148. return util.findProjectId(this.app)
  149. .then((projectId) => {
  150. if (!validator.isNonEmptyString(projectId)) {
  151. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREDENTIAL, 'Must initialize app with a cert credential or set your Firebase project ID as the ' +
  152. `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`);
  153. }
  154. return Promise.resolve(projectId);
  155. });
  156. }
  157. decodeAndVerify(token, projectId, isEmulator, audience) {
  158. return this.safeDecode(token)
  159. .then((decodedToken) => {
  160. this.verifyContent(decodedToken, projectId, isEmulator, audience);
  161. return this.verifySignature(token, isEmulator)
  162. .then(() => decodedToken);
  163. });
  164. }
  165. safeDecode(jwtToken) {
  166. return (0, jwt_1.decodeJwt)(jwtToken)
  167. .catch((err) => {
  168. if (err.code === jwt_1.JwtErrorCode.INVALID_ARGUMENT) {
  169. const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
  170. `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
  171. const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` +
  172. `the entire string JWT which represents ${this.shortNameArticle} ` +
  173. `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage;
  174. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
  175. }
  176. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, err.message);
  177. });
  178. }
  179. /**
  180. * Verifies the content of a Firebase Auth JWT.
  181. *
  182. * @param fullDecodedToken - The decoded JWT.
  183. * @param projectId - The Firebase Project Id.
  184. * @param isEmulator - Whether the token is an Emulator token.
  185. */
  186. verifyContent(fullDecodedToken, projectId, isEmulator, audience) {
  187. const header = fullDecodedToken && fullDecodedToken.header;
  188. const payload = fullDecodedToken && fullDecodedToken.payload;
  189. const projectIdMatchMessage = ` Make sure the ${this.tokenInfo.shortName} comes from the same ` +
  190. 'Firebase project as the service account used to authenticate this SDK.';
  191. const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
  192. `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
  193. let errorMessage;
  194. if (!isEmulator && typeof header.kid === 'undefined') {
  195. const isCustomToken = (payload.aud === FIREBASE_AUDIENCE);
  196. const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d);
  197. if (isCustomToken) {
  198. errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` +
  199. `${this.tokenInfo.shortName}, but was given a custom token.`;
  200. }
  201. else if (isLegacyCustomToken) {
  202. errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` +
  203. `${this.tokenInfo.shortName}, but was given a legacy custom token.`;
  204. }
  205. else {
  206. errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`;
  207. }
  208. errorMessage += verifyJwtTokenDocsMessage;
  209. }
  210. else if (!isEmulator && header.alg !== jwt_1.ALGORITHM_RS256) {
  211. errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + jwt_1.ALGORITHM_RS256 + '" but got ' +
  212. '"' + header.alg + '".' + verifyJwtTokenDocsMessage;
  213. }
  214. else if (typeof audience !== 'undefined' && !payload.aud.includes(audience)) {
  215. errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
  216. audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage;
  217. }
  218. else if (typeof audience === 'undefined' && payload.aud !== projectId) {
  219. errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
  220. projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
  221. verifyJwtTokenDocsMessage;
  222. }
  223. else if (payload.iss !== this.issuer + projectId) {
  224. errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
  225. `"${this.issuer}` + projectId + '" but got "' +
  226. payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage;
  227. }
  228. else if (!(payload.event_type !== undefined &&
  229. (payload.event_type === 'beforeSendSms' || payload.event_type === 'beforeSendEmail'))) {
  230. // excluding `beforeSendSms` and `beforeSendEmail` from processing `sub` as there is no user record available.
  231. // `sub` is the same as `uid` which is part of the user record.
  232. if (typeof payload.sub !== 'string') {
  233. errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
  234. }
  235. else if (payload.sub === '') {
  236. errorMessage = `${this.tokenInfo.jwtName} has an empty "sub" (subject) claim.` +
  237. verifyJwtTokenDocsMessage;
  238. }
  239. else if (payload.sub.length > 128) {
  240. errorMessage = `${this.tokenInfo.jwtName} has a "sub" (subject) claim longer than 128 characters.` +
  241. verifyJwtTokenDocsMessage;
  242. }
  243. }
  244. if (errorMessage) {
  245. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
  246. }
  247. }
  248. verifySignature(jwtToken, isEmulator) {
  249. const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier;
  250. return verifier.verify(jwtToken)
  251. .catch((error) => {
  252. throw this.mapJwtErrorToAuthError(error);
  253. });
  254. }
  255. /**
  256. * Maps JwtError to FirebaseAuthError
  257. *
  258. * @param error - JwtError to be mapped.
  259. * @returns FirebaseAuthError or Error instance.
  260. */
  261. mapJwtErrorToAuthError(error) {
  262. const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
  263. `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
  264. if (error.code === jwt_1.JwtErrorCode.TOKEN_EXPIRED) {
  265. const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
  266. ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
  267. verifyJwtTokenDocsMessage;
  268. return new error_1.FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage);
  269. }
  270. else if (error.code === jwt_1.JwtErrorCode.INVALID_SIGNATURE) {
  271. const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
  272. return new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
  273. }
  274. else if (error.code === jwt_1.JwtErrorCode.NO_MATCHING_KID) {
  275. const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` +
  276. `correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` +
  277. 'is expired, so get a fresh token from your client app and try again.';
  278. return new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
  279. }
  280. return new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, error.message);
  281. }
  282. }
  283. exports.FirebaseTokenVerifier = FirebaseTokenVerifier;
  284. /**
  285. * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
  286. *
  287. * @internal
  288. * @param app - Firebase app instance.
  289. * @returns FirebaseTokenVerifier
  290. */
  291. function createIdTokenVerifier(app) {
  292. return new FirebaseTokenVerifier(CLIENT_CERT_URL, 'https://securetoken.google.com/', exports.ID_TOKEN_INFO, app);
  293. }
  294. exports.createIdTokenVerifier = createIdTokenVerifier;
  295. /**
  296. * Creates a new FirebaseTokenVerifier to verify Firebase Auth Blocking tokens.
  297. *
  298. * @internal
  299. * @param app - Firebase app instance.
  300. * @returns FirebaseTokenVerifier
  301. */
  302. function createAuthBlockingTokenVerifier(app) {
  303. return new FirebaseTokenVerifier(CLIENT_CERT_URL, 'https://securetoken.google.com/', exports.AUTH_BLOCKING_TOKEN_INFO, app);
  304. }
  305. exports.createAuthBlockingTokenVerifier = createAuthBlockingTokenVerifier;
  306. /**
  307. * Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
  308. *
  309. * @internal
  310. * @param app - Firebase app instance.
  311. * @returns FirebaseTokenVerifier
  312. */
  313. function createSessionCookieVerifier(app) {
  314. return new FirebaseTokenVerifier(SESSION_COOKIE_CERT_URL, 'https://session.firebase.google.com/', exports.SESSION_COOKIE_INFO, app);
  315. }
  316. exports.createSessionCookieVerifier = createSessionCookieVerifier;