summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/OAuth2Module.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/src/OAuth2Module.jsm')
-rw-r--r--comm/mailnews/base/src/OAuth2Module.jsm203
1 files changed, 203 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/OAuth2Module.jsm b/comm/mailnews/base/src/OAuth2Module.jsm
new file mode 100644
index 0000000000..79826779c4
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2Module.jsm
@@ -0,0 +1,203 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["OAuth2Module"];
+
+var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm");
+var { OAuth2Providers } = ChromeUtils.import(
+ "resource:///modules/OAuth2Providers.jsm"
+);
+
+/**
+ * OAuth2Module is the glue layer that gives XPCOM access to an OAuth2
+ * bearer token it can use to authenticate in SASL steps.
+ * It also takes care of persising the refreshToken for later usage.
+ *
+ * @implements {msgIOAuth2Module}
+ */
+function OAuth2Module() {}
+OAuth2Module.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["msgIOAuth2Module"]),
+
+ initFromSmtp(aServer) {
+ return this._initPrefs(
+ "mail.smtpserver." + aServer.key + ".",
+ aServer.username,
+ aServer.hostname
+ );
+ },
+ initFromMail(aServer) {
+ return this._initPrefs(
+ "mail.server." + aServer.key + ".",
+ aServer.username,
+ aServer.hostName
+ );
+ },
+ initFromABDirectory(aDirectory, aHostname) {
+ this._initPrefs(
+ aDirectory.dirPrefId + ".",
+ aDirectory.getStringValue("carddav.username", "") || aDirectory.UID,
+ aHostname
+ );
+ },
+ _initPrefs(root, aUsername, aHostname) {
+ let issuer = Services.prefs.getStringPref(root + "oauth2.issuer", null);
+ let scope = Services.prefs.getStringPref(root + "oauth2.scope", null);
+
+ let details = OAuth2Providers.getHostnameDetails(aHostname);
+ if (
+ details &&
+ (details[0] != issuer ||
+ !scope?.split(" ").every(s => details[1].split(" ").includes(s)))
+ ) {
+ // Found in the list of hardcoded providers. Use the hardcoded values.
+ // But only if what we had wasn't a narrower scope of current
+ // defaults. Updating scope would cause re-authorization.
+ [issuer, scope] = details;
+ // Store them for the future, can be useful once we support
+ // dynamic registration.
+ Services.prefs.setStringPref(root + "oauth2.issuer", issuer);
+ Services.prefs.setStringPref(root + "oauth2.scope", scope);
+ }
+ if (!issuer || !scope) {
+ // We need these properties for OAuth2 support.
+ return false;
+ }
+
+ // Find the app key we need for the OAuth2 string. Eventually, this should
+ // be using dynamic client registration, but there are no current
+ // implementations that we can test this with.
+ const issuerDetails = OAuth2Providers.getIssuerDetails(issuer);
+ if (!issuerDetails.clientId) {
+ return false;
+ }
+
+ // Username is needed to generate the XOAUTH2 string.
+ this._username = aUsername;
+ // loginOrigin is needed to save the refresh token in the password manager.
+ this._loginOrigin = "oauth://" + issuer;
+ // We use the scope to indicate realm when storing in the password manager.
+ this._scope = scope;
+
+ // Define the OAuth property and store it.
+ this._oauth = new OAuth2(scope, issuerDetails);
+
+ // Try hinting the username...
+ this._oauth.extraAuthParams = [["login_hint", aUsername]];
+
+ // Set the window title to something more useful than "Unnamed"
+ this._oauth.requestWindowTitle = Services.strings
+ .createBundle("chrome://messenger/locale/messenger.properties")
+ .formatStringFromName("oauth2WindowTitle", [aUsername, aHostname]);
+
+ // This stores the refresh token in the login manager.
+ Object.defineProperty(this._oauth, "refreshToken", {
+ get: () => this.refreshToken,
+ set: token => {
+ this.refreshToken = token;
+ },
+ });
+
+ return true;
+ },
+
+ get refreshToken() {
+ for (let login of Services.logins.findLogins(this._loginOrigin, null, "")) {
+ if (
+ login.username == this._username &&
+ (login.httpRealm == this._scope ||
+ login.httpRealm.split(" ").includes(this._scope))
+ ) {
+ return login.password;
+ }
+ }
+ return "";
+ },
+ set refreshToken(token) {
+ // Check if we already have a login with this username, and modify the
+ // password on that, if we do.
+ let logins = Services.logins.findLogins(
+ this._loginOrigin,
+ null,
+ this._scope
+ );
+ for (let login of logins) {
+ if (login.username == this._username) {
+ if (token) {
+ if (token != login.password) {
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+ );
+ propBag.setProperty("password", token);
+ Services.logins.modifyLogin(login, propBag);
+ }
+ } else {
+ Services.logins.removeLogin(login);
+ }
+ return;
+ }
+ }
+
+ // Unless the token is null, we need to create and fill in a new login
+ if (token) {
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(
+ this._loginOrigin,
+ null,
+ this._scope,
+ this._username,
+ token,
+ "",
+ ""
+ );
+ Services.logins.addLogin(login);
+ }
+ },
+
+ connect(aWithUI, aListener) {
+ let oauth = this._oauth;
+ let promptlistener = {
+ onPromptStartAsync(callback) {
+ this.onPromptAuthAvailable(callback);
+ },
+
+ onPromptAuthAvailable: callback => {
+ oauth.connect(
+ () => {
+ aListener.onSuccess(
+ btoa(
+ `user=${this._username}\x01auth=Bearer ${oauth.accessToken}\x01\x01`
+ )
+ );
+ if (callback) {
+ callback.onAuthResult(true);
+ }
+ },
+ () => {
+ aListener.onFailure(Cr.NS_ERROR_ABORT);
+ if (callback) {
+ callback.onAuthResult(false);
+ }
+ },
+ aWithUI,
+ false
+ );
+ },
+ onPromptCanceled() {
+ aListener.onFailure(Cr.NS_ERROR_ABORT);
+ },
+ onPromptStart() {},
+ };
+
+ let asyncprompter = Cc[
+ "@mozilla.org/messenger/msgAsyncPrompter;1"
+ ].getService(Ci.nsIMsgAsyncPrompter);
+ let promptkey = this._loginOrigin + "/" + this._username;
+ asyncprompter.queueAsyncAuthPrompt(promptkey, false, promptlistener);
+ },
+};