Reputation: 8376
I'm working in a web app which handle resources from a Mongo database, for such resources I'd like to offer an API, so a future mobile application can seize it or consume it from a raw client.
However I'd like to have web app consuming same API, here is where I get a bit confused about how to properly implement this.
Here is what I've done so far:
app.route('/api/auth/')
.post(function (request,response) {
var email = request.body.email;
var password = request.body.password;
var login = new Account({"local.email":email,"local.password":password});
Account.findOne({"local.email":email}, function (err,user) {
if (err) {
response.send(500);
}
if (!user) {
response.send(404);
}
else {
user.validPassword(password, function (err,matched) {
if (err) {
response.send(500);
}
if (matched) {
var uuidToken = uuid.v4();
redisClient.set(uuidToken,user._id,redis.print);
redisClient.expire(user._id,100);
response.send(uuid);
}
else {
response.send(403);
}
});
}
});
});
So basically I receive consumers username and password, I authenticate it against database, If it matches I reply a token, (actually an UUID
). That token gets stored at Redis paired with the user id in databse. Every future request to any API route will verify for such token existance.
Here I wonder:
Basically I perform SAME username-password authentication against database and I then:
1. Start a new server session.
2. Naturally, offer back a cookie with session ID.
3. I create then the Redis UUID and user ID record, which API will check. I guess this is OK as there's any sense in requesting POST /api/auth
authenticating again.
Here I wonder:
This is example of POST /login
:
app.route('/login')
.post(function (request,response,next) {
var email = request.body.email;
var password = request.body.password;
var login = new Account({"local.email":email,"local.password":password});
Account.findOne({"local.email":email}, function (err,user) {
if (err) {
response.redirect('/error');
}
if (!user) {
var cookie = request.cookies.userAttempts;
if (cookie === undefined) {
response.cookie('userAttempts',1);
}
else {
response.cookie('userAttempts',(++cookie));
}
response.redirect('/');
}
else {
user.validPassword(password, function (err,matched) {
if (err) {
// Redirect error site or show err message.
response.redirect('/error');
}
if (matched) {
var session = request.session;
session.userid = user._id;
var uuidToken = uuid.v4();
redisClient.set(uuidToken,user._id,redis.print);
redisClient.expire(uuidToken,900);
response.cookie('email',email);
response.redirect('/start');
}
else {
var cookie = request.cookies.passwordAttemps;
if (cookie === undefined)
response.cookie('passwordAttemps',1);
else {
var attemps = ++request.cookies.attemps
response.cookie('passwordAttemps', attemps)
}
response.redirect('/');
}
});
}
});
})
I think I could get rid of using and writing a typical session implementation and depend somehow on the similar token based auth the API has.
Upvotes: 2
Views: 3716
Reputation: 2679
What you have there is on the right track and basically replaces some of the functionality of cookies. There are a few things to consider though, and you've touched on some of them already.
While using a UUID (v4 I'm guessing?) is good in that it's nondeterministic and "random", on its own the token is worthless. Should redis lose data the token no longer has any context. Nor can you enforce expirations without help from redis. Compare this to a JWT which can carry context on its own, can be decrypted by anybody with the correct key, can handle expirations, and can enforce further common application level constraints (issuer, audience, etc).
Rate limiting. There are a number of ways to handle this and few of them are tied directly to your choice of token scheme aside from the fact that you'd probably use the token as the key to identify a user across requests in the rate limiter.
Transparently passing the token in both a web app and on other clients (mobile app, desktop app, etc) can be a huge pain. In order to access private resources the user will need to pass the token in the request somewhere, likely the headers, and in the case of a web app this means manual intervention on your part to include the token in each request. This means hand coded ajax requests for all authenticated requests. While this can be annoying, at least it's possible to do, and if you're writing a single page app it's likely you'd do that anyways. The same can be said for any mobile or desktop client. Since you already have to make the HTTP request directly in code anyways, why does it matter? Now imagine the scenario where an HTTP GET endpoint, which returns an html page, can only be accessed with proper authentication. In the case of a web app the user is very likely going to access this via a browser redirect or by typing it directly into the URL bar. How is the token added to the request? Other than using cookies, which you're explicitly not using because mobile and desktop clients do not implement them, this is not really possible. However, if your API clients can always modify the HTTP request structure this isn't really a problem.
Now for a shameless plug, our team has a library we use for this. It's mostly used internally and as such is pretty opinionated on its dependencies (express, redis), but hopefully it can help you here. In fact, that library is pretty much just a JWT wrapper around what you have in place. If you decide to use it and notice any issues or deficiencies feel free to file any issues on github. Otherwise there are a whole bunch of other JWT based session management modules on npm that look promising. I would check those out regardless as there are very likely better modules out there than ours. Again, ours is used internally and came about from a pretty specific set of use cases so the chances that it captures all of yours are pretty slim. On the other hand, it sounds like you're using a similar stack so maybe the shoe fits.
If you do use ours it may seem odd that there's a split in the API surface on that module in that you can choose to store data directly in the JWT claims or in redis. This was deliberate and I think your examples illustrate a good use case for both sides. Typically what we do is store the user's email and name in the JWT claims, then store more dynamic session data in redis on their session. For example, upon logging in you'd add the issuer, audience, and user's email to the JWT claims but leave off anything related to "userAttempts". Then upon failed attempts you would add or modify the "userAttempts" on the session data stored in redis related to that JWT. Once a JWT is set it's not possible to modify its contents without generating a new one, so be aware that if you decide to keep relatively dynamic data in the JWT you'll have a constant exchange of old and new JWT's between the server and client.
Upvotes: 1