Skip to content
This repository was archived by the owner on Mar 22, 2022. It is now read-only.

A supposed way to auth requests from SSR to Feathers API #469

Closed
NikitaVlaznev opened this issue Apr 1, 2017 · 25 comments
Closed

A supposed way to auth requests from SSR to Feathers API #469

NikitaVlaznev opened this issue Apr 1, 2017 · 25 comments

Comments

@NikitaVlaznev
Copy link

NikitaVlaznev commented Apr 1, 2017

You have a good docs and examples but it lacks any info on how it is supposed to authorize requests from SSR (Server Side Renderer) to Feathers API.
Is it ok to instantiate feathers-client app for every request? Would not it be to heavy?

There is an example of how to call feathers API from server side:

// Set up a socket connection to our remote API
const socket = io('http://api.feathersjs.com');
const api = client().configure(socketio(socket));
​
app.get('/messages', function(req, res, next){
  api.service('messages')
    .find({ query: {$sort: { updatedAt: -1 } } })
    .then(result => res.render('message-list', result.data))
    .catch(next);
});

But what if the messages service will require authenticated user?
Should i just manually get token from SSR's req and add it somehow to api instance or api.service call?

Taking in mind the asynchronous nature of node it seems that durable way here is to call client() inside the app.get '/messages' handler, is it a supposed way?

It is also unclear does one of the main Feathers boilerplate examples have durable SSR authentication, i've described it here.

@NikitaVlaznev NikitaVlaznev changed the title How it is supposed to auth requests from SSR to feathers API? A supposed way to auth requests from SSR to feathers API Apr 1, 2017
@NikitaVlaznev NikitaVlaznev changed the title A supposed way to auth requests from SSR to feathers API A supposed way to auth requests from SSR to Feathers API Apr 1, 2017
@marshallswain
Copy link
Member

What Framework are you using? This is something worth gathering a collection of examples and demos. You'll for sure need to enable cookies. And make sure you're using the pre release of the generator. The new auth plugins help with implementation of SSR quite a bit thanks to the cookie support. I'm on my phone or I'd help more right now.

@NikitaVlaznev
Copy link
Author

NikitaVlaznev commented Apr 2, 2017

My app consists of a browser client, SSR and API which run in separate processes.
In SSR i'm using express as a main app.
In browser and SSR i'm using the same isomorphic APIClient based on the feathers-client to access API.
The API server is using feathers as main app.

So the requests can go in two ways:

  1. Browser -> SSR -> API
  2. Browser -> API

There is a good examples of how it is expected to do auth when client talks to API or SSR and API are combined into a single process (like the chat example), but separating these servers is a better practive for production and there is no official examples or info in the docs about it, as i can see.

@marshallswain
Copy link
Member

The setup is much simpler when the API and SSR server are on the same machine, but I agree that the ideal setup is to keep them separate. The main problem to solve is the cookie. With feathers-authentication@1.x.x, we've added support for setting the token in a cookie upon successful login.

One problem to consider is the disparity between the transports. The feathers-socketio plugin is, of course, using socket.io, which has to be statefully authenticated on the server. In order to receive authenticated data, you'd have to wait for the socket to connect, then you'd have to make a request to app.authenticate on the client before you can make any other requests. If you can detect the SSR environment, I'd recommend switching to use feathers-rest in that environment. Then you don't have to do the app.authenticate call, since it doesn't have to be statefully authenticated. It can just send the Authorization header (which it's already programmed to do) with the request and retrieve authenticated data.

Now back to the cookie. When SSR and API are on the same server, the cookie for the API server will automatically work for the SSR server. This is because browsers send cookies automatically based on the domain. So when you authenticate with the API server, the browser gets the API server's cookie, and all requests to that domain, including SSR requests, will receive the cookie. This makes the cookie available on the SSR server, which probably allows the feathers-rest and feathers-authentication-client plugins to use it automatically. (You do have to enable the cookie option in the feathers-authentication-client. See the docs)

Putting the SSR server on a different domain, however, gets rid of the ability to automatically use the cookie, because the browser won't send a cookie for a different domain. In order to make this scenario work, the SSR server has to serve as an auth proxy. Here's the basic workflow:

  • Authenticate like normal with the API server, receive the JWT
  • On the client, manually create a cookie that contains the JWT for the SSR server's domain.
  • On the SSR server, check for a JWT in this same cookie location. If it exists, create a cookie containing the JWT for the API server. This must be done before loading the feathers-authentication-client plugin to work the smoothest.
  • With the cookie in place, the feathers-authentication-client should detect the cookie, on load, and automatically setup feathers-rest to include it with every request. This will allow you to retrieve authenticated data.
  • Make sure CORS is turned off for your SSR server, since it's now using cookies and is susceptible to CSRF attacks. This shouldn't be a concern for the API server, because we've intentionally kept Feathers from reading any cookies.

Hopefully this gives you what you need to get going.

@NikitaVlaznev
Copy link
Author

NikitaVlaznev commented Apr 2, 2017

Here is how i got it working.

On every request SSR create an API adaptor before routing:

app.use('/', (req, res, next) => {
    req.api = APIClient(req);
    next();
});

APIClient constructor gets token from cookie an sets it using the set('accessToken', token) method, provided by feathers-authentication-client plugin, this method is not mentioned in docs (at least i do not see it), but it's working:

'use strict';

const feathers = require('feathers');
const superagent = require('superagent');
const hooks = require('feathers-hooks')
const feathers_rest = require('feathers-rest/client');
const auth_plugin = require('feathers-authentication-client');

const config = require('../config');

const host = clientUrl => (
    __SERVER__ ? `http://${config.apiHost}:${config.apiPort}` : clientUrl
);

/*  API adaptor constructor.
*/
module.exports = function APIClient(req) {
    const api = feathers()
        // REST plugin gives ability to query services over HTTP,
        // superagent used as an isomorphic HTTPClient.
        .configure(feathers_rest(host('/api')).superagent(superagent))
        .configure(hooks())
        // Auth plugin gives ability to set accessToken
        .configure(auth_plugin())
    ;

    if (__SERVER__) {
        api.set('accessToken', req['cookies']['feathers-jwt']);
    }
    return api;
}

So, here is a page loading flow i've got:

  1. When i type 'my-app.com' in a browser, it sends a GET request to my SSR, passing an access token in feathers-jwt cookie.
  2. SSR creates a feathers client (i hope it is not to heavy for every request), fetches access token from the cookie and gives it to the client by api.set('accessToken', token) method.
  3. SSR gets data from API using this client and gives it to the template engine (pug/react etc).
  4. SSR returns rendered page to the browser.

This APIClient should work in the browser, but it is the next step to debug it.
Also i need to set token in browser when making requests to API, because if it is on another domain there will be no cookie, and it is better to use Authorization header or token parameter when accessing API, i'll add this code later.

Is it is a supposed way to do it? If so, it would be great to have such example or docs on setting accessToken, i can provide my if you consider it as a good idea.

@marshallswain
Copy link
Member

marshallswain commented Apr 2, 2017

Perfect. That's exactly equivalent to what I described, but even works with the 0.7 version of feathers-authentication, I imagine. You've pointed out a very important oversight on my part with the docs, though. It's not pointed out well in the API docs that the application docs apply to both the server and the client, minus the express part. Oh, and the client is very lightweight, so this will not be a concern. It's basically a combination of the application code with the mocked out express object, which are both tiny. The size of any Ajax library would probably be larger. So no concern, in general.

I think this would be valuable information to share with the community. Would you like to write a blog post? We could link to it in the documentation. Or, if you prefer, you can put together a nice example and I could write an article about it. I think we need two articles, really. One for SSR on the same machine/domain, and another for SSR on separate domains.

@NikitaVlaznev
Copy link
Author

NikitaVlaznev commented Apr 3, 2017

Ok, thank you! I think i provide it as a one more guide section or a "Built with Feathers" example when i'll debug it.
I think there should be a mention of the set('accessToken', token) method in the feathers-authentication-client plugin docs, in Authenticating With Feathers Client and in Universal feathers.

Authenticating With Feathers Client section shows how to obtain and use token on the client and how to build a server that create tokens. It would be great if there were an example of how to obtain and use token when the client is another node js server (SSR).

Universal feathers section says that feathers-authentication-client supports "Token authentication (JWT)", but there is absolutely no info on how to do it (i mean how to make authorized requests from node js if you have a token).

And it would be very helpful to see an example of access to a service that requires authentication in Universal Feathers for Node JS example, it will save few evenings to everyone who starts using the framework for production apps.

Hope my feedback will be useful)

@marshallswain
Copy link
Member

marshallswain commented Apr 3, 2017

I created this issue, yesterday, to plan out the guides we need. I'll be working most of those topics into these new guides.

The set method is already in the Application API documentation, but you're right, the auth client uses it in a special way that should be mentioned. If you're using feathers-authentication-client, you shouldn't have to manually call app.set() for the accessToken, though. It does it automatically when the plugin is loaded, here: https://github.com/feathersjs/feathers-authentication-client/blob/master/src/passport.js#L26 All you have to do is make sure cookie support and storage are both enabled. The rest will happen automatically. https://docs.feathersjs.com/v/auk/api/authentication/client.html#default-options

@marshallswain
Copy link
Member

@NikitaVlaznev, if you feel like you've figured this out, please feel free to close this. I've got work started for documenting this, already.

@bertho-zero
Copy link
Contributor

@marshallswain The jwtFromRequest method of feathers-authentication-jwt (index.js#L50) should be exposed to make it easier to export the token, no?

@marshallswain
Copy link
Member

@bertho-zero can you explain what you mean by "export the token"?

@bertho-zero
Copy link
Contributor

bertho-zero commented Apr 3, 2017

*Extract from request, sorry..

To then easily perform: set('accessToken', token)

@marshallswain
Copy link
Member

@bertho-zero Want to create a separate issue for that?

@marshallswain
Copy link
Member

@bertho-zero what's the goal? You want a utility to extract the JWT from a cookie?

@bertho-zero
Copy link
Contributor

In my app.js i have:

export function createApp(req) {
  if (req === 'rest') {
    return configureApp(rest(host('/api')).superagent(superagent));
  }

  if (__SERVER__ && req) {
    const app = configureApp(rest(host('/api')).superagent(superagent, {
      headers: {
        Cookie: req.get('cookie'),
        authorization: req.header('authorization')
      }
    }));

    const accessToken = req.header('authorization') || (req.cookies && req.cookies['feathers-jwt']);
    app.set('accessToken', accessToken);

    return app;
  }

  return configureApp(socketio(socket));
}

In my client.js i have:

import { createApp } from 'app';

const app = createApp();
const restApp = createApp('rest');

// ...

const renderRouter = props => <ReduxAsyncConnect
  {...props}
  helpers={{ client, app, restApp }}
  filter={item => !item.deferred}
  render={applyRouterMiddleware(useScroll())}
/>;

const render = routes => {
  match({ history, routes }, (error, redirectLocation, renderProps) => {
    ReactDOM.render(
      <HotEnabler>
        <Provider store={store} app={app} restApp={restApp} key="provider">
          <Router {...renderProps} render={renderRouter} history={history}>
            {routes}
          </Router>
        </Provider>
      </HotEnabler>,
      dest
    );
  });
};

render(getRoutes(store));

// ...

And in my server.js i have:

import { createApp } from 'app';

// ...

app.use((req, res) => {
  const client = new ApiClient(req);
  const clientApp = createApp(req);
  const restApp = clientApp;
  const memoryHistory = createHistory(req.originalUrl);
  const store = createStore(memoryHistory, { client, app: clientApp, restApp });
  const history = syncHistoryWithStore(memoryHistory, store);

  function hydrateOnClient() {
    res.send(`<!doctype html>
      ${ReactDOM.renderToString(<Html assets={webpackIsomorphicTools.assets()} store={store} />)}`);
  }

  if (__DISABLE_SSR__) {
    return hydrateOnClient();
  }

  match({
    history,
    routes: getRoutes(store),
    location: req.originalUrl
  }, (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(redirectLocation.pathname + redirectLocation.search);
    } else if (error) {
      console.error('ROUTER ERROR:', pretty.render(error));
      res.status(500);
      hydrateOnClient();
    } else if (renderProps) {
      loadOnServer({ ...renderProps, store, helpers: { client, app: clientApp, restApp } }).then(() => {
        const component = (
          <Provider store={store} app={app} restApp={restApp} key="provider">
            <ReduxAsyncConnect {...renderProps} />
          </Provider>
        );

        res.status(200);

        global.navigator = { userAgent: req.headers['user-agent'] };

        res.send(`<!doctype html>
        ${ReactDOM.renderToString(
          <Html assets={webpackIsomorphicTools.assets()} component={component} store={store} />
        )}`);
      }).catch(mountError => {
        console.error('MOUNT ERROR:', pretty.render(mountError));
        res.status(500);
        hydrateOnClient();
      });
    } else {
      res.status(404).send('Not found');
    }
  });
});

// ...

The main problem is in the file app.js, I would like to be able to replace the following lines with the jwtFromRequest method used by feathers-authentication-jwt:

const accessToken = req.header('authorization') || (req.cookies && req.cookies['feathers-jwt']); // <- this line
app.set('accessToken', accessToken);

@snewell92
Copy link

@marshallswain You mentioned

All you have to do is make sure cookie support and storage are both enabled.

Is cookie support enabled by just having the configuration set up properly, and then using the feathers client rest? So these two things:

config

{
  ...props...
  "authentication": {
    ... etc. etc. ...
    "cookie": {
      "enabled": true,
      "domain": "localhost",
      "name": "feathers-jwt",
      "httpOnly": true,
      "secure": false // override in production
    }
  }
}

client

const feathersClient = feathers()
  .configure(feathers.rest(url).fetch(fetch))
  .configure(feathers.hooks())
  .configure(feathers.authentication({ storage: window.localStorage }));

Now, If I did not have the client part, but a login form instead, that would not work, right? Because the magic that sets the cookie is in the feather client rest configuration as it authenticates to the server, then the server can set the cookie as it authenticates, or the client can set it (or I can manually, since I have the cookie).
Eg:

<form action="/#" method="POST">
  <div class="item">
    <input type="text" name="username" placeholder="Username"/>
  </div>
  <div class="item">
    <input type="password" name="password" placeholder="Password"/>
  </div>
  <div class="item">
    <input type="submit" value="Login" class="button"/>
  </div>
</form>

I just tried the form and I have the express part to extract the token from the cookie and put it in the header, but that never gets fired when the login route is hit. So when the login success redirect happens, I get 401 not authorized.

Just making sure I understand this. Thanks for reading!

@marshallswain
Copy link
Member

@snewell92 Can you please list out the full workflow of what you're trying to do? I don't quite grasp it.

@snewell92
Copy link

snewell92 commented May 16, 2017

This repo is a good place to see where I'm going. I'll explain.

So I'm doing more or less a normal web application / website. Users visit the landing page, see a login page, login, and then are taken to a series of pages that they can do stuff with. Normal normal.

So, for the flow of authentication I have it set up like so: I have my authentication service in its own file authentication.js which gets pulled in as the first service, authentication is configured right after bodyParser (which is after hooks, which is after socket... which is after rest.. - you can view app.js in the repo for all the deets).

After the authentication service is registered I set up the auth create/remove hooks, which is just like the docs. So nothing new here.

After the authentication is set up I set up mysql/users service. All seems well. At this point I tested using feathers client and postman - both work as rest clients and receive back jwt token - yay!

Now all my services are configured, so I move on to make my routes. In the repo this is the file: routes.js. It sets up cookie extraction, validated routes, and the login route (which I don't believe I can use).

That /# I think successfully authenticates the user, however the successRedirect doesn't seem to work as intended, it sends me to the page but the authentication check on that route fails. I conclude that this isn't the way to do authentication.

So now, on my landing page, I include the feathersclient.js file and now I do this:

const url = (new URL(location.toString())).origin;

const feathersClient = feathers()
  .configure(feathers.rest(url).fetch(fetch))
  .configure(feathers.hooks())
  .configure(feathers.authentication({ storage: window.localStorage }));

Once the user has clicked the login button or pressed enter I do this:

const tryAuth = () => {
  let username = unameField.value;
  let password = pwordField.value;

  feathersClient.authenticate({
    strategy: 'local',
    username: username,
    password: password
  }).then(res => {
    // TODO I think feathers should do this part for us :\
    let d = new Date();
    let numOfDays = 1;
    d.setTime(d.getTime() + (1*24*60*60*1000)); // expire in one day
    let expires = "epxires=" + d.toUTCString();
    let cookieName = "feathers-jwt"; // TODO send this from server some how
    document.cookie = cookieName + "=" + res.accessToken + ";" + expires + ";path=/"; 
    //console.log(res);
    window.location.href = "/donuts"; // nav to route
  }).catch(err => {
    console.error("Oopsy!\n" + err);
  });
};

Unfortunately, even manually setting the cookie client-side doesn't seem to work. I'm not sure what to do. I suspect my default.json configuration is somehow wrong...?

To be clear - I am receiving the jwt token in res - and the cookie is set properly. document.cookie returns the token. But the route validation still fails.

Route validation that I'm talking about: (can be seen in context in routes.js)

app.use(
  '/' + r.path,
  auth.express.authenticate('jwt'),
  (req, res) => {
    res.render('main', routeData[r.name]);
  }
);

@snewell92
Copy link

snewell92 commented May 16, 2017

I didn't even have cookie-parser... so I fixed that and now I get the cookie and get the token...

But I still get the same error, even with the cookie being parsed. I don't think the header is being set right with this code...

req.headers.Authorization = "Bearer " + cookieVal;

Or even

res.set('Authorization', "Bearer " + cookieVal);

I'll need to figure that out. But I think the core of this issue is that this whole process could be solved with @NikitaVlaznev 's auth plugin. But maybe this should be embedded into the authentication service?

Why isn't the header being set by the authentication service?

@marshallswain
Copy link
Member

@snewell92 Did you enable the cookie setting for feathers-authentication? It will automatically create a cookie on login. I'm sorry, but I still haven't been able to figure out what you're trying to accomplish. Are you saying that you have login working and now you're trying to SSR a page? I need a high level overview of what you want to happen.

@snewell92
Copy link

snewell92 commented May 17, 2017

SSR => server-side-render?

When users get logged in, the application should remember that they're logged in. (like how traditional websites would use a session cookie or something - but instead of that use the jwt token).

I have the following cookie settings for local development

...
"cookie": {
  "enabled": true,
  "domain": "localhost",
  "name": "feathers-jwt",
  "httpOnly": true,
  "secure": false
}
...

It's in the authentication property in my default.json as seen here.

Sorry, I'm not being clear enough :\

I'm not doing a SPA. I'm doing a multi-page application, first page is login page. Redirects to new pages. I get the token on that first page, but on redirects or subsequent user navigation, feathers doesn't think the user is authenticated.


Hopefully I can explain better this time:

I'm making a full stack web application with feathers on the backend, a 'shell' that is SSR'd (with vue-express), and with angular to compose pieces together on the main page. The only exception being the login page (no SSR, no angular). The shell is just the top main bar, and the side bar - and the main angular entry point that angular bootstraps itself onto.

As far as users are concerned current flow is this: Users hit the login page first. Simple login form with username/password. Upon pressing enter for clicking the login button, user is authenticated, we get back a token, then the page redirects to a route that requires an authenticated user. All subsequent links/routes require authentication.

Problem is that bolded part. I haven't gotten that working yet. From what I gather feathers totally supports this, since it's supposed to set the Authorization header, and if cookies are enabled set the cookie. I don't know what I've done wrong :\


I'm going to start a fresh project and see if I can duplicate this thing. What I am wanting to do is seperate from my Vuejs SSR + angular. I can just set up basic static site with a couple of routes that use default .ejs views. I'll have a landing page that is a login form which, on success, redirects to a route that requires authorization. I'll add a new comment with a more minimal/repeatable example - or report that I fixed it (hopefully!) thanks for reading. <3<3<3

@snewell92
Copy link

@marshallswain is the code @NikitaVlaznev wrote here necessary if I'm doing authentication via an express route? (ie not using the feathers client?)

api.set('accessToken', req['cookies']['feathers-jwt']); in particular - I would probably prefer to do something like this:

req.headers['authorization'] = req.cookies['feathers-jwt'];
next();

And pass it along to auth.express.authenticate('jwt'). I haven't tested that as I've got something working (my server-side application is managing its own session token now via a cookie).

@daffl
Copy link
Member

daffl commented Jan 22, 2018

There is now a recipe showing how to use Feathers authentication with Express middleware (including server side rendering) at https://docs.feathersjs.com/guides/auth/recipe.express-middleware.html

@NikitaVlaznev
Copy link
Author

NikitaVlaznev commented Jan 26, 2018

@daffl, please correct me if i'm wrong. As i can see the application architecture in the recipe you have mentioned differs from the one in this issue starter.
Recipe assumes that there is a Browser and SSR with API services in it, so the request path is Browser -> SSR. The /messages data service is called internally inside SSR without authorization.

const messages = await app.service('messages').find(params);

'chat' and 'messages' service are on the same app instance.

This Issue was started to clarify how to perform authorization when there is a 3 separate processes: Browser, SSR, API.
The request path is Browser -> SSR -> API.
SSR must obtain accessToken from a cookie and send it in the HTTP request to a separate feathers API server, then render page using data from it's answer.

This architecture is required when SSR is not the only one consumer for the API, there is also some mobile applications and integrated partners.

@daffl
Copy link
Member

daffl commented Jan 27, 2018

The more performant way would be to share the app between your SSR application and the API. Then the SSR application can be set up as in the linked article and doesn't have to make expensive internal HTTP request.

@NikitaVlaznev
Copy link
Author

NikitaVlaznev commented Jan 27, 2018

It is not a good idea for a big projects, there are a lot of reasons for separating SSR and API, e.g.:

  1. SSR and API can scale in relation other than 1:1, they will be running on a separate servers to handle high load.
  2. Some API instances can be running in special conditions without rendering at all, web dependencies would be unnecessaries.
  3. Code separation - you can allow your web team, mobile team and backend team to work independently, and if you need to run your backend in partner's environment you do not need to share your web site code.
  4. Different tech - feathers SSR can use API written on different language (java, python, ...).

Definitely there will be one more HTTP request, but using feathers-batch it will be the only one, and in most cases it will not be very expensive, because SSR and API usually have LAN connection.

# for free to subscribe to this conversation on GitHub. Already have an account? #.
Projects
None yet
Development

No branches or pull requests

6 participants