3

I currently have the loopbackJS api hosted on a domain (e.g. http://backend.com), with third party authentication setup via Auth0. I have a front-end hosted as a SPA on another domain (e.g. http://frontend.com)

loopback-component-passport seems to work fine when the front-end is on the same domain as the API, and it sets the userId and access_token cookies accordingly. However, my front-end in production is on a different domain to the API, for example the API auth link would be something like:

"http://backend.com/auth/auth0?returnTo=" + encodeURIComponent("http://frontend.com")

The backend has used the same auth pattern as in the loopback-passport-example, where a providers.json file specifies the connection details for Auth0 (although I have also tried other social providers such as Facebook).

  "auth0-login": {
    "provider": "auth0",
    "module": "passport-auth0",
    "clientID": "AUTH0_CLIENT_ID",
    "clientSecret": "AUTH0_CLIENT_SECRET",
    "callbackURL": "/auth/auth0/callback",
    "authPath": "/auth/auth0",
    "callbackPath": "/auth/auth0/callback",
    "successRedirect": "/",
    "failureRedirect": "/login",
    "scope": ["email"],
    "failureFlash": true
  }

The front-end (http://frontend.com) has a link on the page to redirect to the API authentication:

<a href="http://backend.com/auth/auth0">Login</a>

Clicking on this link redirects to Auth0 properly, and I can login. It then redirects to the specified target (http://backend.com or http://frontend.com, whichever is specified). The returnTo query parameter also seems to work as expected.

Is there a way to capture the access_token just before redirecting back to the front-end, and somehow communicate it (e.g. query parameters, unless that would be too insecure).

J3Y
  • 1,703
  • 1
  • 15
  • 27
  • You [cannot pass cookies from one domain to another][http://stackoverflow.com/questions/6761415/how-to-set-a-cookie-for-another-domain]. You will need to find another way to get the access token (which, by the way, I believe is a loopback generated access token, not the oauth temporary token) – Overdrivr May 25 '16 at 09:38
  • Could you give some details regarding external API and the server app distributing client files, so that I can formulate an answer – Overdrivr May 25 '16 at 09:41
  • Thanks, I have modified the question with some more detail around specifics. Would it be best to approach this assuming separation of domains, or would it be better if it's possible, to put front-end and API on the same domain to get around this issue? – J3Y May 25 '16 at 10:19
  • Unless you have a very good reason for using two domains, it is much more simple to use a single one to handle the REST api and distribute front-end files – Overdrivr May 25 '16 at 11:51
  • How about subdomains, do you think that this situation would be alright by keeping the API and front-end on the same domain, but different subdomains? – J3Y May 25 '16 at 21:54
  • See http://stackoverflow.com/questions/18492576/share-cookie-between-subdomain-and-domain – Overdrivr May 26 '16 at 06:35
  • So it should work, although there may be some redirection issues, I don't know. I guess you have to try it out and see if it works – Overdrivr May 26 '16 at 06:36
  • Managed to get around the cross domain issue by placing the access token on a query parameter on the redirect URL. – J3Y May 26 '16 at 07:46
  • Hum, how can you have a token at the moment of the redirection since the token should be generated after redirection ? – Overdrivr May 26 '16 at 07:56
  • I had a look around and there was this [small snippet](https://github.com/strongloop/loopback-component-passport/pull/23) posted years ago, and the discussion has been further extended on this [issue](https://github.com/strongloop/loopback-component-passport/issues/102) and some pull requests (one of which I submitted before I found out about the first link). Modified the snippet slightly to suit my use case but it's working great now. – J3Y May 26 '16 at 09:15
  • Ok I understand now, you are talking about the redirection that happens once passport was called back by the provider. Ok interesting solution, feel free to post it as answer. – Overdrivr May 26 '16 at 09:26

1 Answers1

2

After some more investigation, I settled on this method to use for passing the access token and userId from loopbackjs backend, to a separate front-end. This was documented on a github pull-request, using a customCallback of passport-configurator.

Other places that have referenced this are this fork, issue #102, issue #14 and pull request #155.

There are 2 options here, either use a fork of loopback-component-passport (e.g. the one referenced above) as your npm dependency, or provide a customCallback as a passport configuration option as documented.

I wanted a little more control on the format of the URL, so ended up with the customCallback method. In loopback-example-passport, inside /server/server.js there is some basic code for passing providers.json to the passport configurator:

var config = {};
try {
  config = require('../providers.json');
} catch (err) {
  console.trace(err);
  process.exit(1); // fatal
}
passportConfigurator.init();
for (var s in config) {
  var c = config[s];
  c.session = c.session !== false;
  passportConfigurator.configureProvider(s, c);
}

This can be essentially replaced with the documented customCallback code, with the passport variable being assigned by passportConfigurator.init():

var providers = {};
try {
  providers = require('../providers.json');
} catch (err) {
  console.trace(err);
  process.exit(1); // fatal
}

const passport = passportConfigurator.init();
Object.keys(providers).forEach(function(strategy) {

  var options = providers[strategy];
  options.session = options.session !== false;

  var successRedirect = function(req) {
    if (!!req && req.session && req.session.returnTo) {
      var returnTo = req.session.returnTo;
      delete req.session.returnTo;
      return returnTo;
    }
    return options.successRedirect || '';
  };

  options.customCallback = !options.redirectWithToken 
    ? null 
    : function (req, res, next) {
      var url = require('url');
      passport.authenticate(
        strategy,
        {session: false},
        function(err, user, info) {
          if (err) {
            return next(err);
          }

          if (!user) {
            return res.redirect(options.failureRedirect);
          }
          var redirect = url.parse(successRedirect(req), true);

          delete redirect.search;

          redirect.query = {
            'access_token': info.accessToken.id,
            'userId': user.id.toString()
          };
          redirect = url.format(redirect);
          return res.redirect(redirect);
        }
      )(req, res, next);
  };

  passportConfigurator.configureProvider(strategy, options);
});

In the above example, I have essentially copied the successRedirect function used in passport-configurator.js, to use the same returnTo query parameter. An option within providers.json can be set e.g. "redirectWithToken": true, which results in redirect only for the auth strategies that need external redirect.

One more final bit of code in case the returnTo redirect is required. If it exists as a query parameter, it should be added at a session level:

app.use(function(req, res, next) {
  var returnTo = req.query.returnTo;
  if (returnTo) {
    req.session = req.session || {};
    req.session.returnTo = require('querystring').unescape(returnTo);
  }
  next();
});

Now, if the backend api is at a URL such as http://api.com, and the front-end is hosted at another domain e.g. http://gui.com, an authentication link can be placed on the front-end:

<a href="http://api.com/auth/facebook?returnTo=http%3A%2F%2Fgui.com">Login!</a>

This will result in an API auth call, then redirect back to the returnTo link with the access token and userId in the query parameters.

Potentially in the future, one of the issues or other pull requests will be merged that could provide a more ideal method for 3rd party domain redirection, but until then this method work well.

J3Y
  • 1,703
  • 1
  • 15
  • 27