summaryrefslogtreecommitdiffstats
path: root/services/sync/tps/extensions/tps/resource/auth/fxaccounts.sys.mjs
blob: 81c0fd578add92e504742e0d53530a3e9e72f6bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

import { Log } from "resource://gre/modules/Log.sys.mjs";
import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";

import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";

const fxAccounts = getFxAccountsSingleton();
import { FxAccountsClient } from "resource://gre/modules/FxAccountsClient.sys.mjs";
import { FxAccountsConfig } from "resource://gre/modules/FxAccountsConfig.sys.mjs";
import { Logger } from "resource://tps/logger.sys.mjs";

/**
 * Helper object for Firefox Accounts authentication
 */
export var Authentication = {
  /**
   * Check if an user has been logged in
   */
  async isLoggedIn() {
    return !!(await this.getSignedInUser());
  },

  async isReady() {
    let user = await this.getSignedInUser();
    return user && user.verified;
  },

  _getRestmailUsername(user) {
    const restmailSuffix = "@restmail.net";
    if (user.toLowerCase().endsWith(restmailSuffix)) {
      return user.slice(0, -restmailSuffix.length);
    }
    return null;
  },

  async shortWaitForVerification(ms) {
    let userData = await this.getSignedInUser();
    let timeoutID;
    let timeoutPromise = new Promise(resolve => {
      timeoutID = setTimeout(() => {
        Logger.logInfo(`Warning: no verification after ${ms}ms.`);
        resolve();
      }, ms);
    });
    await Promise.race([
      fxAccounts.whenVerified(userData).finally(() => clearTimeout(timeoutID)),
      timeoutPromise,
    ]);
    userData = await this.getSignedInUser();
    return userData && userData.verified;
  },

  async _openVerificationPage(uri) {
    let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
    let newtab = mainWindow.gBrowser.addWebTab(uri);
    let win = mainWindow.gBrowser.getBrowserForTab(newtab);
    await new Promise(resolve => {
      win.addEventListener("loadend", resolve, { once: true });
    });
    let didVerify = await this.shortWaitForVerification(10000);
    mainWindow.gBrowser.removeTab(newtab);
    return didVerify;
  },

  async _completeVerification(user) {
    let username = this._getRestmailUsername(user);
    if (!username) {
      Logger.logInfo(
        `Username "${user}" isn't a restmail username so can't complete verification`
      );
      return false;
    }
    Logger.logInfo("Fetching mail (from restmail) for user " + username);
    let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
      username
    )}`;
    let triedAlready = new Set();
    const tries = 10;
    const normalWait = 2000;
    for (let i = 0; i < tries; ++i) {
      let resp = await fetch(restmailURI);
      let messages = await resp.json();
      // Sort so that the most recent emails are first.
      messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt));
      for (let m of messages) {
        // We look for a link that has a x-link that we haven't yet tried.
        if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) {
          continue;
        }
        let confirmLink = m.headers["x-link"];
        triedAlready.add(confirmLink);
        Logger.logInfo("Trying confirmation link " + confirmLink);
        try {
          if (await this._openVerificationPage(confirmLink)) {
            return true;
          }
        } catch (e) {
          Logger.logInfo(
            "Warning: Failed to follow confirmation link: " +
              Log.exceptionStr(e)
          );
        }
      }
      if (i === 0) {
        // first time through after failing we'll do this.
        await fxAccounts.resendVerificationEmail();
      }
      if (await this.shortWaitForVerification(normalWait)) {
        return true;
      }
    }
    // One last try.
    return this.shortWaitForVerification(normalWait);
  },

  async deleteEmail(user) {
    let username = this._getRestmailUsername(user);
    if (!username) {
      Logger.logInfo("Not a restmail username, can't delete");
      return false;
    }
    Logger.logInfo("Deleting mail (from restmail) for user " + username);
    let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
      username
    )}`;
    try {
      // Clean up after ourselves.
      let deleteResult = await fetch(restmailURI, { method: "DELETE" });
      if (!deleteResult.ok) {
        Logger.logInfo(
          `Warning: Got non-success status ${deleteResult.status} when deleting emails`
        );
        return false;
      }
    } catch (e) {
      Logger.logInfo(
        "Warning: Failed to delete old emails: " + Log.exceptionStr(e)
      );
      return false;
    }
    return true;
  },

  /**
   * Wrapper to retrieve the currently signed in user
   *
   * @returns Information about the currently signed in user
   */
  async getSignedInUser() {
    try {
      return await fxAccounts.getSignedInUser();
    } catch (error) {
      Logger.logError(
        "getSignedInUser() failed with: " + JSON.stringify(error)
      );
      throw error;
    }
  },

  /**
   * Wrapper to synchronize the login of a user
   *
   * @param account
   *        Account information of the user to login
   * @param account.username
   *        The username for the account (utf8)
   * @param account.password
   *        The user's password
   */
  async signIn(account) {
    Logger.AssertTrue(account.username, "Username has been found");
    Logger.AssertTrue(account.password, "Password has been found");

    Logger.logInfo("Login user: " + account.username);

    try {
      // Required here since we don't go through the real login page
      await FxAccountsConfig.ensureConfigured();

      let client = new FxAccountsClient();
      let credentials = await client.signIn(
        account.username,
        account.password,
        true
      );
      await fxAccounts._internal.setSignedInUser(credentials);
      if (!credentials.verified) {
        await this._completeVerification(account.username);
      }

      return true;
    } catch (error) {
      throw new Error("signIn() failed with: " + error.message);
    }
  },

  /**
   * Sign out of Firefox Accounts.
   */
  async signOut() {
    if (await Authentication.isLoggedIn()) {
      // Note: This will clean up the device ID.
      await fxAccounts.signOut();
    }
  },
};