If both User and AccessToken are extended, login fails with:
<project dir>/node_modules/loopback/common/models/user.js:105
this.accessTokens.create({
^
TypeError: Cannot read property 'create' of undefined
at ModelConstructor.User.createAccessToken (<project dir>/node_modules/loopback/common/models/user.js:105:22)
at <project dir>/node_modules/loopback/common/models/user.js:261:22
at <project dir>/node_modules/loopback/common/models/user.js:319:9
at <project dir>/node_modules/loopback/node_modules/bcryptjs/dist/bcrypt.js:261:17
at <project dir>/node_modules/loopback/node_modules/bcryptjs/dist/bcrypt.js:1198:21
at Immediate.next (<project dir>/node_modules/loopback/node_modules/bcryptjs/dist/bcrypt.js:1078:21)
at Immediate._onImmediate (<project dir>/node_modules/loopback/node_modules/continuation-local-storage/node_modules/async-listener/glue.js:188:31)
at processImmediate [as _immediateCallback] (timers.js:383:17)
<project dir>/common/models/customer.json:
{
"name": "Customer",
"plural": "customers",
"base": "User",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"customerAccessTokens": {
"type": "hasMany",
"model": "CustomerAccessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
}
},
"acls": [],
"methods": {}
}
<project dir>/common/models/customer-access-token.json:
{
"name": "CustomerAccessToken",
"base": "AccessToken",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"customer": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "userId"
}
},
"acls": [],
"methods": {}
}
<project dir>/server/model-config.json:
{
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models"
],
"mixins": [
"loopback/common/mixins",
"loopback/server/mixins",
"../common/mixins",
"./mixins"
]
},
"ACL": {
"dataSource": "db",
"public": false
},
"RoleMapping": {
"dataSource": "db",
"public": false
},
"Role": {
"dataSource": "db",
"public": false
},
"Customer": {
"dataSource": "db",
"public": true
},
"CustomerAccessToken": {
"dataSource": "db",
"public": false
}
}
If the default entry for AccessToken is not removed from model-config.json, login works but ignores CustomerAccessToken. The issue affects both LoopBack 2.x and 3.x and is possibly related to #398 and #1450.
I was struggling with this too today. After some investigation and debugging in the loopback source code I believe I found out how to do this (using LoopBack v2, haven't tried v3). I'll call the custom user model ExtendedUser and the custom access token ExtendedAccessToken, but you can pick anything your want.
"idInjection": false and override the user relation like this: "relations": {
"user": {
"type": "belongsTo",
"model": "ExtendedUser",
"foreignKey": "userId"
}
}
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "ExtendedAccessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
}
}
_Note: the relation names (user/accessTokens) are crucial in the above points!_
req.accessToken for your own middleware or express routes, you'll have to use the loopback.token middleware with an explicit AccessToken model reference. For example in a boot script:var loopback = require('loopback');
module.exports = function(app) {
app.use(loopback.token({
model: app.models.ExtendedAccessToken
}));
};
So, unless there is an issue with this approach that I'm not yet aware of, it can be done without hacks. But it would be nice to have this better documented in a tutorial or something, it took me hours to figure this out.
Works perfectly (also for 3.x). Thanks a lot, appreciated!
@ritch, @raymondfeng and @bajtos, can you comment on whether the built-in models User and AccessToken can be removed from model-config.json if both are extended?
can you comment on whether the built-in models User and AccessToken can be removed from model-config.json if both are extended?
Yes, both builtin models should be removed from model-config when you are using your own extended models.
Thanks!
@bekl @bajtos Thanks for posting this and I'm glad its all recent! I added everything you have listed and we have a route that we use to check to see if the username exists already(we've been struggling with this route so any help on this would be great) GET /Jobusers/{username}/exists. I follow the normal steps of creating the user, login with the user, i can verify that the access token is in my db(was I supposed to add any columns?) then I add the token and click set token. After that I go to query the route posted above and I am getting 401 access denied. Any ideas?
You need to add appropriate ACLs to your remote method (see https://docs.strongloop.com/display/public/LB/Remote+methods#Remotemethods-AddingACLstoremotemethods).
Yes, both builtin models should be removed from model-config when you are using your own extended models.
I explicitly had to remove the builtin models, otherwise the accessTokens relationship on ExtendedUser would sometimes still reference the AccessToken model, not my ExtendedAccessToken model. This occurred in the context of calling ExtendedUser.login via the API. Note that the login method had not been overridden in ExtendedUser, so the builtin User.login was actually being called.
Thanks a lot for this solution @tvdstaaij 馃憤
@tvdstaaij Thanks for the detailed solution.
Tested with Loopback 3.6. Two important points:
Don't make the boot script above that uses loopback.token. It will cause an error 500 Cannot call AccessToken.findById(). Also, if you have in your server.js the script from the docs which uses current user id as a literal in URLs for REST, remove it. I am not sure what the alternative is (cc: @bajtos)
Docs Ref: Making authenticated requests
Sometimes the script in the file authentication.js refers to the built-in memory db datasource used for authentication like so:
module.exports = function enableAuthentication(server) {
server.enableAuth({ datasource: 'db' });
};
So make sure to change the datasource to your database, or just go with server.enableAuth();
I did workaround like this
create our own middleware that will add accessToken
app.middleware("auth", function(req, res, next) {
req.accessToken = req.query.accessToken || req.query.access_token || req.headers.authorization
next()
})
and then you can use this
app.middleware("auth", loopback.token({
currentUserLiteral: "maps",
model: app.models.accessToken,
cookies: ["access_token"]
}))
@tvdstaaij Thanks for the detailed solution.
Tested with Loopback 3.6. Two important points:
- Don't make the boot script above that uses
loopback.token. It will cause an error 500 Cannot call AccessToken.findById(). Also, if you have in yourserver.jsthe script from the docs which uses current user id as a literal in URLs for REST, remove it. I am not sure what the alternative is (cc: @bajtos)
Docs Ref: Making authenticated requests- Sometimes the script in the file
authentication.jsrefers to the built-in memory db datasource used for authentication like so:module.exports = function enableAuthentication(server) { server.enableAuth({ datasource: 'db' }); };So make sure to change the datasource to your database, or just go with
server.enableAuth();
I am getting the error you had described ".....It will cause an error 500 Cannot call AccessToken.findById()." .
Have you figured a work around solution for it.
I am trying to store and retrieve acccessToken from a custom accesstoken model.Although the access token is being stored in this model.Setting this accesstoken is not working as i think its still referring to the base accessToken model.
Most helpful comment
I was struggling with this too today. After some investigation and debugging in the loopback source code I believe I found out how to do this (using LoopBack v2, haven't tried v3). I'll call the custom user model ExtendedUser and the custom access token ExtendedAccessToken, but you can pick anything your want.
"idInjection": falseand override the user relation like this:_Note: the relation names (
user/accessTokens) are crucial in the above points!_req.accessTokenfor your own middleware or express routes, you'll have to use theloopback.tokenmiddleware with an explicit AccessToken model reference. For example in a boot script:So, unless there is an issue with this approach that I'm not yet aware of, it can be done without hacks. But it would be nice to have this better documented in a tutorial or something, it took me hours to figure this out.