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

auth.hooks.authenticate(['jwt']) not adding user to params #545

Closed
MHerszak opened this issue Jul 17, 2017 · 14 comments
Closed

auth.hooks.authenticate(['jwt']) not adding user to params #545

MHerszak opened this issue Jul 17, 2017 · 14 comments

Comments

@MHerszak
Copy link

MHerszak commented Jul 17, 2017

Steps to reproduce

I am currently trying to access hooks.params.user on almost all my services on the server. I am using auth.hooks.authenticate(['jwt']).

Expected behavior

I expect a user to be attached to hook.params.

Actual behavior

Every time I access hook.params it seems to be empty.

System configuration

Module versions (especially the part that's not working):

"feathers": "^2.1.7",
"feathers-authentication": "^1.2.7",
"feathers-authentication-client": "^0.3.2",
"feathers-authentication-hooks": "^0.1.4",
"feathers-authentication-jwt": "^0.3.2",

NodeJS version:

"engines": {
    "node": "8.1.4",
    "npm": "5.2.0"
  }
@MHerszak MHerszak changed the title auth.hooks.authenticate(['jwt', 'local'], { assignProperty: 'custom' }) not adding user to params auth.hooks.authenticate(['jwt']) not adding user to params Jul 17, 2017
@marshallswain
Copy link
Member

That's odd. Looking at the code, it looks like they get merged in here: https://github.com/feathersjs/feathers-authentication/blob/master/src/hooks/authenticate.js#L55

Can you try using a debugger to track it down around that point? (it will have to be in the compiled version)

@MHerszak
Copy link
Author

MHerszak commented Jul 17, 2017

@marshallswain This is what I get when I debug:

feathers-authentication:hooks:authenticate Attempting to authenticate using jwt strategy with options { name: 'jwt',
[2]   bodyKey: 'accessToken',
[2]   secret: 'super secret',
[2]   header: 'Authorization',
[2]   entity: 'user',
[2]   service: 'users',
[2]   passReqToCallback: true,
[2]   session: false,
[2]   jwt: 
[2]    { header: { typ: 'access' },
[2]      audience: 'https://yourdomain.com',
[2]      subject: 'anonymous',
[2]      issuer: 'feathers',
[2]      algorithm: 'HS256',
[2]      expiresIn: '1d' } }

I am also debugging the request object above. Params are already empty there. I feel like I am missing something. The accessToken is available in the body, though.

@ekryski
Copy link
Member

ekryski commented Jul 24, 2017

@MHerszak do you have more logs? Do you see the user in the debug logs? What does your config look like? How are you setting up/registering authentication? All that would make it much easier to see what is going on. There are lots of moving parts with auth.

@MHerszak
Copy link
Author

Hi, @ekryski,

I don't see a user but I see my accessToken in the header. My config file:

module.exports = {
  app: {
    brand: process.env.BRAND_NAME,
    title: process.env.BRAND_TITLE,
    defaultEmail: process.env.DEFAULT_EMAIL
  },
  services: {
    email: {
      sendgrid: {
        SENDGRID_API_KEY: process.env.EMAIL_ENV_KEY
      }
    },
    aws: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      region: process.env.AWS_REGION
    }
  },
  authentication: {
    // service: 'login',
    secret: 'super secret',
    cookie: {
      enabled: true,
      httpOnly: false,
      name: 'feathers-jwt',
      maxAge: ONE_DAY,
      secure: process.env.NODE_ENV === 'production'
    }
  },
  host: process.env.ONLINE_DOMAIN || process.env.HOST || 'localhost',
  domain: process.env.ONLINE_DOMAIN,
  port: process.env.PORT || 3000,
  apiHost: process.env.APIHOST || 'localhost',
  apiPort: process.env.APIPORT,
  protocol: process.env.protocol || 'http',
  mongoDB: process.env.MONGO_URL,
  wkDir: process.cwd(),
  staticDir: {
    emails: 'static/emails'
  },
  staticFile: {
    hbs: 'hbs'
  },
};

In terms of registering and sign in. I have a afterCreate hook and do this:

function populateUser(authConfig) {
  return hook => hook.app.passport.verifyJWT(hook.result.accessToken, authConfig)
    .then(payload => hook.app.service('users').get(payload.userId))
    .then(user => {
      hook.result.user = user;
      return hook;
    });
}

I just noticed verifyJWT only has one parameter now. Where is the authConfig going to?

Does that help?

Thanks for your response!

@minirobotdan
Copy link

I have the same problem whilst running unit tests. My config is broadly the same as the OP. I create some dummy users in the before test hooks, and my tests fail due to the associateCurrentUser hook not finding the configured key (the error is actually just symptomatic of hook.params.user being an empty object).

Test bootstrap code as follows:

const adminUser = {
    name: 'admin user',
    email: 'admin@test.com',
    password: 'test',
    role: 'admin'
};
const clientUser = {
    name: 'client user',
    email: 'user@test.com',
    password: 'test',
    role: 'user',
    employer: '573217f7d7006e11467ec18b' // associateCurrentUser key
};

const createUsers = function () {
    const service = app.service('users');
    return Promise.all([service.create(adminUser), service.create(clientUser)]);
};

const authenticateUser = function () {
    const strategy = 'local';
    return app.service('authentication').create({ email: 'user@test.com', password: 'test', strategy })
    .then(result => {
        return result.accessToken;
    });
};

Abridged test bootstrap code:

before(usersMocks.tearDownUsers);
before(employerMocks.tearDownEmployers);
before(employerMocks.createEmployer);
before(usersMocks.createUsers);
before(() => {
    return usersMocks.authenticateUser().then(token => {
        userToken = token;
    });
});
before(() => {
    return usersMocks.authenticateAdminUser().then(token => {
        adminToken = token;
    });
});

it('creates a job and attaches the employer field correctly', done => {
    const service = app.service('jobs');
    chai.request(app)
        .post('/jobs')
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .set('Authorization', userToken)
        .send(jobsMocks.dummyData[0])
        .end((err, res) => {
            if(err) { 
                done(err); // Test fails here, stepping through reveals the problems described above
            }
            expect(err).to.be.null;
            expect(res.body.employer).to.equal(usersMocks.clientUser.employer);
            expect(res.body.slug).to.equal('foo');
            done();
        });
});

When I make the request in the test, the hook contains authenticated: true, and the code makes it past the hooks.authenticate('jwt') middleware call ok, but the user is not appended to the params. Looking through _lib/passport/authenticate.js, it appears as if the user payload is never returned from the strategy.success callback and merged into the request body, but I'm not sure why.

Regular code execution seems fine, this only seems to manifest in tests in my case.

@hbj
Copy link

hbj commented Dec 28, 2017

Having the same issue in unit tests when doing authenticating through the app.authenticate function, but works correctly when authenticating through an HTTP request (POST /authenticate).

@daffl
Copy link
Member

daffl commented Dec 29, 2017

Internal requests are not authenticated. Just pass the user you need as params.user on the service params.

@hbj
Copy link

hbj commented Dec 29, 2017

I have corrected my answer, I don't know whether you answered the original or the corrected one.

But why when using the app.authenticate middleware directly the user is empty but is populated when called through a request?

@daffl
Copy link
Member

daffl commented Dec 30, 2017

Internal calls on the server are allowed to do everything, why would you need to authenticate if you can just pass the user you need?

@hbj
Copy link

hbj commented Dec 30, 2017

I want to test authenticated requests, but I thought it would be easier to create the authorization token by calling the authenticate function instead of making an authentication request. I do get a token, but when I use it in a request I get an empty params.user object in the hooks, which doesn't happen if I get the token through an authentication request.

@abalad
Copy link

abalad commented Mar 3, 2018

It seems that this affects libs as feathers-authentication-hooks, which in queryWithCurrentUser, uses the hook.param.user internally and when it does not find it generates an error: There is no current user to associate.

@yousseftarek
Copy link

yousseftarek commented Jul 24, 2018

I'm having an issue with getting the user to show up in hook.param.user.
This bit of code:
app.service('authentication').hooks({ after : { create : [ context => { context.result.user = context.params.user; console.log(context.result, context.params); } ] } })
doesn't seem to show the user and keep printing undefined.

@sjones6
Copy link

sjones6 commented Aug 14, 2018

I'm experiencing the same issue.

I have a similar setup as mentioned by @minirobotdan (i.e., seed some users, log them in, then attach their JWT as an header. After some debugging, I tracked the issue down (documented below).

It's starts with these lines.

Here's the relevant extract:

const id = payload[`${this.options.entity}Id`];
if (id === undefined) {
  debug(`JWT payload does not contain ${this.options.entity}Id`);
  return done(null, {}, payload);
}
debug(`Looking up ${this.options.entity} by id`, id);

It's looking for userId in the JWT payload, which only contains the following keys in the test environment: "iat", "exp", "aud", "iss", "sub", "jti". In the development/production environment, the payload object does include 'userId'.

This appears to be set in @feathers/authentication-local:

/* snip ... */
.then(entity => {
 const id = entity[this.service.id];
 const payload = { [`${this.options.entity}Id`]: id }; // <- this adds the user id.
  done(null, entity, payload);
})

These lines are never hit in test ... because in my test code, I "authenticated" the fake user like so:

app.service('/authentication')
  .create({
    strategy: 'local',
    password: 'foo',
    email: 'bar'
}).then(({ accessToken }) => { /* use access token */})

Somehow, while the authentication request does indeed generate an access token, it bypasses the bit in the authentication-local package that sets the userId on the JSON payload.

The "fix" here is to switch this to simulate an actual request (I'm using supertest):

request(app)
 .post('/authentication')
 .send({ /* authentication payload */
 .then(res => { res.body.accessToken /* has jwt now */ });

Based on @daffl 's comment above, it sounds like this is expected behavior. So, perhaps the answer is to just go all the way and simulate a full login request rather than grabbing the application service and putting the JWT creation directly against the service.

@daffle can you confirm that there's no other way to use internal call app.service('/authentication').create(/* etc */) whereby a fully formed JWT that includes the userId is returned? Seems like this is just confusing since the internal call to the authentication service returns a JWT but it's not the same as the JWT generated from an external call.

@daffl
Copy link
Member

daffl commented May 8, 2019

This has been fixed Feathers v4 authentication.

Please see the migration guide for more information. Closing this issue in order to archive this repository. Related issues can be opened at the new code location in the Feathers main repository.

@daffl daffl closed this as completed May 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants