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