I was recently noodling around on a problem involving more secure express.router() endpoints on node.js servers. As you use more ‘x-as-a-service’ APIs, you begin to notice that Regex and regular expressions are a common available feature – Firebase, for example, employs matching strings as a part of its security method. This got me thinking – why can’t I do something similar for my own points of entry?
In the process, I found some hard ways to do it, but I also found a pretty easy way to make this happen.
#0 – A quick review on express.router
While the Express project describes itself as a “minimal and flexible web application framework”, the Router() function docs call it a “mini-application.” What is meant by this is that in Express a router object is a function limited to only routing functions and middleware operations. You can still accomplish a good deal from ‘just’ inside of middleware, however.
For instance, to execute something every time the router is invoked, you simply place it at the top level of your router.
1 2 3 4 |
router.use(function(req, res, next) { // perform some sort of logic test, such as body contents in req or authentication as occurred next(); // moving on... }); |
Similarly, you can use the same sort of design pattern on a deeper location of your route, allowing for more localized and less global logic.
1 2 3 4 |
router.use('/user', function(req, res, next) { // perform additional logic next(); }); |
From there, you can respond to routes using .get and .post functions, two paradigms which should feel familiar as typical HTTP methods to anyone with a background in server technologies.
1 2 3 4 5 6 7 |
router.get('/user/:parameter', function(req, res) { // do something }); router.post('/profile', function(req, res) { // do something else }); |
With that oversimplified explanation out of the way, let’s jump into something more interesting.
#1 – Using Parameters on .get methods
As far as experimenting and creating examples for this sort of functionality, I find it far easier to start by working off of .get methods. But first, let’s create a little server code.
1 2 3 4 5 6 7 8 |
// Let's get it started var express = require('express'); var app = express(); var router = express.Router(); var port = process.env.PORT || 8080; app.use('/api', router); app.listen(port); console.log('Port ' + port + ' is a go.'); |
Router() methods, at their most basic, will simply resolve as long as you have them setup.
1 2 3 |
router.get('/user', function(req, res){ res.send('I am route!'); }); |
You can also pass parameters into your routes by using the pattern below. They are accessed through the param method inside your route function.
1 2 3 |
router.get('/user/:username/:thing', function(req, res){ res.send('Hey, ' + req.params.username + '. I am ' + req.params.thing + '!'); }); |
This is cool and easy, but it also means that anyone can just walk right on in. That is less cool. What would be nice is if it were possible to prevent random or snooping traffic from making it’s way into our system.
#2 – Use Regex in line with your Route
As mentioned before, Router() also allows you to include logic onto your route. In this instance, that is handy so that you can check against the parameter that is being passed against your route.
1 2 3 4 |
router.route('/user/:username([A-Z]+)/:thing') .get(function(req, res) { res.send('Hey, ' + req.params.username + '. I am ' + req.params.thing + '!'); }); |
As a very simple example, this updated route will now only accept usernames that are composed of uppercase letters.
Should it fail due to an unmatched URL, it will return a 404 as well as an error message thanks to some Express defaults.
In case you are interested, route actually works by turning the route’s strings into regular expressions, which Express then matches against the incoming requested routes. What we end up doing here, then, is placing additional constraints on those internal regular expressions.
#3 – Create Params Functions as Middleware Functions
That pattern works if a) you have a fairly simple string you are trying to match and b) you are don’t need to do anything else other than pass or fail. Should you want to get more complex or creative, however, you are going to want to dig further into the documentation.
Router.param() lets you add increased complexity to the execution series for your router method, and in this case is a great benefit to making more complex checking requirements manageable. param logic is fired before your route code, giving you the same sort of managed process as before while also pushing you past the ‘yes’ or ‘no’ restrictions.
To start, you simply need to remove the inline check and make a simpler route statement.
1 2 3 4 |
router.route('/user/:username/:thing') .get(function(req, res) { res.send('Hey, ' + req.params.username + '. I am ' + req.params.thing + '!'); }); |
You then need to create a param function. The method should be named to match the parameter you are actually trying to match, and when set up correctly will pass the GET variable into the param function so you can work with it however you so choose. In this case, I will opt to only check against a regular expression to confirm that the username is made up of numbers. If it is, we will proceed as normal. If it is not, then I will toss it to Express’ error methods.
1 2 3 4 5 6 |
router.param('username', function(req, res, next, username) { if(username.match(/([0-9]+)/) !== null) next(); else next(err); }); |
See this post if you are having some trouble.
Another nice thing about this design pattern is that param functions are local to their router objects, so you don’t have to worry as much about naming conflicts – authenticated pages such as profile/:username and public pages such as homepage/:username can require different constraints while keeping your code readable.
While these patterns are only a start, they should provide a reliable foundation for creating specifically constrained systems that reject unexpected inputs. It is, of course, not a bullet proof method by itself, but can be added as as nice addition to any routing recipe.
I’d be very interested to hear any thoughts, questions, comments, or suggestions below!
Note that routing regexps are case-insensitive by default. Your example #2 could be clarified.