Skip to content

Instantly share code, notes, and snippets.

@kewisch
Created August 31, 2020 11:41
Show Gist options
  • Save kewisch/be78a55577e58c6d88b5b39ea598365c to your computer and use it in GitHub Desktop.
Save kewisch/be78a55577e58c6d88b5b39ea598365c to your computer and use it in GitHub Desktop.
Proof of Concept: Google OAuth2 without the browser.identity API
/* 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/.
* Portions Copyright (C) Philipp Kewisch, 2020 */
class OAuth2 {
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
APPROVAL_URL = "https://accounts.google.com/o/oauth2/approval/v2"
TOKEN_URL = "https://oauth2.googleapis.com/token"
LOGOUT_URL = "https://oauth2.googleapis.com/revoke"
EXPIRE_GRACE_SECONDS = 60
WINDOW_WIDTH = 430
WINDOW_HEIGHT = 750
constructor({ clientId, clientSecret, scope }) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scope = scope;
}
get expired() {
return Date.now() > this.expires;
}
async _approvalUrlViaTabs(wnd) {
return new Promise((resolve, reject) => {
let tabListener = (tabId, changeInfo) => {
if (changeInfo.url) {
browser.tabs.onUpdated.removeListener(tabListener);
browser.windows.onRemoved.removeListener(windowListener);
resolve(new URL(changeInfo.url));
}
};
let windowListener = (windowId) => {
if (windowId == wnd.id) {
browser.tabs.onUpdated.removeListener(tabListener);
browser.windows.onRemoved.removeListener(windowListener);
reject({ error: "canceled" });
}
};
browser.windows.onRemoved.addListener(windowListener);
browser.tabs.onUpdated.addListener(tabListener, {
urls: [this.APPROVAL_URL + "*"],
windowId: wnd.id
});
});
}
async _approvalUrlViaWebRequest(wnd) {
return new Promise((resolve, reject) => {
let listener = (details) => {
browser.webRequest.onBeforeRequest.removeListener(listener);
browser.windows.onRemoved.removeListener(windowListener);
resolve(new URL(details.url));
};
let windowListener = (windowId) => {
if (windowId == wnd.id) {
browser.webRequest.onBeforeRequest.removeListener(listener);
browser.windows.onRemoved.removeListener(windowListener);
reject({ error: "canceled" });
}
};
browser.windows.onRemoved.addListener(windowListener);
browser.webRequest.onBeforeRequest.addListener(listener, {
urls: [this.APPROVAL_URL + "*"],
windowId: wnd.id
});
});
}
async login({ titlePreface="", loginHint="" }) {
// Create a window initiating the OAuth2 login process
let params = new URLSearchParams({
client_id: this.clientId,
scope: this.scope,
response_type: "code",
redirect_uri: "urn:ietf:wg:oauth:2.0:oob:auto",
login_hint: loginHint,
hl: browser.i18n.getUILanguage(), // eslint-disable-line id-length
});
let wnd = await browser.windows.create({
titlePreface: titlePreface,
type: "popup",
url: this.AUTH_URL + "?" + params,
width: this.WINDOW_WIDTH,
height: this.WINDOW_HEIGHT,
});
// Wait for the approval request to settle. There are two ways to do this: via the tabs
// permission, or via webRequest. Use the method that uses the least amount of extra
// permissions.
let approvalUrl = await this._approvalUrlViaTabs(wnd);
await browser.windows.remove(wnd.id);
// Turn the approval code into the refresh and access tokens
params = new URLSearchParams(approvalUrl.search.substr(1));
if (params.get("response").startsWith("error=")) {
throw { error: params.get("response").substr(6) };
}
let response = await fetch(this.TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", },
body: new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
code: params.get("approvalCode"),
grant_type: "authorization_code",
redirect_uri: "urn:ietf:wg:oauth:2.0:oob:auto"
})
});
if (!response.ok) {
throw { error: "request_error" };
}
let details = await response.json();
this.accessToken = details.access_token;
this.refreshToken = details.refresh_token;
this.grantedScopes = details.scope;
this.expires = new Date(Date.now() + 1000 * (details.expires_in - this.EXPIRE_GRACE_SECONDS));
}
async refresh(force=false) {
if (!force && !this.expired) {
return;
}
let response = await fetch(this.TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", },
body: new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: "refresh_token",
refresh_token: this.refreshToken
})
});
if (!response.ok) {
throw { error: "request_error" };
}
let details = await response.json();
this.accessToken = details.access_token;
this.expires = new Date(Date.now() + 1000 * (details.expires_in - this.EXPIRE_GRACE_SECONDS));
this.grantedScopes = details.scope;
}
async logout() {
let token = this.expired ? this.refreshToken : this.accessToken;
let response = await fetch(this.LOGOUT_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", },
body: new URLSearchParams({ token })
});
if (!response.ok) {
throw { error: "request_error" };
}
this.accessToken = null;
this.refreshToken = null;
this.grantedScopes = null;
this.expires = null;
}
}
browser.browserAction.onClicked.addListener(async () => {
try {
let oauth = new OAuth2({
clientId: "YOUR_CLIENT_ID_HERE",
clientSecret: "YOUR_CLIENT_SECRET_HERE",
scope: "SPACE_SEPARATED_SCOPES_HERE",
});
await oauth.login({
titlePreface: "Google OAuth Login: ",
loginHint: "ACCOUNT_EMAIL_HERE",
});
console.log("AUTHINFO", oauth.accessToken, oauth.refreshToken);
await oauth.refresh(true);
console.log("AUTHINFO", oauth.accessToken, oauth.refreshToken);
await oauth.logout();
} catch (e) {
console.error(e);
}
});
// 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/.
// Portions Copyright (C) Philipp Kewisch, 2020
{
"manifest_version": 2,
"name": "Google OAuth2 PoC",
"description": "Proof of concept for OAuth2 without the browser.identity API",
"version": "1.0.0",
"browser_specific_settings": {
"gecko": {
"id": "google-oauth-poc@mozilla.kewis.ch"
}
},
"icons": {
"128": "images/addon.svg"
},
"permissions": [
// There are two methods to wait for the OAuth process to complete. Via webRequest:
// "webRequest",
// "https://accounts.google.com/o/oauth2/approval/v2",
// ...or via tabs.onUpdated. Pick whatever permission you already use.
"tabs"
],
"browser_action": {
"default_icon": "images/addon.svg",
"default_title": "Start Google OAuth"
},
"background": {
"scripts": [
"background.js"
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment