feathersjs-ecosystem / authentication

[MOVED] Feathers local, token, and OAuth authentication over REST and Websockets using JSON Web Tokens (JWT) with PassportJS.
MIT License
317 stars 118 forks source link

JWT Cookie #389

Closed muvhaus-sl closed 6 years ago

muvhaus-sl commented 7 years ago

First, sorry if I've got things wrong. This is my first project with feathersJS/react native...so I'have been struggling a bit, and trying to make things work.

I have been trying to get OAuth2 (Facebook) and Local authentication to work using the latest feathers-authentication(1.0.2)

I managed to get up to a point where I would see the accessToken being delivered as body of a GET request to the server, in the format as follows: http://{my local dev server}/auth/facebook/callback?code={ a facebook generated code here...}#=

The accessToken, when decoded using JWT does include userID and the other properties. So, after I got this right, I tried to use it on the React Native App. The sample I found (https://github.com/sscaff1/hopePing) used CookieManager, for the simple reason that we cannot get the content of the response (inside the WebView) and we cannot run "fetch" on the same URL. I did not understand how to setup a "successRedirect"...so I went on to enable Cookies. This is how my default.json looks like:

"auth": { 
    "local":{},
    "cookie": {
    "enabled": true,
    "name": "feathers-jwt",
    "httpOnly": false,
    "secure": false, 
        "domain": "my domain"
    },
    "jwt": {
        "header": { "typ": "access" },
        "audience": "my domain", 
        "subject": "anonymous", 
        "issuer": "feathers", 
        "algorithm": "HS256", 
        "expiresIn": "1d"
    },
    "session":false,
    "secret": " there is a secret here...",
    "facebook": {
        "clientID": "my facebook app ID",
        "clientSecret": "my facebook secret",
        "scope": ["public_profile","email"],
        "profileFields": ["id", "displayName", "email", "name", "cover", "picture"] //seems irrelevant
    },
    "google": {
        "clientID": "your google client id",
        "clientSecret": "your google client secret",
        "scope": ["profile"]
    }
}

But then, the cookie would not show up on Chrome, no matter how I changed any of the above Cookie settings.

I set the Debug=feathers-authentication* and then I realized the last log I got for the Set-Cookie.js was debug('Running setCookie middleware with options:', options);

I've put a temporary log after: if (options.enabled && options.name) { and it would show.

I pinpointed that the following condition was not met: if (res.hook && res.hook.method !== 'remove' && res.data && res.data.accessToken) {

res.hook was undefined. If I remove res.hook && res.hook.method!=='remove', it works as expected, but I get 2 cookies for the same name/content.

Sorry for the long text, I hope I have explained everything properly. I wanted to know if I'm doing something wrong.

thanks in advance.

muvhaus-sl commented 7 years ago

On a side note, I wanted to ask if the Authentication 1.0.2 should populate the Users' table with more than the {provider}Id column. Currently I can only see the that column getting populated while the others are null.

I'm using mySQL and Sequelize to back up the Users service.

marshallswain commented 7 years ago

Addressing your side note first:

On a side note, I wanted to ask if the Authentication 1.0.2 should populate the Users' table with more than the {provider}Id column. Currently I can only see the that column getting populated while the others are null.

OAuth plugins will populate a column that matches the provider name. So with GitHub auth, you'd see a github attribute.

marshallswain commented 7 years ago

@muvhaus-sl I'm not experienced with React Native, yet. Last month we created the feathers-authentication-popups module that helps handle the auth workflow for web apps. We've discussed how to use it for React Native, but we have yet to implement it. It's one of the very next things on the list, though. Maybe @ekryski can pitch in when he returns.

daffl commented 7 years ago

The reason that it only adds providerId is that we had problems with databases that require the data format to be pre-defined (e.g. all *SQL dbs). In that case you'll have to map the information you want from the oAuth callback to the fields you defined anyway.

muvhaus-sl commented 7 years ago

@daffl ,

Is there any example that I can refer to?

Sorry if I'm being too demanding. It has been a very steep learning curve to get where I am, hopefully I can gain understanding and contribute back soon enough.

muvhaus-sl commented 7 years ago

@marshallswain
On react native, the CookieManager setup that has been mentioned a few times on other issues works fine. I got Facebook to work fair enough. So far, only Google has blocked the usage of Webviews for OAuth and I don't think its for technical reasons, I think it has been just another corporate move...

Perhaps the viable solution would be for the feathers client to have a widget like Auth0's lock? However, I must say, its a lot of trouble and I think it is not part of the scope of what Feathers is trying to do?

I don't think there is a clean way of doing a client side "widget" if OAuth providers start having different requirements...

jiangts commented 7 years ago

experiencing the same issue as in the original question. In my case, I'm trying to migrate the 0.7.x /auth/local route to using /login, as described by the line in the README.

app.post('/login', auth.express.authenticate('local', { successRedirect: '/app', failureRedirect: '/login' }));

The redirect part works. However, after trying for a couple hours I too couldn't get the authToken to show up in the cookie after redirect. Tried manually setting auth.express.setCookie as a middleware to the post request and passing in app.get('auth') but to no avail. Putting a logger into the actual setCookie middleware revealed that it was never being called during a "normal" login flow where a user posts to /login.

All of that to say, I'm pretty confused

muvhaus-sl commented 7 years ago

@jiangts, Do you have this on your configuration:

"cookie": {
    "enabled": true,
    "name": "feathers-jwt",
    "httpOnly": false,
    "secure": false,  //this is not very safe for production usage, but I dont have https for testing.
        "domain": "my domain" //replace with your actual domain or IP address
    },

Then, the other thing I did to find out the reasons was to set the debug flag for "feathers-authentication*" In doing that, I could spot where the setCookie was stopping.

jiangts commented 7 years ago

Hi @muvhaus-sl,

I followed your suggestion and got the following log:

feathers-authentication-local:verify Checking credentials +22ms <email> <password>
  feathers-authentication-local:verify user found +13ms
  feathers-authentication-local:verify Verifying password +1ms
  feathers-authentication-local:verify Password correct +205ms
  feathers-authentication:passport:authenticate 'local' authentication strategy succeeded +0ms { _id: 586d446040ccc98452435a84,
  email: '<email>',
  password: '<pw hash>',
  __v: 0,
  updatedAt: 2017-01-04T18:52:16.385Z,
  createdAt: 2017-01-04T18:52:16.385Z } { userId: 586d446040ccc98452435a84 }
  feathers-authentication:express:authenticate Redirecting to / +1ms
  feathers-authentication:express:expose-headers Exposing Express headers to hooks and services +4s
  feathers-authentication:express:expose-cookies Exposing Express cookies to hooks and services +0ms undefined

Still not sure where setCookie failed...

Did you get something similar?

muvhaus-sl commented 7 years ago

@jiangts How is your auth config looking like? And, do the Cookie domain and the actual host name match? I would assume that if your cookie domain is set to "something.com" and you are accessing the server as "127.0.0.1" or "localhost", then the browser will not get the cookie?

I did not use Express, so the Expose-cookies is not triggered. On my case it uses Set-cookies.

jiangts commented 7 years ago

@muvhaus-sl

I figured it out. However, I haven't gotten a flow that's as easy/simple as the version in 0.7.x with both setting the cookie and also redirecting the user. In fact, I had to write quite a bit of code on my own instead of just "glue" code over feathers-auth. I'm not sure if this is intentional for such a common use case (just a normal login), or maybe I'm missing something, but in either case, this wasn't anything like pleasant experience I've had so far with feathers :(

For future people, my solution was to create a new service called /login and put auth.express.setCookie(app.get('auth')) as a middleware to that service. This gets the cookie set properly. Then, I wrote some of my own code to do the redirect and clearing the cookie in case of auth failure, as auth.express.successRedirect and auth.express.failureRedirect didn't quite do what I wanted.

daffl commented 7 years ago

So @marshallswain can probably explain this much better than me but the problem of allowing the JWT in a cookie for all API endpoints (and why it has been removed) is that the API will become vulnerable to Cross Site Request Forgery (CSRF), so if someone hijacks a users browser they can make any authorized requests to the API. This is why only specific routes should be explicitly enabled for cookies. Maybe @ekryski can weight in if there is something else that can be done better for your usecase though.

ekryski commented 7 years ago

@muvhaus-sl @jiangts can you post a link to a github repo? Quite frankly, there might be a bug in setting the cookie but it also might be that you just have things set up a different way. @jiangts the intention is for it to work the same as 0.7.x but there are a few more scenarios that we are covering now so we have those extra guards in the set-cookie middleware.

eblanshey commented 7 years ago

I also was having a similar issue. Perhaps I am misunderstanding something. One of the purposes of enabling cookies is to have non-SPAs (old school apps) be able to authenticate without having to set up tokens in the header, correct? Otherwise I don't see the purpose of the cookie being saved in the browser and resent along with every request. And yet you say that due to CSRF cookies are disabled on all routes (if I'm understanding you.) I thought that enabling the cookie option in the config allows cookies on all routes, and the authentication schemes/providers should be able to read it like normal.

When I use the provided middleware, a cookie does NOT get set (both cookies and sessions have been enabled, confirmed via debugging):

app.post('/login', auth.express.authenticate('local', { 'successLogin: '/', failureRedirect: '/login' }));

I have also tried something like the following:

app.post('/login', function(req, res, next) {
  return app.service('authentication').create({
    email: 'test@test.com',
    password: 'asdfasdf'
  }).then(function (result) {
    console.log('Authenticated with result: ', result);
    next();
  })
});

Additionally, I've tried setting up the setCookie middleware manually to no avail:

app.post('/login', auth.express.authenticate('local', { failureRedirect: '/' })); // note, no successRedirect, to get to the cookie middleware
app.post('/login', auth.express.setCookie(app.get('auth')));
app.post('/login', function (req, res) {
  res.send('DONE!');
});

If I do a POST request directly to the authentication service endpoint, I get a token AND a cookie set properly, so I know cookies are being set OK, just not with regular express routes.

Further digging shows that the setCookie middleware gets to this point:

if (res.hook && res.hook.method !== 'remove' && res.data && res.data.accessToken) {```

There is no hook property on the response, and this is where I'm stuck -- I can't figure out where hook gets added to the response. I've been at this the entire day, and I don't know what else to try.

My apologies if this should be obvious, I'm quite new to Express, let alone feathers. But nowhere in the documentation do I see where hook gets set, nor how cookies can be used with regular routes.

Thanks in advance.

EDIT: Just noticed that even with the cookie being set properly if using the standard /authentication service, the following calls to a protected route, e.g. with find: auth.hooks.authenticate(['jwt', 'local']),, authentication fails with NotAuthenticated: No auth token. Logs show that both local and jwt methods failed--they seem to ignore the cookie.

muvhaus-sl commented 7 years ago

@eblanshey My original issue had the same symptom as yours: set-cookie.js would stop at the condition. res.hook With res.hook being undefined.

For a speedy solution, I commented that part of the condition and it looks like everything is working as expected. I'm also not sure why it was undefined to start with.

If I understood it right, on the auth.hooks.authenticate(['jwt', 'local'])

You need to include 'cookie' on that array. I had to include "facebook" if I wanted to allow facebook authentication, for instance. And as far as I understand, cookie becomes a strategy if enabled, so you need to allow it on the list of possible "authenticate" strategies.

Looking forward for a more elegant solution on the set-cookie, as well as some corrections from the more experienced people when it comes to the setup.

eblanshey commented 7 years ago

@muvhaus-sl unfortunately adding "cookie" or "cookies" to the array doesn't work: Unknown authentication strategy 'cookie'. I think the functionality is simply broken.

I did a quick fix by adding a middleware that adds the Authorization header from the value of the cookie (using cookie-parser library):

app.use(function(req, res, next) {
  let cookieValue = req.cookies[app.get('auth').cookie.name];
  if (typeof cookieValue === 'string') {
    req.headers.authorization = "Bearer "+cookieValue;
  }

  next();
});

As for actually setting the cookie, since that only needs to be done once on one route, I'll just make the login an ajax request to /authentication.

To the core devs: I understand feathers is mainly geared towards creating APIs, but full support and documentation for "old school" apps would great, especially since the docs guide you through how to set up view engines and what not.

My use case for using feathers is to quickly create a server-side rendered app, under the hood using the services to fetch data. That way I'd have a fairly easy roadmap to changing it to an SPA down the road when I have more time (just using the service endpoints that the traditional routes use). This could be a great selling point for devs to use Feathers for their next project. As it stands now, authentication using traditional methods is very difficult to understand. I suppose adding built-in CSRF protection would be needed, which could be a matter of using https://github.com/expressjs/csurf and automatically applying it to all non-service routes.

matt-d-rat commented 7 years ago

I believe I have got the same issue as seeing the following debug statement on my feathers server, and my cookies are not being set following a successful authentication:

feathers-authentication:express:expose-cookies Exposing Express cookies to hooks and services +0ms undefined

My auth config has the following set for cookies:

"cookie": {
    "enabled": true,
    "name": "feathers-jwt",
    "httpOnly": false,
    "secure": false
}
marshallswain commented 7 years ago

@eblanshey

One of the purposes of enabling cookies is to have non-SPAs (old school apps) be able to authenticate without having to set up tokens in the header, correct? Otherwise I don't see the purpose of the cookie being saved in the browser and resent along with every request. And yet you say that due to CSRF cookies are disabled on all routes (if I'm understanding you.) I thought that enabling the cookie option in the config allows cookies on all routes, and the authentication schemes/providers should be able to read it like normal.

I'm really short on time, today, so this is going to kind of come out as a stream of consciousness, but I'll do my best.

It's pretty common to conflate enabling cookies with turning on "old-school" auth for routes. With Feathers auth we separate the two. Turning cookies on in the config just creates a cookie. It doesn't automatically enable old school, CSRF-vulnerable routes.

It's important to keep the SSR server and API server as distinct entities. The SSR server can pull the JWT from the cookie, decode it to get the payload. This will include whatever entity data you've populated inside the JWT (like userId). You can then use this data inside the params

import decode from 'jwt-decode';

app.use(function(req, res, next) {
  let jwt = req.cookies[app.get('auth').cookie.name];
  let payload = decode(jwt);
  // Only use service.find and service.get for GET requests
  app.service('todos').get({query: {}, user: payload.userId})
    .then(response => {
       // Use the response data and assemble your HTML.
    });
});

You probably don't want to expose that cookie value to be used by the rest of the API server. At least, I wouldn't want to deal with CSRF at all. Even a good CSRF library is only going to mitigate CSRF attacks, not eliminate them. If you keep the cookie auth data away from the API server, you've eliminated the risk of CSRF completely. If your goal is to enable SSR it's just not necessary to expose cookie auth data to the API server. The example above works more like an auth proxy to get authenticated data from the server in order to form HTML content. (You'd want to make sure CORS is disabled for SSR routes)

We chose to keep the API server distanced from cookie auth because most people aren't aware of the risks or how to mitigate them. And in most cases I've seen where people want to turn cookie support on, there's an alternative method that doesn't introduce security or privacy issues. From my perspective, an API server should probably only expect a cookie for auth as an additional check on top of requiring a header. Otherwise, API servers shouldn't be able to consume auth data from a cookie.

jamesholcomb commented 7 years ago

Running into the same issue as well upgrading a React Native app from feathers-auth 0.7.x to 1.0. The app uses the cookie from an OAuth webview flow purely to obtain and store the JWT. Subsequent calls to services use sockets and bearer token authentication.

Is it possible to have the cookie set just for the /auth/{provider} request and subsequent redirect?

ekryski commented 7 years ago

I'll take a look at this today. There must be a bug somewhere as you should be able to send back a cookie with the JWT in it and parse it your React Native webview and set it back in AsyncStorage yourself. So long as you are making calls after all that is completed it should work.

You should NOT be relying on the JWT inside the cookie to authenticate with your app. (ie. client sends cookie with JWT in it and server verifies it from there). In order to do that you need to enable the CookieExtractor provided by passport-jwt explicitly https://github.com/feathersjs/feathers-authentication-jwt#extractjwt.

A simple reproducable example from someone sure would be helpful... 😁

AsyncPiMaker commented 7 years ago

For myself, a working chat app update would answer many questions. Using the standard chat app from the Feathers book, I attempted to update to auth 1.0.2 and related packages using the migration guide. #https://github.com/feathersjs/feathers-authentication/issues/386

Here is my services/authentication/index.js:

'use strict';

const auth = require('feathers-authentication');
const local = require('feathers-authentication-local');
const jwt = require('feathers-authentication-jwt');

module.exports = function() {
  const app = this;

  let config = app.get('auth');

  app.configure(auth(config))
    .configure(jwt())
    .configure(local())

  app.service('authentication')
    .hooks({
      before: {
        // You can chain multiple strategies on create method
        create: auth.hooks.authenticate(['jwt', 'local'])
      }
    });
};

I get this in the body when I authenticate: {"accessToken":"eyJhbGciOiJIUzUxMiIsInR5cCI6ImFjY2VzcyJ9.eyJ1c2VySWQiOiJOaUtlb3E2cTQ4cU1Sa0paIiwiaWF0IjoxNDg2NTg2NzgzLCJleHAiOjE0ODkxNzg3ODMsImF1ZCI6Imh0dHBzOi8vMTI3LjAuMC4xIiwiaXNzIjoiZmVhdGhlcnMiLCJzaXYBOiJhbm9ueW1vdXMifQ.tTYovuN39PMJxCmK6-I_vX9-LP6vKSa7Gu4ttYAZioD0ifiSOamcmTs4hz3nlPGu5wGBkTTtNSZDuPrOxKNTKQ"}

snewell92 commented 7 years ago

tldr; I can get token, what do I do with it to validate the logged in user on subsequent routes/navigations?

I'm authenticating users with front-facing login form /, then redirecting to a main home page /main.

My set up is a little complicated (MySQL + Feathers + VueJS SSR (really pre-rendering, but I haven't gotten that to work so I'm just using express-vue) + Angular), but that shouldn't matter much.

I can use the feathersClient to authenticate users, but I don't know what to do with the response. I can authenticate, but then if I manually go to a page that is behind an auth check it fails. I assume this is because cookie check fails?

This is my default.json's auth property.

  "auth": {
    "secret": "...bigsecret...",
    "strategies": [
      "local",
      "jwt"
    ],
    "path": "/authentication",
    "service": "users",
    "jwt": {
      "header": {
        "type": "access"
      },
      "audience": "localhost", // TODO probably need to programatically get this...?
      "subject": "anonymous",
      "issuer": "feathers",
      "algorithm": "HS256",
      "expiresIn": "1d"
    },
    "local": {
      "entity": "users",
      "service": "users",
      "usernameField": "username",
      "passwordField": "password"
    },
    "cookie": {
      "enabled": true,
      "domain": "localhost",
      "name": "feathers-jwt",
      "httpOnly": true,
      "secure": false // TODO make sure this is true in prod
    }
  }

Here is my authentication service and hooks

const authentication = require('feathers-authentication');
const jwt = require('feathers-authentication-jwt');
const local = require('feathers-authentication-local');
const manage = require('feathers-authentication-management');

module.exports = function() {
  const app = this;
  const config = app.get('auth');
  const managedConfig = app.get('managedAuth');

  // Set up authentication with the secret
  app.configure(authentication(config));
  app.configure(jwt());
  app.configure(local(config.local));
  app.configure(manage(managedConfig));

  app.service('authentication').hooks({
    before: {
      create: [
        authentication.hooks.authenticate(config.strategies)
      ],
      remove: [
        authentication.hooks.authenticate('jwt')
      ]
    }
  });
}

From the client's perspective, am I supposed to be able to use feathersClient to authenticate and then future navigation just works? Because this is not so 😢

I add routes with a function like so (I can just pass this to a forEach over an array of routes, makes it trivially easy to add new routes)

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

Ideally, wouldn't the feathers server set the header for the client? I'm at a loss of what I'm supposed to do...

To repro - set up normal feathers app, latest authentication - I'm using username and password as opposed to email (and only that, no OAuth for this project) - client landing page is / which is login form (don't even need to set up html, can just use chrome debugger), then use feathers client to get back the token. Clicking a link to /main or manually navigating to /main or even location.href= /main all don't work, even though the server should know this user is authenticated. I don't even know if this is supposed to work though...

I think I'm just missing something though... This should be easy, right? Logging users in? 😭


I found these two issues, so I think I'll have something to try tomorrow. I'm probably just doing something wrong.

PavelPolyakov commented 7 years ago

Is there any update on the topic? I see it started from the react native, but I use regular react and have the issue that after the auth any cookie is set.

https://github.com/feathersjs/feathers-authentication-local/issues/17#issuecomment-302887815

not sure if the issue is the same, but I assume so.

Do I understand right, that the use case of the cookie jwt is, that after the page is refreshed manually - customer doesn't need to authenticate himself again?

Regards,

marshallswain commented 7 years ago

If you're going to use the feathers client with Socket.io to make requests, authentication has to happen on every refresh. This is a limitation of the socket auth.

PavelPolyakov commented 7 years ago

@marshallswain yes, thanks, but if the cookie is set and the token is still valid, the authentication could be more or less "automatic".

therefore the fact of setting the cookie automatically is important.

PavelPolyakov commented 7 years ago

I also tracked that this piece of code: https://github.com/feathersjs/feathers-authentication/blob/master/src/express/set-cookie.js#L14

is never called (in my case at least), however the setCookie is registered.

marshallswain commented 7 years ago

Correct. We left it out on purpose to avoid CSRF attacks. You have to manually register it. The cookie is still useful in SSR scenarios without that middleware enabled, so it's not enabled automatically when you turn on the cookie option.

PavelPolyakov commented 7 years ago

Ah that's the case.

@marshallswain could you, please, confirm that I understood it right. The feathers-jwt cookie would be set (on the client) after the correct authentication only in case I register the express middleware explicitly? Like it's described here: https://docs.feathersjs.com/api/authentication/server.html#express-middleware .

And the option cookie: {enabled: true} is not enough?

marshallswain commented 7 years ago

Oops. I meant the expose-cookie middleware. set-cookie should run with the options you posted. On a development machine, you'll need these options to get a cookie:

    enabled: false, // whether the cookie should be enabled
    name: 'feathers-jwt', // the cookie name
    httpOnly: false, // whether the cookie should not be available to client side JavaScript
    secure: false // whether cookies should only be available over HTTPS

That middleware should run on successful authentication. Do you have your before hooks on the authentication service setup?

PavelPolyakov commented 7 years ago

@marshallswain yes, I think I have everything set.

here I describe more about the issue: https://github.com/feathersjs/feathers-authentication-local/issues/17#issuecomment-302887815

Regards,

daffl commented 6 years ago

It looks like this issue has become a little bit of a kitchen sink of authentication cookie issues but I think the main issue of authenticating Express routes by storing the JWT in the cookie has now been addressed and there is a guide at https://docs.feathersjs.com/guides/auth/recipe.express-middleware.html

r3gisc commented 5 years ago

My experience to fix this kind of issue. It may help => #541: