Skip to content

Instantly share code, notes, and snippets.

@nickbusy
Last active May 25, 2022 07:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nickbusy/5ff0e008b9c9e4fea23e1598cab41bec to your computer and use it in GitHub Desktop.
Save nickbusy/5ff0e008b9c9e4fea23e1598cab41bec to your computer and use it in GitHub Desktop.
Google OAuth for a Meteor web app accessible using multiple domain names (branding)

Multi-domain Meteor app instance and Google OAuth

Overview

Changes to Meteor packages (OAuth & Google) to support Meteor web app deployed under multiple domains (for branding purpose, for example).

Only changes to Google and the core OAuth packages are included here. Other OAuth packages, such as Facebook can be amended in a similar way.

The solution does not include sharing the login state across the domains. If that's what you need, there's another suggested solution for that on the net.

Note: The files attached don't include complete source code from the packages but only changed functions.

Scenario

The web application is deployed with multiple CNAME records pointing to the same instance - main.example.com, brand2.example.com. The application is starting with ROOT_URL=https://main.example.com. Both domain names are registered as authorised origins and redirect URIs in Google API manager.

Assumption

Custom client code for Meteor.startup overrides the Meteor.absoluteUrl.defaultOptions.rootUrl as required (example attached).

Problem

When the server side prepaires HTTP POST request to exchange authorization code to an access token, the value of redirect_uri parameter depends on the current ROOT_URL (same for connections via different domain names). This results in mismatch of the redirect_uri during the the authorization code request and the token request.

Solution

Pass the rootUrl used with the authorization code request from the client to server side inside the state parameter and use it to override rootUrl while building a redirect_uri for the access token request.

// nbusy: this is an example only
if(Meteor.isClient){
Meteor.startup(function(){
var host = window.location.host;
if (["main.example.com", "brand2.example.com"].includes(host))
Meteor.absoluteUrl.defaultOptions.rootUrl = "https://" + host;
});
}
// package: google-oauth | google
// nbusy: only code with changes is included
// ---
// returns an object containing:
// - accessToken
// - expiresIn: lifetime of token in seconds
// - refreshToken, if this is the first authorization request
var getTokens = function (query) {
var config = ServiceConfiguration.configurations.findOne({service: 'google'});
if (!config)
throw new ServiceConfiguration.ConfigError();
var response;
try {
// nbusy
var absoluteUrlOptions;
var rootUrl = OAuth._stateFromQuery(query).rootUrl;
if (rootUrl) {
absoluteUrlOptions = {rootUrl: rootUrl};
}
response = HTTP.post(
"https://accounts.google.com/o/oauth2/token", {params: {
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('google', config, undefined, absoluteUrlOptions), // nbusy
grant_type: 'authorization_code'
}});
} catch (err) {
throw _.extend(new Error("Failed to complete OAuth handshake with Google. " + err.message),
{response: err.response});
}
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error("Failed to complete OAuth handshake with Google. " + response.data.error);
} else {
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresIn: response.data.expires_in,
idToken: response.data.id_token
};
}
};
// package: oauth
// nbusy: only code with changes is included
// ---
OAuth._stateParam = function (loginStyle, credentialToken, redirectUrl) {
var state = {
loginStyle: loginStyle,
credentialToken: credentialToken,
rootUrl: Meteor.absoluteUrl.defaultOptions.rootUrl, // nbusy
isCordova: Meteor.isCordova
};
if (loginStyle === 'redirect')
state.redirectUrl = redirectUrl || ('' + window.location);
// Encode base64 as not all login services URI-encode the state
// parameter when they pass it back to us.
// Use the 'base64' package here because 'btoa' isn't supported in IE8/9.
return Base64.encode(JSON.stringify(state)); // 54
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment