Bug 1814091 - Move CanvasContext.getPreferredFormat to GPU.getPreferredCanvasFormat...
[gecko.git] / services / fxaccounts / FxAccountsClient.sys.mjs
blob093df838de07c29c96713215266676aa3eeb4c65
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
7 import { HawkClient } from "resource://services-common/hawkclient.sys.mjs";
8 import { deriveHawkCredentials } from "resource://services-common/hawkrequest.sys.mjs";
9 import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs";
11 const {
12   ERRNO_ACCOUNT_DOES_NOT_EXIST,
13   ERRNO_INCORRECT_EMAIL_CASE,
14   ERRNO_INCORRECT_PASSWORD,
15   ERRNO_INVALID_AUTH_NONCE,
16   ERRNO_INVALID_AUTH_TIMESTAMP,
17   ERRNO_INVALID_AUTH_TOKEN,
18   log,
19 } = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
20 import { Credentials } from "resource://gre/modules/Credentials.sys.mjs";
22 const HOST_PREF = "identity.fxaccounts.auth.uri";
24 const SIGNIN = "/account/login";
25 const SIGNUP = "/account/create";
26 // Devices older than this many days will not appear in the devices list
27 const DEVICES_FILTER_DAYS = 21;
29 export var FxAccountsClient = function(
30   host = Services.prefs.getCharPref(HOST_PREF)
31 ) {
32   this.host = host;
34   // The FxA auth server expects requests to certain endpoints to be authorized
35   // using Hawk.
36   this.hawk = new HawkClient(host);
37   this.hawk.observerPrefix = "FxA:hawk";
39   // Manage server backoff state. C.f.
40   // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol
41   this.backoffError = null;
44 FxAccountsClient.prototype = {
45   /**
46    * Return client clock offset, in milliseconds, as determined by hawk client.
47    * Provided because callers should not have to know about hawk
48    * implementation.
49    *
50    * The offset is the number of milliseconds that must be added to the client
51    * clock to make it equal to the server clock.  For example, if the client is
52    * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
53    */
54   get localtimeOffsetMsec() {
55     return this.hawk.localtimeOffsetMsec;
56   },
58   /*
59    * Return current time in milliseconds
60    *
61    * Not used by this module, but made available to the FxAccounts.jsm
62    * that uses this client.
63    */
64   now() {
65     return this.hawk.now();
66   },
68   /**
69    * Common code from signIn and signUp.
70    *
71    * @param path
72    *        Request URL path. Can be /account/create or /account/login
73    * @param email
74    *        The email address for the account (utf8)
75    * @param password
76    *        The user's password
77    * @param [getKeys=false]
78    *        If set to true the keyFetchToken will be retrieved
79    * @param [retryOK=true]
80    *        If capitalization of the email is wrong and retryOK is set to true,
81    *        we will retry with the suggested capitalization from the server
82    * @return Promise
83    *        Returns a promise that resolves to an object:
84    *        {
85    *          authAt: authentication time for the session (seconds since epoch)
86    *          email: the primary email for this account
87    *          keyFetchToken: a key fetch token (hex)
88    *          sessionToken: a session token (hex)
89    *          uid: the user's unique ID (hex)
90    *          unwrapBKey: used to unwrap kB, derived locally from the
91    *                      password (not revealed to the FxA server)
92    *          verified (optional): flag indicating verification status of the
93    *                               email
94    *        }
95    */
96   _createSession(path, email, password, getKeys = false, retryOK = true) {
97     return Credentials.setup(email, password).then(creds => {
98       let data = {
99         authPW: CommonUtils.bytesAsHex(creds.authPW),
100         email,
101       };
102       let keys = getKeys ? "?keys=true" : "";
104       return this._request(path + keys, "POST", null, data).then(
105         // Include the canonical capitalization of the email in the response so
106         // the caller can set its signed-in user state accordingly.
107         result => {
108           result.email = data.email;
109           result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey);
111           return result;
112         },
113         error => {
114           log.debug("Session creation failed", error);
115           // If the user entered an email with different capitalization from
116           // what's stored in the database (e.g., Greta.Garbo@gmail.COM as
117           // opposed to greta.garbo@gmail.com), the server will respond with a
118           // errno 120 (code 400) and the expected capitalization of the email.
119           // We retry with this email exactly once.  If successful, we use the
120           // server's version of the email as the signed-in-user's email. This
121           // is necessary because the email also serves as salt; so we must be
122           // in agreement with the server on capitalization.
123           //
124           // API reference:
125           // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md
126           if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) {
127             if (!error.email) {
128               log.error("Server returned errno 120 but did not provide email");
129               throw error;
130             }
131             return this._createSession(
132               path,
133               error.email,
134               password,
135               getKeys,
136               false
137             );
138           }
139           throw error;
140         }
141       );
142     });
143   },
145   /**
146    * Create a new Firefox Account and authenticate
147    *
148    * @param email
149    *        The email address for the account (utf8)
150    * @param password
151    *        The user's password
152    * @param [getKeys=false]
153    *        If set to true the keyFetchToken will be retrieved
154    * @return Promise
155    *        Returns a promise that resolves to an object:
156    *        {
157    *          uid: the user's unique ID (hex)
158    *          sessionToken: a session token (hex)
159    *          keyFetchToken: a key fetch token (hex),
160    *          unwrapBKey: used to unwrap kB, derived locally from the
161    *                      password (not revealed to the FxA server)
162    *        }
163    */
164   signUp(email, password, getKeys = false) {
165     return this._createSession(
166       SIGNUP,
167       email,
168       password,
169       getKeys,
170       false /* no retry */
171     );
172   },
174   /**
175    * Authenticate and create a new session with the Firefox Account API server
176    *
177    * @param email
178    *        The email address for the account (utf8)
179    * @param password
180    *        The user's password
181    * @param [getKeys=false]
182    *        If set to true the keyFetchToken will be retrieved
183    * @return Promise
184    *        Returns a promise that resolves to an object:
185    *        {
186    *          authAt: authentication time for the session (seconds since epoch)
187    *          email: the primary email for this account
188    *          keyFetchToken: a key fetch token (hex)
189    *          sessionToken: a session token (hex)
190    *          uid: the user's unique ID (hex)
191    *          unwrapBKey: used to unwrap kB, derived locally from the
192    *                      password (not revealed to the FxA server)
193    *          verified: flag indicating verification status of the email
194    *        }
195    */
196   signIn: function signIn(email, password, getKeys = false) {
197     return this._createSession(
198       SIGNIN,
199       email,
200       password,
201       getKeys,
202       true /* retry */
203     );
204   },
206   /**
207    * Check the status of a session given a session token
208    *
209    * @param sessionTokenHex
210    *        The session token encoded in hex
211    * @return Promise
212    *        Resolves with a boolean indicating if the session is still valid
213    */
214   async sessionStatus(sessionTokenHex) {
215     const credentials = await deriveHawkCredentials(
216       sessionTokenHex,
217       "sessionToken"
218     );
219     return this._request("/session/status", "GET", credentials).then(
220       () => Promise.resolve(true),
221       error => {
222         if (isInvalidTokenError(error)) {
223           return Promise.resolve(false);
224         }
225         throw error;
226       }
227     );
228   },
230   /**
231    * List all the clients connected to the authenticated user's account,
232    * including devices, OAuth clients, and web sessions.
233    *
234    * @param sessionTokenHex
235    *        The session token encoded in hex
236    * @return Promise
237    */
238   async attachedClients(sessionTokenHex) {
239     const credentials = await deriveHawkCredentials(
240       sessionTokenHex,
241       "sessionToken"
242     );
243     return this._requestWithHeaders(
244       "/account/attached_clients",
245       "GET",
246       credentials
247     );
248   },
250   /**
251    * Retrieves an OAuth authorization code.
252    *
253    * @param String sessionTokenHex
254    *        The session token encoded in hex
255    * @param {Object} options
256    * @param options.client_id
257    * @param options.state
258    * @param options.scope
259    * @param options.access_type
260    * @param options.code_challenge_method
261    * @param options.code_challenge
262    * @param [options.keys_jwe]
263    * @returns {Promise<Object>} Object containing `code` and `state`.
264    */
265   async oauthAuthorize(sessionTokenHex, options) {
266     const credentials = await deriveHawkCredentials(
267       sessionTokenHex,
268       "sessionToken"
269     );
270     const body = {
271       client_id: options.client_id,
272       response_type: "code",
273       state: options.state,
274       scope: options.scope,
275       access_type: options.access_type,
276       code_challenge: options.code_challenge,
277       code_challenge_method: options.code_challenge_method,
278     };
279     if (options.keys_jwe) {
280       body.keys_jwe = options.keys_jwe;
281     }
282     return this._request("/oauth/authorization", "POST", credentials, body);
283   },
285   /**
286    * Destroy an OAuth access token or refresh token.
287    *
288    * @param String clientId
289    * @param String token The token to be revoked.
290    */
291   async oauthDestroy(clientId, token) {
292     const body = {
293       client_id: clientId,
294       token,
295     };
296     return this._request("/oauth/destroy", "POST", null, body);
297   },
299   /**
300    * Query for the information required to derive
301    * scoped encryption keys requested by the specified OAuth client.
302    *
303    * @param sessionTokenHex
304    *        The session token encoded in hex
305    * @param clientId
306    * @param scope
307    *        Space separated list of scopes
308    * @return Promise
309    */
310   async getScopedKeyData(sessionTokenHex, clientId, scope) {
311     if (!clientId) {
312       throw new Error("Missing 'clientId' parameter");
313     }
314     if (!scope) {
315       throw new Error("Missing 'scope' parameter");
316     }
317     const params = {
318       client_id: clientId,
319       scope,
320     };
321     const credentials = await deriveHawkCredentials(
322       sessionTokenHex,
323       "sessionToken"
324     );
325     return this._request(
326       "/account/scoped-key-data",
327       "POST",
328       credentials,
329       params
330     );
331   },
333   /**
334    * Destroy the current session with the Firefox Account API server and its
335    * associated device.
336    *
337    * @param sessionTokenHex
338    *        The session token encoded in hex
339    * @return Promise
340    */
341   async signOut(sessionTokenHex, options = {}) {
342     const credentials = await deriveHawkCredentials(
343       sessionTokenHex,
344       "sessionToken"
345     );
346     let path = "/session/destroy";
347     if (options.service) {
348       path += "?service=" + encodeURIComponent(options.service);
349     }
350     return this._request(path, "POST", credentials);
351   },
353   /**
354    * Check the verification status of the user's FxA email address
355    *
356    * @param sessionTokenHex
357    *        The current session token encoded in hex
358    * @return Promise
359    */
360   async recoveryEmailStatus(sessionTokenHex, options = {}) {
361     const credentials = await deriveHawkCredentials(
362       sessionTokenHex,
363       "sessionToken"
364     );
365     let path = "/recovery_email/status";
366     if (options.reason) {
367       path += "?reason=" + encodeURIComponent(options.reason);
368     }
370     return this._request(path, "GET", credentials);
371   },
373   /**
374    * Resend the verification email for the user
375    *
376    * @param sessionTokenHex
377    *        The current token encoded in hex
378    * @return Promise
379    */
380   async resendVerificationEmail(sessionTokenHex) {
381     const credentials = await deriveHawkCredentials(
382       sessionTokenHex,
383       "sessionToken"
384     );
385     return this._request("/recovery_email/resend_code", "POST", credentials);
386   },
388   /**
389    * Retrieve encryption keys
390    *
391    * @param keyFetchTokenHex
392    *        A one-time use key fetch token encoded in hex
393    * @return Promise
394    *        Returns a promise that resolves to an object:
395    *        {
396    *          kA: an encryption key for recevorable data (bytes)
397    *          wrapKB: an encryption key that requires knowledge of the
398    *                  user's password (bytes)
399    *        }
400    */
401   async accountKeys(keyFetchTokenHex) {
402     let creds = await deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
403     let keyRequestKey = creds.extra.slice(0, 32);
404     let morecreds = await CryptoUtils.hkdfLegacy(
405       keyRequestKey,
406       undefined,
407       Credentials.keyWord("account/keys"),
408       3 * 32
409     );
410     let respHMACKey = morecreds.slice(0, 32);
411     let respXORKey = morecreds.slice(32, 96);
413     const resp = await this._request("/account/keys", "GET", creds);
414     if (!resp.bundle) {
415       throw new Error("failed to retrieve keys");
416     }
418     let bundle = CommonUtils.hexToBytes(resp.bundle);
419     let mac = bundle.slice(-32);
420     let key = CommonUtils.byteStringToArrayBuffer(respHMACKey);
421     // CryptoUtils.hmac takes ArrayBuffers as inputs for the key and data and
422     // returns an ArrayBuffer.
423     let bundleMAC = await CryptoUtils.hmac(
424       "SHA-256",
425       key,
426       CommonUtils.byteStringToArrayBuffer(bundle.slice(0, -32))
427     );
428     if (mac !== CommonUtils.arrayBufferToByteString(bundleMAC)) {
429       throw new Error("error unbundling encryption keys");
430     }
432     let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
434     return {
435       kA: keyAWrapB.slice(0, 32),
436       wrapKB: keyAWrapB.slice(32),
437     };
438   },
440   /**
441    * Obtain an OAuth access token by authenticating using a session token.
442    *
443    * @param {String} sessionTokenHex
444    *        The session token encoded in hex
445    * @param {String} clientId
446    * @param {String} scope
447    *        List of space-separated scopes.
448    * @param {Number} ttl
449    *        Token time to live.
450    * @return {Promise<Object>} Object containing an `access_token`.
451    */
452   async accessTokenWithSessionToken(sessionTokenHex, clientId, scope, ttl) {
453     const credentials = await deriveHawkCredentials(
454       sessionTokenHex,
455       "sessionToken"
456     );
457     const body = {
458       client_id: clientId,
459       grant_type: "fxa-credentials",
460       scope,
461       ttl,
462     };
463     return this._request("/oauth/token", "POST", credentials, body);
464   },
466   /**
467    * Determine if an account exists
468    *
469    * @param email
470    *        The email address to check
471    * @return Promise
472    *        The promise resolves to true if the account exists, or false
473    *        if it doesn't. The promise is rejected on other errors.
474    */
475   accountExists(email) {
476     return this.signIn(email, "").then(
477       cantHappen => {
478         throw new Error("How did I sign in with an empty password?");
479       },
480       expectedError => {
481         switch (expectedError.errno) {
482           case ERRNO_ACCOUNT_DOES_NOT_EXIST:
483             return false;
484           case ERRNO_INCORRECT_PASSWORD:
485             return true;
486           default:
487             // not so expected, any more ...
488             throw expectedError;
489         }
490       }
491     );
492   },
494   /**
495    * Given the uid of an existing account (not an arbitrary email), ask
496    * the server if it still exists via /account/status.
497    *
498    * Used for differentiating between password change and account deletion.
499    */
500   accountStatus(uid) {
501     return this._request("/account/status?uid=" + uid, "GET").then(
502       result => {
503         return result.exists;
504       },
505       error => {
506         log.error("accountStatus failed", error);
507         return Promise.reject(error);
508       }
509     );
510   },
512   /**
513    * Register a new device
514    *
515    * @method registerDevice
516    * @param  sessionTokenHex
517    *         Session token obtained from signIn
518    * @param  name
519    *         Device name
520    * @param  type
521    *         Device type (mobile|desktop)
522    * @param  [options]
523    *         Extra device options
524    * @param  [options.availableCommands]
525    *         Available commands for this device
526    * @param  [options.pushCallback]
527    *         `pushCallback` push endpoint callback
528    * @param  [options.pushPublicKey]
529    *         `pushPublicKey` push public key (URLSafe Base64 string)
530    * @param  [options.pushAuthKey]
531    *         `pushAuthKey` push auth secret (URLSafe Base64 string)
532    * @return Promise
533    *         Resolves to an object:
534    *         {
535    *           id: Device identifier
536    *           createdAt: Creation time (milliseconds since epoch)
537    *           name: Name of device
538    *           type: Type of device (mobile|desktop)
539    *         }
540    */
541   async registerDevice(sessionTokenHex, name, type, options = {}) {
542     let path = "/account/device";
544     let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
545     let body = { name, type };
547     if (options.pushCallback) {
548       body.pushCallback = options.pushCallback;
549     }
550     if (options.pushPublicKey && options.pushAuthKey) {
551       body.pushPublicKey = options.pushPublicKey;
552       body.pushAuthKey = options.pushAuthKey;
553     }
554     body.availableCommands = options.availableCommands;
556     return this._request(path, "POST", creds, body);
557   },
559   /**
560    * Sends a message to other devices. Must conform with the push payload schema:
561    * https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json
562    *
563    * @method notifyDevice
564    * @param  sessionTokenHex
565    *         Session token obtained from signIn
566    * @param  deviceIds
567    *         Devices to send the message to. If null, will be sent to all devices.
568    * @param  excludedIds
569    *         Devices to exclude when sending to all devices (deviceIds must be null).
570    * @param  payload
571    *         Data to send with the message
572    * @return Promise
573    *         Resolves to an empty object:
574    *         {}
575    */
576   async notifyDevices(
577     sessionTokenHex,
578     deviceIds,
579     excludedIds,
580     payload,
581     TTL = 0
582   ) {
583     const credentials = await deriveHawkCredentials(
584       sessionTokenHex,
585       "sessionToken"
586     );
587     if (deviceIds && excludedIds) {
588       throw new Error(
589         "You cannot specify excluded devices if deviceIds is set."
590       );
591     }
592     const body = {
593       to: deviceIds || "all",
594       payload,
595       TTL,
596     };
597     if (excludedIds) {
598       body.excluded = excludedIds;
599     }
600     return this._request("/account/devices/notify", "POST", credentials, body);
601   },
603   /**
604    * Retrieves pending commands for our device.
605    *
606    * @method getCommands
607    * @param  sessionTokenHex - Session token obtained from signIn
608    * @param  [index] - If specified, only messages received after the one who
609    *                   had that index will be retrieved.
610    * @param  [limit] - Maximum number of messages to retrieve.
611    */
612   async getCommands(sessionTokenHex, { index, limit }) {
613     const credentials = await deriveHawkCredentials(
614       sessionTokenHex,
615       "sessionToken"
616     );
617     const params = new URLSearchParams();
618     if (index != undefined) {
619       params.set("index", index);
620     }
621     if (limit != undefined) {
622       params.set("limit", limit);
623     }
624     const path = `/account/device/commands?${params.toString()}`;
625     return this._request(path, "GET", credentials);
626   },
628   /**
629    * Invokes a command on another device.
630    *
631    * @method invokeCommand
632    * @param  sessionTokenHex - Session token obtained from signIn
633    * @param  command - Name of the command to invoke
634    * @param  target - Recipient device ID.
635    * @param  payload
636    * @return Promise
637    *         Resolves to the request's response, (which should be an empty object)
638    */
639   async invokeCommand(sessionTokenHex, command, target, payload) {
640     const credentials = await deriveHawkCredentials(
641       sessionTokenHex,
642       "sessionToken"
643     );
644     const body = {
645       command,
646       target,
647       payload,
648     };
649     return this._request(
650       "/account/devices/invoke_command",
651       "POST",
652       credentials,
653       body
654     );
655   },
657   /**
658    * Update the session or name for an existing device
659    *
660    * @method updateDevice
661    * @param  sessionTokenHex
662    *         Session token obtained from signIn
663    * @param  id
664    *         Device identifier
665    * @param  name
666    *         Device name
667    * @param  [options]
668    *         Extra device options
669    * @param  [options.availableCommands]
670    *         Available commands for this device
671    * @param  [options.pushCallback]
672    *         `pushCallback` push endpoint callback
673    * @param  [options.pushPublicKey]
674    *         `pushPublicKey` push public key (URLSafe Base64 string)
675    * @param  [options.pushAuthKey]
676    *         `pushAuthKey` push auth secret (URLSafe Base64 string)
677    * @return Promise
678    *         Resolves to an object:
679    *         {
680    *           id: Device identifier
681    *           name: Device name
682    *         }
683    */
684   async updateDevice(sessionTokenHex, id, name, options = {}) {
685     let path = "/account/device";
687     let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
688     let body = { id, name };
689     if (options.pushCallback) {
690       body.pushCallback = options.pushCallback;
691     }
692     if (options.pushPublicKey && options.pushAuthKey) {
693       body.pushPublicKey = options.pushPublicKey;
694       body.pushAuthKey = options.pushAuthKey;
695     }
696     body.availableCommands = options.availableCommands;
698     return this._request(path, "POST", creds, body);
699   },
701   /**
702    * Get a list of currently registered devices that have been accessed
703    * in the last `DEVICES_FILTER_DAYS` days
704    *
705    * @method getDeviceList
706    * @param  sessionTokenHex
707    *         Session token obtained from signIn
708    * @return Promise
709    *         Resolves to an array of objects:
710    *         [
711    *           {
712    *             id: Device id
713    *             isCurrentDevice: Boolean indicating whether the item
714    *                              represents the current device
715    *             name: Device name
716    *             type: Device type (mobile|desktop)
717    *           },
718    *           ...
719    *         ]
720    */
721   async getDeviceList(sessionTokenHex) {
722     let timestamp = Date.now() - 1000 * 60 * 60 * 24 * DEVICES_FILTER_DAYS;
723     let path = `/account/devices?filterIdleDevicesTimestamp=${timestamp}`;
724     let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
725     return this._request(path, "GET", creds, {});
726   },
728   _clearBackoff() {
729     this.backoffError = null;
730   },
732   /**
733    * A general method for sending raw API calls to the FxA auth server.
734    * All request bodies and responses are JSON.
735    *
736    * @param path
737    *        API endpoint path
738    * @param method
739    *        The HTTP request method
740    * @param credentials
741    *        Hawk credentials
742    * @param jsonPayload
743    *        A JSON payload
744    * @return Promise
745    *        Returns a promise that resolves to the JSON response of the API call,
746    *        or is rejected with an error. Error responses have the following properties:
747    *        {
748    *          "code": 400, // matches the HTTP status code
749    *          "errno": 107, // stable application-level error number
750    *          "error": "Bad Request", // string description of the error type
751    *          "message": "the value of salt is not allowed to be undefined",
752    *          "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
753    *        }
754    */
755   async _requestWithHeaders(path, method, credentials, jsonPayload) {
756     // We were asked to back off.
757     if (this.backoffError) {
758       log.debug("Received new request during backoff, re-rejecting.");
759       throw this.backoffError;
760     }
761     let response;
762     try {
763       response = await this.hawk.request(
764         path,
765         method,
766         credentials,
767         jsonPayload
768       );
769     } catch (error) {
770       log.error(`error ${method}ing ${path}`, error);
771       if (error.retryAfter) {
772         log.debug("Received backoff response; caching error as flag.");
773         this.backoffError = error;
774         // Schedule clearing of cached-error-as-flag.
775         CommonUtils.namedTimer(
776           this._clearBackoff,
777           error.retryAfter * 1000,
778           this,
779           "fxaBackoffTimer"
780         );
781       }
782       throw error;
783     }
784     try {
785       return { body: JSON.parse(response.body), headers: response.headers };
786     } catch (error) {
787       log.error("json parse error on response: " + response.body);
788       // eslint-disable-next-line no-throw-literal
789       throw { error };
790     }
791   },
793   async _request(path, method, credentials, jsonPayload) {
794     const response = await this._requestWithHeaders(
795       path,
796       method,
797       credentials,
798       jsonPayload
799     );
800     return response.body;
801   },
804 function isInvalidTokenError(error) {
805   if (error.code != 401) {
806     return false;
807   }
808   switch (error.errno) {
809     case ERRNO_INVALID_AUTH_TOKEN:
810     case ERRNO_INVALID_AUTH_TIMESTAMP:
811     case ERRNO_INVALID_AUTH_NONCE:
812       return true;
813   }
814   return false;