mailnews/base/src/OAuth2Providers.sys.mjs
author Mozilla Releng Treescript <release+treescript@mozilla.org>
Fri, 11 Jul 2025 18:52:03 +0000 (7 hours ago)
changeset 45458 4031593033c0833fd362eb8e1c04f9aac163d5a2
parent 45401 fb2c057d0b7808fbd48d6917da801c8b88083c3b
permissions -rw-r--r--
no bug - Bumping Thunderbird l10n changesets r=release a=l10n-bump DONTBUILD af -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ar -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ast -> 566eb51108b1fc42316fd4e25ceb91db7718e979 be -> 566eb51108b1fc42316fd4e25ceb91db7718e979 bg -> 566eb51108b1fc42316fd4e25ceb91db7718e979 br -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ca -> 566eb51108b1fc42316fd4e25ceb91db7718e979 cak -> 566eb51108b1fc42316fd4e25ceb91db7718e979 cs -> 566eb51108b1fc42316fd4e25ceb91db7718e979 cy -> 566eb51108b1fc42316fd4e25ceb91db7718e979 da -> 566eb51108b1fc42316fd4e25ceb91db7718e979 de -> 566eb51108b1fc42316fd4e25ceb91db7718e979 dsb -> 566eb51108b1fc42316fd4e25ceb91db7718e979 el -> 566eb51108b1fc42316fd4e25ceb91db7718e979 en-CA -> 566eb51108b1fc42316fd4e25ceb91db7718e979 en-GB -> 566eb51108b1fc42316fd4e25ceb91db7718e979 es-AR -> 566eb51108b1fc42316fd4e25ceb91db7718e979 es-ES -> 566eb51108b1fc42316fd4e25ceb91db7718e979 es-MX -> 566eb51108b1fc42316fd4e25ceb91db7718e979 et -> 566eb51108b1fc42316fd4e25ceb91db7718e979 eu -> 566eb51108b1fc42316fd4e25ceb91db7718e979 fi -> 566eb51108b1fc42316fd4e25ceb91db7718e979 fr -> 566eb51108b1fc42316fd4e25ceb91db7718e979 fy-NL -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ga-IE -> 566eb51108b1fc42316fd4e25ceb91db7718e979 gd -> 566eb51108b1fc42316fd4e25ceb91db7718e979 gl -> 566eb51108b1fc42316fd4e25ceb91db7718e979 he -> 566eb51108b1fc42316fd4e25ceb91db7718e979 hr -> 566eb51108b1fc42316fd4e25ceb91db7718e979 hsb -> 566eb51108b1fc42316fd4e25ceb91db7718e979 hu -> 566eb51108b1fc42316fd4e25ceb91db7718e979 hy-AM -> 566eb51108b1fc42316fd4e25ceb91db7718e979 id -> 566eb51108b1fc42316fd4e25ceb91db7718e979 is -> 566eb51108b1fc42316fd4e25ceb91db7718e979 it -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ja -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ja-JP-mac -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ka -> 566eb51108b1fc42316fd4e25ceb91db7718e979 kab -> 566eb51108b1fc42316fd4e25ceb91db7718e979 kk -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ko -> 566eb51108b1fc42316fd4e25ceb91db7718e979 lt -> 566eb51108b1fc42316fd4e25ceb91db7718e979 lv -> 566eb51108b1fc42316fd4e25ceb91db7718e979 mk -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ms -> 566eb51108b1fc42316fd4e25ceb91db7718e979 nb-NO -> 566eb51108b1fc42316fd4e25ceb91db7718e979 nl -> 566eb51108b1fc42316fd4e25ceb91db7718e979 nn-NO -> 566eb51108b1fc42316fd4e25ceb91db7718e979 pa-IN -> 566eb51108b1fc42316fd4e25ceb91db7718e979 pl -> 566eb51108b1fc42316fd4e25ceb91db7718e979 pt-BR -> 566eb51108b1fc42316fd4e25ceb91db7718e979 pt-PT -> 566eb51108b1fc42316fd4e25ceb91db7718e979 rm -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ro -> 566eb51108b1fc42316fd4e25ceb91db7718e979 ru -> 566eb51108b1fc42316fd4e25ceb91db7718e979 sk -> 566eb51108b1fc42316fd4e25ceb91db7718e979 sl -> 566eb51108b1fc42316fd4e25ceb91db7718e979 sq -> 566eb51108b1fc42316fd4e25ceb91db7718e979 sr -> 566eb51108b1fc42316fd4e25ceb91db7718e979 sv-SE -> 566eb51108b1fc42316fd4e25ceb91db7718e979 th -> 566eb51108b1fc42316fd4e25ceb91db7718e979 tr -> 566eb51108b1fc42316fd4e25ceb91db7718e979 uk -> 566eb51108b1fc42316fd4e25ceb91db7718e979 uz -> 566eb51108b1fc42316fd4e25ceb91db7718e979 vi -> 566eb51108b1fc42316fd4e25ceb91db7718e979 zh-CN -> 566eb51108b1fc42316fd4e25ceb91db7718e979 zh-TW -> 566eb51108b1fc42316fd4e25ceb91db7718e979
/* 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/. */

/**
 * Details of supported OAuth2 Providers.
 */
// When we add a Google mail account, ask for address book and calendar scopes
// as well. Then we can add an address book or calendar without asking again.
//
// Don't ask for all the scopes when adding an address book or calendar
// independently of the mail set-up process. If a mail account already exists,
// we already have a token, and if it doesn't the user is likely to be setting
// up an address book/calendar without wanting mail.
const GOOGLE_SCOPES = {
  imap: "https://mail.google.com/",
  pop3: "https://mail.google.com/",
  smtp: "https://mail.google.com/",
  carddav: "https://www.googleapis.com/auth/carddav",
  caldav: "https://www.googleapis.com/auth/calendar",
};
const FASTMAIL_SCOPES = {
  imap: "https://www.fastmail.com/dev/protocol-imap",
  pop3: "https://www.fastmail.com/dev/protocol-pop",
  smtp: "https://www.fastmail.com/dev/protocol-smtp",
  carddav: "https://www.fastmail.com/dev/protocol-carddav",
  caldav: "https://www.fastmail.com/dev/protocol-caldav",
};
const COMCAST_SCOPES = "https://email.comcast.net/ profile openid";
const MICROSOFT_SCOPES = {
  imap: "https://outlook.office.com/IMAP.AccessAsUser.All",
  pop3: "https://outlook.office.com/POP.AccessAsUser.All",
  smtp: "https://outlook.office.com/SMTP.Send",
  extra: "offline_access",
};
const EWS_SCOPES = {
  ews: "https://outlook.office.com/EWS.AccessAsUser.All",
  // "exchange" is used in the account setup, then the config is copied to "ews".
  exchange: "https://outlook.office.com/EWS.AccessAsUser.All",
  extra: "offline_access",
};

/**
 * Map of hostnames to [issuer, scope].
 */
var kHostnames = new Map([
  ["imap.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
  ["smtp.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
  ["pop.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
  ["imap.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
  ["smtp.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
  ["pop.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
  ["www.googleapis.com", ["accounts.google.com", GOOGLE_SCOPES.carddav]],
  [
    "apidata.googleusercontent.com",
    ["accounts.google.com", GOOGLE_SCOPES.caldav],
  ],

  ["imap.mail.ru", ["o2.mail.ru", "mail.imap"]],
  ["smtp.mail.ru", ["o2.mail.ru", "mail.imap"]],

  ["imap.yandex.com", ["oauth.yandex.com", "mail:imap_full"]],
  ["smtp.yandex.com", ["oauth.yandex.com", "mail:smtp"]],

  ["imap.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
  ["pop.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
  ["smtp.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],

  ["imap.aol.com", ["login.aol.com", "mail-w"]],
  ["pop.aol.com", ["login.aol.com", "mail-w"]],
  ["smtp.aol.com", ["login.aol.com", "mail-w"]],

  // outlook.office365.com, smtp.office365.com
  ["office365.com", ["login.microsoftonline.com", MICROSOFT_SCOPES]],
  // autodiscover-s.outlook.com, smtp-mail.outlook.com
  ["outlook.com", ["login.microsoftonline.com", MICROSOFT_SCOPES]],
  // autodiscover.hotmail.com
  ["hotmail.com", ["login.microsoftonline.com", MICROSOFT_SCOPES]],

  ["imap.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
  ["pop.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
  ["smtp.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
  [
    "carddav.fastmail.com",
    ["www.fastmail.com", "https://www.fastmail.com/dev/protocol-carddav"],
  ],

  ["imap.comcast.net", ["comcast.net", COMCAST_SCOPES]],
  ["pop.comcast.net", ["comcast.net", COMCAST_SCOPES]],
  ["smtp.comcast.net", ["comcast.net", COMCAST_SCOPES]],

  // For testing purposes.
  ["mochi.test", ["test.test", "test_scope"]],
  [
    "test.test",
    [
      "test.test",
      {
        imap: "test_mail",
        pop3: "test_mail",
        smtp: "test_mail",
        ews: "test_mail",
        carddav: "test_addressbook",
        caldav: "test_calendar",
      },
    ],
  ],
]);

/**
 * Map of issuers to clientId, clientSecret, authorizationEndpoint, tokenEndpoint,
 *  and usePKCE (RFC7636).
 * Issuer is a unique string for the organization that a Thunderbird account
 * was registered at.
 *
 * For the moment these details are hard-coded, since dynamic client
 * registration is not yet supported. Don't copy these values for your
 * own application - register one for yourself! This code (and possibly even the
 * registration itself) will disappear when this is switched to dynamic
 * client registration.
 */
var kIssuers = new Map([
  [
    "accounts.google.com",
    {
      name: "accounts.google.com",
      builtIn: true,
      clientId:
        "406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com",
      clientSecret: "kSmqreRr0qwBWJgbf5Y-PjSU",
      authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
      tokenEndpoint: "https://www.googleapis.com/oauth2/v3/token",
    },
  ],
  [
    "o2.mail.ru",
    {
      name: "o2.mail.ru",
      builtIn: true,
      clientId: "thunderbird",
      clientSecret: "I0dCAXrcaNFujaaY",
      authorizationEndpoint: "https://o2.mail.ru/login",
      tokenEndpoint: "https://o2.mail.ru/token",
    },
  ],
  [
    "oauth.yandex.com",
    {
      name: "oauth.yandex.com",
      builtIn: true,
      clientId: "2a00bba7374047a6ab79666485ffce31",
      clientSecret: "3ded85b4ec574c2187a55dc49d361280",
      authorizationEndpoint: "https://oauth.yandex.com/authorize",
      tokenEndpoint: "https://oauth.yandex.com/token",
    },
  ],
  [
    "login.yahoo.com",
    {
      name: "login.yahoo.com",
      builtIn: true,
      clientId:
        "dj0yJmk9NUtCTWFMNVpTaVJmJmQ9WVdrOVJ6UjVTa2xJTXpRbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD0yYw--",
      clientSecret: "f2de6a30ae123cdbc258c15e0812799010d589cc",
      authorizationEndpoint: "https://api.login.yahoo.com/oauth2/request_auth",
      tokenEndpoint: "https://api.login.yahoo.com/oauth2/get_token",
    },
  ],
  [
    "login.aol.com",
    {
      name: "login.aol.com",
      builtIn: true,
      clientId:
        "dj0yJmk9OXRHc1FqZHRQYzVvJmQ9WVdrOU1UQnJOR0pvTjJrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD02NQ--",
      clientSecret: "79c1c11991d148ddd02a919000d69879942fc278",
      authorizationEndpoint: "https://api.login.aol.com/oauth2/request_auth",
      tokenEndpoint: "https://api.login.aol.com/oauth2/get_token",
    },
  ],

  [
    "login.microsoftonline.com",
    {
      name: "login.microsoftonline.com",
      builtIn: true,
      clientId: "9e5f94bc-e8a4-4e73-b8be-63364c29d753", // Application (client) ID
      // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
      authorizationEndpoint:
        "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
      tokenEndpoint:
        "https://login.microsoftonline.com/common/oauth2/v2.0/token",
      redirectionEndpoint: "https://localhost",
    },
  ],

  [
    "www.fastmail.com",
    {
      name: "www.fastmail.com",
      builtIn: true,
      clientId: "35f141ae",
      authorizationEndpoint: "https://api.fastmail.com/oauth/authorize",
      tokenEndpoint: "https://api.fastmail.com/oauth/refresh",
      usePKCE: true,
    },
  ],

  [
    "comcast.net",
    {
      name: "comcast.net",
      builtIn: true,
      clientId: "thunderbird-oauth",
      clientSecret: "fc5d0a314549bb3d059e0cec751fa4bd40a9cc7b",
      authorizationEndpoint: "https://oauth.xfinity.com/oauth/authorize",
      tokenEndpoint: "https://oauth.xfinity.com/oauth/token",
      usePKCE: true,
    },
  ],

  // For testing purposes.
  [
    "test.test",
    {
      name: "test.test",
      builtIn: true,
      clientId: "test_client_id",
      clientSecret: "test_secret",
      authorizationEndpoint: "https://oauth.test.test/form",
      tokenEndpoint: "https://oauth.test.test/token",
      redirectionEndpoint: "https://localhost",
    },
  ],
]);

/**
 * OAuth2Providers: Methods to lookup OAuth2 parameters for supported OAuth2
 * providers.
 */
export var OAuth2Providers = {
  /**
   * @typedef hostnameDetails
   * @property {string} issuer - A string representing the organization.
   * @property {string} allScopes - A space-separated list of all scopes for
   *   the hostname.
   * @property {string} requiredScopes - A space-separated list of all scopes
   *  required for the given type.
   */

  /**
   * Map a hostname to the relevant issuer and scope.
   *
   * @param {string} hostname - The hostname of the server. For example
   *  "imap.googlemail.com".
   * @param {string} type - The type of activity we need a token for,
   *   e.g. "imap" or "caldav".
   * @returns {hostnameDetails} An object containing issuer and scope information
   *   for the hostname and type, or undefined if not found.
   */
  getHostnameDetails(hostname, type) {
    if (!type) {
      throw new Error("passing a `type` argument is required");
    }
    if (type.startsWith("owl")) {
      type = "exchange";
    }

    const details = this._getHostnameDetails(hostname);
    if (!details) {
      // No data, return.
      return undefined;
    }

    let [issuer, scopes] = details;
    if (
      issuer == "login.microsoftonline.com" &&
      ["ews", "exchange"].includes(type)
    ) {
      // Special case for EWS, to avoid asking for the scope when not needed.
      scopes = EWS_SCOPES;
    }
    if (typeof scopes == "string") {
      // Scopes not separated into types.
      return { issuer, allScopes: scopes, requiredScopes: scopes };
    }

    const allScopes = combineScopes(Object.values(scopes));
    if (!scopes[type]) {
      // No data for type.
      return undefined;
    }

    const requiredScopes = combineScopes([scopes[type], scopes.extra]);
    return { issuer, allScopes, requiredScopes };
  },

  _getHostnameDetails(hostname) {
    // During CardDAV SRV autodiscovery, rfc6764#section-6 says:
    //
    // *  The client will need to make authenticated HTTP requests to
    //    the service.  Typically, a "user identifier" is required for
    //    some form of user/password authentication.  When a user
    //    identifier is required, clients MUST first use the "mailbox"
    //
    // However macOS Contacts does not do this and just uses the "localpart"
    // instead. To work around this bug, during SRV autodiscovery Fastmail
    // returns SRV records of the form '0 1 443 d[0-9]+.carddav.fastmail.com.'
    // which encodes the internal domainid of the queried SRV domain in the
    // sub-domain of the Target (rfc2782) of the SRV result. This can
    // then be extracted from the Host header on each DAV request, the
    // original domain looked up and attached to the "localpart" to create
    // a full "mailbox", allowing autodiscovery to just work for usernames
    // in any domain including self hosted domains.
    //
    // So for this hostname -> issuer/scope lookup to work, we need to
    // look not just at the hostname, but also any sub-domains of this
    // hostname.
    while (hostname.includes(".")) {
      const foundHost = kHostnames.get(hostname);
      if (foundHost) {
        return foundHost;
      }
      hostname = hostname.replace(/^[^.]*[.]/, "");
    }
    return undefined;
  },

  /**
   * Map an issuer to OAuth2 account details.
   *
   * @param {string} issuer - The organization issuing OAuth2 parameters, e.g.
   *   "accounts.google.com".
   *
   * @returns {Array} An array containing [clientId, clientSecret, authorizationEndpoint, tokenEndpoint].
   *   clientId and clientSecret are strings representing the account registered
   *   for Thunderbird with the organization.
   *   authorizationEndpoint and tokenEndpoint are url strings representing
   *   endpoints to access OAuth2 authentication.
   */
  getIssuerDetails(issuer) {
    return kIssuers.get(issuer);
  },

  /**
   * Add a provider at run-time. This will typically only be called by the
   * extension API.
   *
   * @param {string} issuer - To identify this provider in the login manager.
   * @param {string} clientId - Identifies the OAuth client to the server.
   * @param {string} clientSecret - Identifies the OAuth client to the server.
   * @param {string} authorizationEndpoint - OAuth authorization endpoint address.
   * @param {string} tokenEndpoint - OAuth token endpoint address.
   * @param {string} redirectionEndpoint - OAuth redirection endpoint.
   * @param {boolean} usePKCE - If the authorization uses PKCE.
   * @param {string[]} hostnames - One or more hostnames which use this OAuth provider.
   * @param {string} scopes - The scopes to request when using this OAuth provider.
   */
  registerProvider(
    issuer,
    clientId,
    clientSecret,
    authorizationEndpoint,
    tokenEndpoint,
    redirectionEndpoint,
    usePKCE,
    hostnames,
    scopes
  ) {
    if (kIssuers.has(issuer)) {
      throw new Error(`Issuer ${issuer} already registered.`);
    }
    for (const hostname of hostnames) {
      if (kHostnames.has(hostname)) {
        throw new Error(`Hostname ${hostname} already registered.`);
      }
    }
    kIssuers.set(issuer, {
      name: issuer,
      builtIn: false,
      clientId,
      clientSecret,
      authorizationEndpoint,
      tokenEndpoint,
      redirectionEndpoint,
      usePKCE,
    });
    for (const hostname of hostnames) {
      kHostnames.set(hostname, [issuer, scopes]);
    }
  },

  /**
   * Remove a runtime-added provider. Built-in providers cannot be removed.
   *
   * @param {string} issuer - The same string used for `registerProvider`.
   */
  unregisterProvider(issuer) {
    if (!kIssuers.has(issuer)) {
      throw new Error(`Issuer ${issuer} was not registered.`);
    }
    if (kIssuers.get(issuer).builtIn) {
      throw new Error(`Refusing to unregister built-in provider ${issuer}.`);
    }
    kIssuers.delete(issuer);
    for (const [hostname, details] of kHostnames) {
      if (details[0] == issuer) {
        kHostnames.delete(hostname);
      }
    }
  },
};

/**
 * Turns zero or more space-delimited strings of scopes into a single string,
 * avoiding duplicates.
 *
 * @param {string[]} scopeStrings
 * @returns {string}
 */
function combineScopes(scopeStrings) {
  const scopes = new Set();
  for (const scopeString of scopeStrings) {
    if (!scopeString) {
      continue;
    }
    for (const scope of scopeString.split(" ")) {
      scopes.add(scope);
    }
  }
  return [...scopes].join(" ");
}