Microservices User Info and Authorization
andyet postpostgresnode.jshapiarchitecturejavascriptMicroservice is a buzz-word we’ve been hearing a lot of lately, however, it’s neither a new concept, nor is it a bad idea. Writing your processes as APIs in such a way that they can be run many times enables horizontal scaling and availability and has been a common best practice for quite awhile, but the subtleties of dealing with user information in a microservice should be addressed.
Authentication and Authorization #
Each microservice should not have to do its own authentication, but it does need to do its own authorization. At Seaworthy, we like to run an openid-connect (an oAuth extension) service that handles logins and token generation separate from the rest of the APIs, which lines up pretty well with the whole microservice way of doing things. In the end, the other APIs receive the webtoken either directly or are pre-validated through a gateway. The API can safely assume each request is authenticated, but this doesn’t give us much in the way of object-level permissions (AKA authorization).
Each API should keep track of its own object-level permissions, and it can do so without anything more than a pre-validated userid or groupid. Simply record an object or row that has the id of the object, the id of the user or group, and a set of flags for which permissions they have on that object. That way, when a user tries to do an action on an object, we can join to the appropriate permissions object if it exists, and determine what the user can and can't do to that object. The point is, object-level-permissions exists in the microservice database store without extra user context.
A pseudo-code example of how you might check an object-level permission before updating in an SQL database:
IF EXISTS (SELECT write FROM objecttype_access WHERE userid=$userid AND objectid=$objectid AND write=True) THEN
UPDATE objecttype SET value=$value WHERE objectid=$objectid;
END IF;
The only users who can change permissions or have any access to REST routes that involve permissions, should be users with a special administrator or group-administrator scope in their webtoken. This way, you can bootstrap and manage the permissions for other users.
A Node.js hapi example of how you might check for “api_admin” scope before you allow adding an object-level permission:
server.route({ method: 'post', path: '/someobject_access', config: {
auth: { scope: 'api_admin' },
handler: function (request, result) {
var pl = request.payload;
db.query("insert into objectttype_access (userid, objectid, read, write) values ($1, $2, $3, $4)",
[pl.userid, pl.objectid, pl.read, pl.write], function (err) {
if (err) throw Boom.badImplementation("Adding permission failed.");
reply();
});
}
}});
Using this approach we do not require extra user information for authorization such as the username and/or email address - that information is stored elsewhere and only required for authentication.
User Info For the Client #
When a client is using your microservice, and an object refers to a user, the microservice’s API should only send back the userid it is referring to. If the client needs more information about that user, like the display name, it should query the user information microservice, and possibly cache the result. Your user information service should really only display public information about the user, unless it keeps track of object level permissions on things like email addresses. Keep billing information, addresses, and password tokens in separate databases so that if this service is compromised, critical customer information isn’t.
A Node.js wreck example of using the API and filling in user information from a separate API:
wreck.get("https://someapi.yourproject.com/someobjects",
{headers: {authorization: token}},
function (err, response) {
if (err) throw err;
async.map(response.payload.results, function (item, mapcb) {
wreck.get("https://user-info-api.yourproject.com/users/" + item.userid, function (err, userResponse) {
if (err) return mapcb(err);
item.user = userResponse.payload;
mapcb(err, item);
});
}, function (err, results) {
if (err) throw err;
console.log(results);
});
});
Conclusion #
In the end, remember to keep user information separate: authentication, billing, and identity information should all be in separate services with separate databases. Keep track of object-level permissions in the API that those objects live in, and manage permissions with special scopes. API clients should only get the immutable userid from your microservice API, and should cross-reference an identity API to get more context.
If you follow these rules, you’ll have a way of building microservice APIs that are easy to write, don’t duplicate information, scale well, and keep your critical user-information separate.
If you have a microservices problem... if no one else can help... and if you can find them... maybe you can hire... The Seaworthy-Team.