When you build a REST API, creating the infrastructure to generate and manage API keys, OAuth tokens, and scopes can be tedious, risky and time-consuming. Fortunately, Stormpath just added API key management to our express-stormpath module. Now, API and webapp developers using express.js can generate and manage API Keys and OAuth tokens as well as lookup and secure developer accounts – all without custom OAuth code.
What We’re Building
This post will walk you through an express application that lets you:
- Create a new account (register) with email and password.
- Log into your new account (login) with email and password.
- Display a dashboard page once you’ve logged in, that is only accessible to registered users.
- Automatically generate and show an API Key for a logged in user on the dashboard.
- Make a REST call using Basic authentication.
- Generate an OAuth token with scope.
- Make a REST call using Bearer authentication.
- Allow a logged in user to log out of their account (logout).
The API I built is a simple one: it returns the weather for a requested city in Fahrenheit.
Before We Start…
- Follow along with the app online
- When you have the application running in a browser, open the “Web Inspector” and navigate to the Network tab to see some important things happening:
- When making any REST calls on generating OAuth tokens, click on the request made to the
/weather
endpoint and look at the Request Headers –> Authorization. - When generating an OAuth Token, click on the
oauth
request and scroll down to the section titled “Form Data” to see the grant_type and the scope of the request.
- When making any REST calls on generating OAuth tokens, click on the request made to the
- Note the directory structure of the github repository for this project. All server-side logic (which this post focuses on) will be in
/server.js
.
Let’s get started.
Login
User registration and login are built into express-stormpath, and a good place to start with any app. You can see the login screens on the live app.
Let’s take a look at the code that makes this happen. We first import the necessary packages (all code in this post goes into the root node file, in my case /server.js)
:
var express = require('express');
var server = express();
var stormpath = require('express-stormpath');
Next, we set up the server to use Stormpath:
server.use(stormpath.init(server, {
application: process.env['STORMPATH_APP_HREF'],
secretKey: process.env['STORMPATH_SECRET_KEY'],
redirectUrl: '/dashboard',
getOauthTokenUrl: '/oauth',
oauthTTL: 3600
}));
The application
field points Stormpath to the right application for the project. Users will be pointed to the redirectUrl
after logging in or creating an account. As for the Oauth fields, we will get back to those a little later. It is important to avoid storing your API Key, Application Href, and Secret Keys (used for user sessions) in plain text in your code. Instead, export these as environment variables and access them in server.js by doing process.env['name_of_env_var']
. Stormpath-express is capable of reading the environment variables itself, so exporting them to your system is enough; however, above I show how to manually set the stormpath-express variables in code.
Once a user goes to our site (the root of the site or ‘/’), we need to redirect them to the custom login page provided by stormpath-express, which by default lives at /login
.
server.get('/', function(req, res) {
res.redirect(302, "/login");
});
Now that we have login and account creation out of the way, let’s use API Keys to protect our weather API.
API Key Generation
First, we need to give the user an API key to use. This way, any REST endpoint is protected and accessible only to users who possess a valid API Key and Secret. Once a user logs in or creates an account, they will go directly to the application dashboard, where an API Key is automatically generated and displayed.
Let’s see what the code looks like:
server.get('/dashboard', stormpath.loginRequired, function(req, res) {
res.locals.user.getApiKeys(function(err, collectionResult) {
if(collectionResult.items.length == 0) {
res.locals.user.createApiKey(function(err, apiKey) {
res.locals.apiKeyId = apiKey.id;
res.locals.apiKeySecret = apiKey.secret;
res.locals.username = res.locals.user.username;
res.render("dashboard.ejs");
});
}
else {
collectionResult.each(function(apiKey) {
res.locals.apiKeyId = apiKey.id;
res.locals.apiKeySecret = apiKey.secret;
res.locals.username = res.locals.user.username;
res.render("dashboard.ejs");
});
}
})
});
By calling res.locals.user.getApiKeys
we ask Stormpath to return a collection of an account’s API Keys. In the if
statement, we check if the account has any API Keys. If not, Stormpath generates one and returns it to the client. In the else
statement, where an API Key has already been generated, Stormpath returns the first API Key available.
Making a REST Call With Basic Authentication
Now the user is logged in and has access using the API Key Id and Secret. Let’s have them make an API call.
In this sample application, the REST endpoint returns a floating point number, representing the weather in the requested city. For example if a GET request is made to /weather/London
, a floating point number with one digit after the decimal is returned to the client representing the weather in London. Only one endpoint is available in the form of weather/
, where city can be any one of the four cities provided by the radio buttons.
First, the client has to Base64 encode the key:secret pair and send this to the server as the authorization header. In angular.js, the HTTP request would look something like this:
$http({method: "GET", url: '/weather/' + $scope.city, headers: {'Authorization': 'Basic ' + sharedProperties.getEncodedAuth()}}).success(function(data, status, headers, config) {
$scope.temp = data + ' F';
$scope.myCity = $scope.city;
})
.error(function(data, status, headers, config) {
$window.alert("Error");
});
In this HTTP request, we specify the desired city in the url: /weather/
and add our Base64 encoded API key as the authorization header.
Now lets take a look at what happens on the server side, where this request gets processed:
server.get('/weather/:city', stormpath.apiAuthenticationRequired, function(req, res) {
if(req.headers.authorization.indexOf('Basic') === 0) {
getWeather();
}
else {
res.status(403).end();
}
function getWeather() {
console.log("Getting weather for " + req.params.city);
var url = "http://api.openweathermap.org/data/2.5/weather?q=" + req.params.city;
var data = "";
http.get(url, function(myRes) {
myRes.on('data', function(chunk) {
data += chunk;
});
myRes.on('end', function() {
callback(data);
});
}).on('error', function() {
console.log("Error getting data.");
});
}
function callback(finalData) {
var json = JSON.parse(finalData);
//convert to Farenheight
var farenheight = Math.round((((parseFloat(json.main.temp) - 273.15)
* 1.8) + 32) * 10)/10;
res.status(200).json(farenheight);
}
});
First, notice the stormpath.apiAuthenticationRequired
call that precedes the callback function of our route. This function verifies that the authorization credentials sent over are legitimate. If they are not, the server will return a 401 Unauthorized
error. Assuming the credentials are correct, the server is then allowed to return the weather of the desired city.
Here is how the same request can be made with CURL:
curl --user "[API_KEY]:[API_SECRET]" http://localhost:8080/weather/London
If authentication is successful you will get back a floating point number representing the weather in London. If it is not, you will see: {“error”:“Invalid API credentials.”}.
Generating an OAuth Token with Scope
Basic Authentication is acceptable for a few use cases, but we strongly recommend you use OAuth if security is important to your API. By using OAuth, making requests to protected endpoints does not expose the API Key Id and Secret. It also gives a developer the ability to only give access to certain scopes of the endpoint the user is trying to access. This is compared to authenticating with API keys, which gives access to the entire endpoint.
By checking the desired cities and clicking Get Oauth
, the user gets a token which can now be used to target the REST endpoint. What exactly happened on the server side to generate this Oauth Token? Let’s look at the server setup code one more time:
server.use(stormpath.init(server, {
application: process.env['STORMPATH_APP_HREF'],
secretKey: process.env['STORMPATH_SECRET_KEY'],
redirectUrl: '/dashboard',
getOauthTokenUrl: '/oauth',
oauthTTL: 3600
}));
The getOauthTokenUrl
is set to /oauth
, which means that a POST
request sent to that URL will check for API Key credentials and return a Token that is valid for 1 hour (by default). This POST
request also needs to have a form parameter “grant_type” with the value set as the requested scope. Here is the request in angular.js:
myData = $.param({grant_type: "client_credentials", scope: scopeData});
$http({method: "POST", url: '/oauth',
headers: {'Authorization': 'Basic ' + sharedProperties.getEncodedAuth(), 'Content-Type': 'application/x-www-form-urlencoded'},
data : myData})
.success(function(data, status, headers, config) {
var oauthToken = data.access_token;
$scope.oauthToken = oauthToken;
}).
error(function(data, status, headers, config) {
$window.alert("Error");
});
Making a REST Call Using the OAuth Token
In order to hit the REST endpoint using Oauth, we must send our token to the weather/
endpoint using Bearer authentication:
$http({method: "GET", url: '/weather/' + $scope.city, headers: {'Authorization': 'Bearer ' + sharedProperties.getOauthToken()}}).success(function(data, status, headers, config) {
$scope.temp = data + ' F';
$scope.myCity = $scope.city;
})
.error(function(data, status, headers, config) {
$window.alert("Permission Denied!");
});
Now, on the server side we can add the logic for Bearer authentication, and parse our requested scopes.
server.get('/weather/:city', stormpath.apiAuthenticationRequired, function(req, res) {
if(req.headers.authorization.indexOf('Basic') === 0) {
getWeather();
}
else if(req.headers.authorization.indexOf('Bearer') === 0) {
var requestedCity = req.params.city.replace(/\s+/g, '');
if(res.locals.permissions.indexOf(requestedCity) >= 0){
getWeather();
}
else {
res.status(403).end();
}
}
else {
res.status(403).end();
}
function getWeather() {
console.log("Getting weather for " + req.params.city);
var url = "http://api.openweathermap.org/data/2.5/weather?q=" + req.params.city;
var data = "";
http.get(url, function(myRes) {
myRes.on('data', function(chunk) {
data += chunk;
});
myRes.on('end', function() {
callback(data);
});
}).on('error', function() {
console.log("Error getting data.");
});
}
function callback(finalData) {
var json = JSON.parse(finalData);
console.log(json.main.temp);
//convert to Farenheight
var farenheight = Math.round((((parseFloat(json.main.temp) - 273.15)
* 1.8) + 32) * 10)/10;
res.status(200).json(farenheight);
}
});
The requested scopes live inside the res.locals.permissions
object and we can search it to see if the city we want the weather for is permitted for us. If so, the server will proceed to return the weather; if not, a 403
is returned. Compared to a 401
error, which is stands for an unauthorized request, a 403
represents a forbidden request.
London was part of the scopes in the Oauth Token so getting its weather is no problem:
Berlin on the other hand was not, so the weather is not given and an error is returned instead:
Conclusion
Node.js and the stormpath-express package make it easy to generate and manage API Key-based authentication in your webapp or API. If you’d like to see more code and even run this application yourself, check out the source code and let us know what you think!