We are building our way towards integrating user information and authentication into the Notes application. The next step is to wrap the user data model we just created into a REST server. After that, we'll create a couple of scripts so that we can add some users, perform other administrative tasks, and generally verify that the service works. Finally, we'll extend the Notes application with login and logout support.
In the package.json file, change the main tag to the following line of code:
"main": "user-server.mjs",
Then create a file named user-server.mjs, containing the following code:
import restify from 'restify';
import util from 'util';
import DBG from 'debug';
const log = DBG('users:service');
const error = DBG('users:error');
import * as usersModel from './users-sequelize';
var server = restify.createServer({
name: "User-Auth-Service",
version: "0.0.1"
});
server.use(restify.plugins.authorizationParser());
server.use(check);
server.use(restify.plugins.queryParser());
server.use(restify.plugins.bodyParser({
mapParams: true
}));
The createServer method can take a long list of configuration options. These two may be useful for identifying information.
As with Express applications, the server.use calls initialize what Express would call middleware functions, but which Restify calls handler functions. These are callback functions whose API is function (req, res, next). As with Express, these are the request and response objects, and next is a function which, when called, carries execution to the next handler function.
Unlike Express, every handler function must call the next function. In order to tell Restify to stop processing through handlers, the next function must be called as next(false). Calling next with an error object also causes the execution to end, and the error is sent back to the requestor.
The handler functions listed here do two things: authorize requests and handle parsing parameters from both the URL and the post request body. The authorizationParser function looks for HTTP basic auth headers. The check function is shown later and emulates the idea of an API token to control access.
Refer to http://restify.com/docs/plugins-api/ for more information on the built-in handlers available in Restify.
Add this to user-server.mjs:
// Create a user record
server.post('/create-user', async (req, res, next) => {
try {
var result = await usersModel.create(
req.params.username, req.params.password,
req.params.provider,
req.params.familyName, req.params.givenName,
req.params.middleName,
req.params.emails, req.params.photos);
res.send(result);
next(false);
} catch(err) { res.send(500, err); next(false); }
});
As for Express, the server.VERB functions let us define the handlers for specific HTTP actions. This route handles a POST on /create-user, and, as the name implies, this will create a user by calling the usersModel.create function.
As a POST request, the parameters arrive in the body of the request rather than as URL parameters. Because of the mapParams flag on the bodyParams handler, the arguments passed in the HTTP body are added to req.params.
We simply call usersModel.create with the parameters sent to us. When completed, the result object should be a user object, which we send back to the requestor using res.send:
// Update an existing user record
server.post('/update-user/:username', async (req, res, next) => {
try {
var result = await usersModel.update(
req.params.username, req.params.password,
req.params.provider,
req.params.familyName, req.params.givenName,
req.params.middleName,
req.params.emails, req.params.photos);
res.send(usersModel.sanitizedUser(result));
next(false);
} catch(err) { res.send(500, err); next(false); }
});
The /update-user route is handled in a similar way. However, we have put the username parameter on the URL. Like Express, Restify lets you put named parameters in the URL like as follows. Such named parameters are also added to req.params.
We simply call usersModel.update with the parameters sent to us. That, too, returns an object we send back to the caller with res.send:
// Find a user, if not found create one given profile information
server.post('/find-or-create', async (req, res, next) => {
log('find-or-create '+ util.inspect(req.params));
try {
var result = await usersModel.findOrCreate({
id: req.params.username, username: req.params.username,
password: req.params.password, provider:
req.params.provider,
familyName: req.params.familyName, givenName:
req.params.givenName,
middleName: req.params.middleName,
emails: req.params.emails, photos: req.params.photos
});
res.send(result);
next(false);
} catch(err) { res.send(500, err); next(false); }
});
This handles our findOrCreate operation. We simply delegate this to the model code, as done previously.
As the name implies, we'll look to see whether the named user already exists and, if so, simply return that user, otherwise it will be created:
// Find the user data (does not return password)
server.get('/find/:username', async (req, res, next) => {
try {
var user = await usersModel.find(req.params.username);
if (!user) {
res.send(404, new Error("Did not find "+
req.params.username));
} else {
res.send(user);
}
next(false);
} catch(err) { res.send(500, err); next(false); }
});
Here, we support looking up the user object for the provided username.
If the user was not found, then we return a 404 status code because it indicates a resource that does not exist. Otherwise, we send the object that was retrieved:
// Delete/destroy a user record
server.del('/destroy/:username', async (req, res, next) => {
try {
await usersModel.destroy(req.params.username);
res.send({});
next(false);
} catch(err) { res.send(500, err); next(false); }
});
This is how we delete a user from the Notes application. The DEL HTTP verb is meant to be used to delete things on a server, making it the natural choice for this functionality:
// Check password
server.post('/passwordCheck', async (req, res, next) => {
try {
await usersModel.userPasswordCheck(
req.params.username, req.params.password);
res.send(check);
next(false);
} catch(err) { res.send(500, err); next(false); }
});
This is another aspect of keeping the password solely within this server. The password check is performed by this server, rather than in the Notes application. We simply call the usersModel.userPasswordCheck function shown earlier and send back the object it returns:
// List users
server.get('/list', async (req, res, next) => {
try {
var userlist = await usersModel.listUsers();
if (!userlist) userlist = [];
res.send(userlist);
next(false);
} catch(err) { res.send(500, err); next(false); }
});
Then, finally, if required, we send a list of Notes application users back to the requestor. In case no list of users is available, we at least send an empty array:
server.listen(process.env.PORT, "localhost", function() {
log(server.name +' listening at '+ server.url);
});
// Mimic API Key authentication.
var apiKeys = [ {
user: 'them',
key: 'D4ED43C0-8BD6-4FE2-B358-7C0E230D11EF'
} ];
function check(req, res, next) {
if (req.authorization) {
var found = false;
for (let auth of apiKeys) {
if (auth.key === req.authorization.basic.password
&& auth.user === req.authorization.basic.username) {
found = true;
break;
}
}
if (found) next();
else {
res.send(401, new Error("Not authenticated"));
next(false);
}
} else {
res.send(500, new Error('No Authorization Key'));
next(false);
}
}
As with the Notes application, we listen to the port named in the PORT environment variable. By explicitly listening only on localhost, we'll limit the scope of systems that can access the user authentication server. In a real deployment, we might have this server behind a firewall with a tight list of host systems allowed to have access.
This last function, check, implements authentication for the REST API itself. This is the handler function we added earlier.
It requires the caller to provide credentials on the HTTP request using the basic auth headers. The authorizationParser handler looks for this and gives it to us on the req.authorization.basic object. The check function simply verifies that the named user and password combination exists in the local array.
This is meant to mimic assigning an API key to an application. There are several ways of doing so; this is just one.
This approach is not limited to just authenticating using HTTP basic auth. The Restify API lets us look at any header in the HTTP request, meaning we could implement any kind of security mechanism we like. The check function could implement some other security method, with the right code.
Because we added check with the initial set of server.use handlers, it is called on every request. Therefore, every request to this server must provide the HTTP basic auth credentials required by this check.
This strategy is good if you want to control access to every single function in your API. For the user authentication service, that's probably a good idea. Some REST services in the world have certain API functions that are open to the world and others protected by an API token. To implement that, the check function should not be configured among the server.use handlers. Instead, it should be added to the appropriate route handlers as follows:
server.get('/request/url', authHandler, (req, res, next) => {
..
});
Such an authHandler would be coded similarly to our check function. A failure to authenticate is indicated by sending an error code and using next(false) to end the routing function chain.
We now have the complete code for the user authentication server. It defines several request URLs, and for each, the corresponding function in the user model is called.
Now we need a YAML file to hold the database credentials, so create sequelize-sqlite.yaml, containing the following code:
dbname: users
username:
password:
params:
dialect: sqlite
storage: users-sequelize.sqlite3
Since this is Sequelize, it's easy to switch to other database engines simply by supplying a different configuration file. Remember that the filename of this configuration file must appear in the SEQUELIZE_CONNECT environment variable.
Finally, package.json should look as follows:
{
"name": "user-auth-server",
"version": "0.0.1",
"description": "",
"main": "user-server.js",
"scripts": {
"start": "DEBUG=users:* PORT=3333 SEQUELIZE_CONNECT=sequelize-sqlite.yaml node --experimental-modules user-server"
},
"author": "",
"license": "ISC",
"engines": {
"node": ">=8.9"
},
"dependencies": {
"debug": "^2.6.9",
"fs-extra": "^5.x",
"js-yaml": "^3.10.x",
"mysql": "^2.15.x",
"restify": "^6.3.x",
"restify-clients": "^1.5.x",
"sqlite3": "^3.1.x",
"sequelize": "^4.31.x"
}
}
We configure this server to listen on port 3333 using the database credentials we just gave and with debugging output for the server code.
You can now start the user authentication server:
$ npm start
> user-auth-server@0.0.1 start /Users/david/chap08/users
> DEBUG=users:* PORT=3333 SEQUELIZE_CONNECT=sequelize-mysql.yaml node user-server
users:server User-Auth-Service listening at http://127.0.0.1:3333 +0ms
But we don't have any way to interact with this server, yet.