12

I have a Chrome extension that requests a user to login using the chrome.identity.getAuthToken route. This works fine, but when you login you can only use the users that you have accounts in Chrome for.

The client would like to be able to login with a different Google account; so rather than using the.client@gmail.com, which is the account Chrome is signed in to, they want to be able to login using the.client@company.com, which is also a valid Google account.

It is possible for me to be logged in to Chrome with one account, and Gmail with a second account, and I do not get the option to choose in the extension.

Is this possible?

boodle
  • 506
  • 4
  • 12

2 Answers2

16

Instead of authenticating the user using the chrome.identity.getAuthToken , just implement the OAuth part yourself.

You can use libraries to help you, but the last time I tried the most helpful library (the Google API Client) will not work on a Chrome extension.

Check out the Google OpenID Connect documentation for more info. In the end all you have to do is redirect the user to the OAuth URL, use your extension to get Google's answer (the authorization code) and then convert the authorization code to an access token (it's a simple POST call).

Since for a Chrome extension you cannot redirect to a web server, you can use the installed app redirect URI : urn:ietf:wg:oauth:2.0:oob. With this Google will display a page containing the authorization code.

Just use your extension to inject some javascript code in this page to get the authorization code, close the HTML page, perform the POST call to obtain the user's email.

David
  • 5,481
  • 2
  • 20
  • 33
  • Great answer. This is very close to how we ended up doing it. I wish we had known about the installed app redirect URI though. The route we took was to direct to the OAuth approval page, which directed to a page on one of our servers informing the user of either the success, or reason for the failure. A bit circuitous, but it works and the client is happy. – boodle Mar 20 '15 at 07:51
  • 1
    This technique works if you just need the email/name of the user. If you need to connect with the licensing api of chrome webstore, then you must create a "chrome app" in developer console which will work only with chrome.identity.getAuthToken – Silver Moon Feb 19 '16 at 13:29
  • 3
    Can you explain this part: "Just use your extension to inject some javascript code in this page to get the authorization code, close the HTML page, perform the POST call to obtain the user's email."? I'm not quite sure how to hook into the authorization page after the user completes the flow. – Cory May 30 '17 at 07:05
1

Based on David's answer, I found out that chrome.identity (as well as generic browser.identity) API now provides a chrome.identity.launchWebAuthFlow method which can be used to launch an OAuth workflow. Following is a sample class showing how to use it:

class OAuth {

    constructor(clientId) {
        this.tokens = [];
        this.redirectUrl = chrome.identity.getRedirectURL();
        this.clientId = clientId;
        this.scopes = [
            "https://www.googleapis.com/auth/gmail.modify",
            "https://www.googleapis.com/auth/gmail.compose",
            "https://www.googleapis.com/auth/gmail.send"
        ];
        this.validationBaseUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo";
    }

    generateAuthUrl(email) {
        const params = {
            client_id: this.clientId,
            response_type: 'token',
            redirect_uri: encodeURIComponent(this.redirectUrl),
            scope: encodeURIComponent(this.scopes.join(' ')),
            login_hint: email
        };

        let url = 'https://accounts.google.com/o/oauth2/auth?';
        for (const p in params) {
            url += `${p}=${params[p]}&`;
        }
        return url;
    }


    extractAccessToken(redirectUri) {
        let m = redirectUri.match(/[#?](.*)/);
        if (!m || m.length < 1)
            return null;
        let params = new URLSearchParams(m[1].split("#")[0]);
        return params.get("access_token");
    }

    /**
    Validate the token contained in redirectURL.
    This follows essentially the process here:
    https://developers.google.com/identity/protocols/OAuth2UserAgent#tokeninfo-validation
    - make a GET request to the validation URL, including the access token
    - if the response is 200, and contains an "aud" property, and that property
    matches the clientID, then the response is valid
    - otherwise it is not valid

    Note that the Google page talks about an "audience" property, but in fact
    it seems to be "aud".
    */
    validate(redirectURL) {
        const accessToken = this.extractAccessToken(redirectURL);
        if (!accessToken) {
            throw "Authorization failure";
        }
        const validationURL = `${this.validationBaseUrl}?access_token=${accessToken}`;
        const validationRequest = new Request(validationURL, {
            method: "GET"
        });

        function checkResponse(response) {
            return new Promise((resolve, reject) => {
                if (response.status != 200) {
                    reject("Token validation error");
                }
                response.json().then((json) => {
                    if (json.aud && (json.aud === this.clientId)) {
                        resolve(accessToken);
                    } else {
                        reject("Token validation error");
                    }
                });
            });
        }

        return fetch(validationRequest).then(checkResponse.bind(this));
    }

    /**
    Authenticate and authorize using browser.identity.launchWebAuthFlow().
    If successful, this resolves with a redirectURL string that contains
    an access token.
    */
    authorize(email) {
        const that = this;
        return new Promise((resolve, reject) => {
            chrome.identity.launchWebAuthFlow({
                interactive: true,
                url: that.generateAuthUrl(email)
            }, function(responseUrl) {
                resolve(responseUrl);
            });
        });
    }

    getAccessToken(email) {
        if (!this.tokens[email]) {
            const token = await this.authorize(email).then(this.validate.bind(this));
            this.tokens[email] = token;
        }
        return this.tokens[email];
    }
}

DISCLAIMER: above class is based on open-source sample code from Mozilla Developer Network.

Usage:

const clientId = "YOUR-CLIENT-ID"; // follow link below to see how to get client id
const oauth = new OAuth();
const token = await oauth.getAccessToken("sample@gmail.com");

Of course, you need to handle the expiration of tokens yourself i.e. when you get 401 from Google's API, remove token and try to authorize again.

A complete sample extension using Google's OAuth can be found here.

Cashif Ilyas
  • 1,619
  • 2
  • 14
  • 22