Tutorial: Build an OAuth2 protected REST API with node.js using Restify, mongodb, and Passport.js - Brandon Werner - Site Home - MSDN Blogs

Brandon Werner

Extending the magic of software to the cloud

Tutorial: Build an OAuth2 protected REST API with node.js using Restify, mongodb, and Passport.js

Tutorial: Build an OAuth2 protected REST API with node.js using Restify, mongodb, and Passport.js

Rate This
  • Comments 3

 

This tutorial will give you with a quick and easy way to set up a REST API Service using the OAuth2 protocol in node.js. There has been a lot of questions in the community on how to build one of these, particularly how to add security to your endpoints after building a REST API server. There is a lot of code on GitHub, but nothing that walks you through the creation process from beginning to end for both the novice and the expert. I attempt to walk you through this carefully and provide just enough code so that you can branch out on your own.The sample server included in the download are designed to run on any platform. By the end of these walkthroughs, you should be able to build a running REST API server with the following features:

  • A node.js server running an REST API interface with JSON using MongoDB as persistant storage
  • REST APIs leveraging OAuth2 API protection for endpoints, for instance using Windows Azure Active Directory

I've released all of the source code for this running example in GitHub under an Apache 2.0 license, so feel free to clone (or even better, fork!) and provide feedback on the forums.

OAuth2 API Protection - Roll your own or use a service?

Although you can write your own identity layer that implements OAuth2 endpoints for tokens and authorization, I don't recommend this as there is a lot to consider from a security and identity perspective. For our purposes, we'll be using Windows Azure Active Directory to provide OAuth2 endpoint protection. It's really simple to setup and free! However, you don't need to use Windows Azure Active Directory to use this tutorial. The code here can be used to work against any OAuth2 provider. If you are interested in building your own OAuth2 server, a great start is the Restify-OAuth2 project.

About Node.js Modules

We will be using Node.js modules in this walkthrough. Modules are loadable JavaScript packages that provide specific functionality for your application. You usually install modules by using the Node.js NPM command-line tool in the NPM installation directory, but some modules, such as the HTTP module, are included the core Node.js package. Installed modules are saved in the node_modules directory at the root of your Node.js installation directory. Each module in the node_modules directory maintains its own node_modules directory that contains any modules that it depends on, and each required module has a node_modules directory. This recursive directory structure represents the dependency chain. This dependency chain structure results in a larger application footprint, but it guarantees that all dependencies are met and that the version of the modules used in development will also be used in production. This makes the production app behavior more predictable and prevents versioning problems that might affect users.

Step 1 (Optional): Register a Windows Azure AD Tenant

To use the OAuth2 API protection in this sample you will need a Windows Azure Active Directory Tenant. If you're not sure what a tenant is or how you would get one, see What is a Windows Azure AD tenant? and Sign up for Windows Azure as an organization to get you started with using Windows Azure AD.

NOTE: If you don't want to use Windows Azure Active Directory for OAuth2 endpoint protection, you can skip this step. It's really cool though, you should try it out.

Step 2 (Optional): Add A Web API to your tenant

After you get your Windows Azure AD tenant, add this API server to your tenant so you can use it to protect your API endpoints. If you need help with this step, see: Register the REST API Service Windows Azure Active Directory As part of registration you will be asked for your replyURL. For this walkthrough, you should have used http://localhost:8888/auth/provider/callback

 

NOTE: If you don't want to use Windows Azure Active Directory for OAuth2 endpoint protection, you can skip this step. It's really cool though, you should try it out.

Step 3: Download node.js for your platform

To successfully use this sample, you must have a working installation of Node.js. Install Node.js from http://nodejs.org.

Step 4: Install MongoDB on to your platform

To successfully use this sample, you must have a working installation of MongoDB. We will use MongoDB to make our REST API persistant across server instances. Install MongoDB from http://mongodb.org. NOTE: This walkthrough assumes that you use the default installation and server endpoints for MongoDB, which at the time of this writing is: mongodb://localhost

Step 5: Install the Restify modules in to your Web API

We will be using Resitfy to build our REST API. Restify is a minimal and flexible Node.js application framework derived from Express that has a robust set of features for building REST APIs on top of Connect.

Install Restify

From the command-line, change directories to the azuread directory. If the azuread directory does not exist, create it. cd azuread - or- mkdir azuread; cd azuread Type the following command: npm install restifyThis command installs Restify.
DID YOU GET AN ERROR?
When using npm on some operating systems, you may receive an error of Error: EPERM, chmod '/usr/local/bin/..' and a request to try running the account as an administrator. If this occurs, use the sudo command to run npm at a higher privilege level.
DID YOU GET AN ERROR REGARDING DTRACE?
You may see something like this when installing Restify:
clang: error: no such file or directory: 'HD/azuread/node_modules/restify/node_modules/dtrace-provider/libusdt' make: *** [Release/DTraceProviderBindings.node] Error 1 gyp ERR! build error gyp ERR! stack Error: `make` failed with exit code: 2 gyp ERR! stack at ChildProcess.onExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:267:23) gyp ERR! stack at ChildProcess.EventEmitter.emit (events.js:98:17) gyp ERR! stack at Process.ChildProcess._handle.onexit (child_process.js:789:12) gyp ERR! System Darwin 13.1.0 gyp ERR! command "node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild" gyp ERR! cwd /Volumes/Development HD/azuread/node_modules/restify/node_modules/dtrace-provider gyp ERR! node -v v0.10.11 gyp ERR! node-gyp -v v0.10.0 gyp ERR! not ok npm WARN optional dep failed, continuing dtrace-provider@0.2.8 
Restify provides a powerful mechanism to trace REST calls using DTrace. However, many operating systems do not have DTrace available. You can safely ignore these errors. The output of this command should appear similar to the following:
restify@2.6.1 node_modules/restify ├── assert-plus@0.1.4 ├── once@1.3.0 ├── deep-equal@0.0.0 ├── escape-regexp-component@1.0.2 ├── qs@0.6.5 ├── tunnel-agent@0.3.0 ├── keep-alive-agent@0.0.1 ├── lru-cache@2.3.1 ├── node-uuid@1.4.0 ├── negotiator@0.3.0 ├── mime@1.2.11 ├── semver@2.2.1 ├── spdy@1.14.12 ├── backoff@2.3.0 ├── formidable@1.0.14 ├── verror@1.3.6 (extsprintf@1.0.2) ├── csv@0.3.6 ├── http-signature@0.10.0 (assert-plus@0.1.2, asn1@0.1.11, ctype@0.5.2) └── bunyan@0.22.0 (mv@0.0.5) 

Step 6: Install Passport.js in to your Web API

Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based or Resitify web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more. We will use the OAuth2 strategy in our code. We will install this module and then add the OAuth2 strategy plug-in. From the command-line, change directories to the azuread directory. Enter the following command to install passport.js npm install passportThe output of the commadn should appear similar to the following:
passport@0.1.17 node_modules\passport ├── pause@0.0.1 └── pkginfo@0.2.3 

Step 7: Add Passport.js OAuth2 Support to your Web API

Next, we will add the OAuth strategy, using passport-oauth, a OAuth2 handler for Passport. We will also add JWT token handler support by using node-jwt and install support for Bearer tokens. NOTE: Although OAuth2 provides a framework in which any known token type can be issued, only certain token types have gained wide-spread use. For protecting endpoints, that has turned out to be Bearer tokens. Bearer tokens are the most widely used type of token in OAuth2, and many implementations assume that bearer tokens are the only type of token issued. From the command-line, change directories to the azuread directory. Type the following command to install Passport.js passport-azure-ad module: npm install passport-oauth npm install passport-http-bearer npm install node-jwtThe output of the commamd should appear similar to the following:
node_modules\passport-oauth ├── xtend@2.0.3 ├── xml-crypto@0.0.9 ├── xmldom@0.1.13 └── xml2js@0.1.14 (sax@0.5.2) 

Step 8: Add MongoDB modules to your Web API

We will be using MongoDB as our datastore For that reason, we need to install both the widely used plug-in to manage models and schemas called Mongoose, as well as the database driver for MongoDB, also called MongoDB.
  • npm install mongoose
  • npm install mongodb

Step 9: Install additional modules

Next, we'll install the remaining required modules. From the command-line, change directories to the azuread folder if not already there: cd azureadEnter the following commands to install the following modules in your node_modules directory:
  • npm install crypto
  • npm install assert-plus
  • npm install posix-getopt
  • npm install util
  • npm install path
  • npm install connect
  • npm install xml-crypto
  • npm install xml2js
  • npm install xmldom
  • npm install async
  • npm install request
  • npm install underscore
  • npm install grunt-contrib-jshint@0.1.1
  • npm install grunt-contrib-nodeunit@0.1.2
  • npm install grunt-contrib-watch@0.2.0
  • npm install grunt@0.4.1
  • npm install xtend@2.0.3
  • npm install bunyan
  • npm update

Step 10: Create a server.js with your dependencies

The server.js file will be providing the majority of our functionality for our Web API server. We will be adding most of our code to this file. For production purposes you would refactor the functionality in to smaller files, such as separate routes and controllers. For the purpose of this demo we will use server.js for this functionality. From the command-line, change directories to the azuread folder if not already there: cd azuread Create a server.js file in our favorite editor and add the following information:
 'use strict'; /** * Module dependencies. */ var fs = require('fs'); var path = require('path'); var util = require('util'); var assert = require('assert-plus'); var bunyan = require('bunyan'); var getopt = require('posix-getopt'); var mongoose = require('mongoose/'); var restify = require('restify');
Save the file. We will return to it shortly.

Step 11: Create a config file to store your OAuth2 Endpoint settings

This code file passes the configuration parameters for your OAuth2 server to Passport.js. If you used Windows Azure Active Directory, you created these configuration values when you added the Web API to the portal in the first part of the walkthrough. We will explain what to put in the values of these parameters after you've copied the code. From the command-line, change directories to the azuread folder if not already there: cd azuread Create a config.js file in our favorite editor and add the following information:
 exports.creds = { mongoose_auth_local: 'mongodb://localhost/tasklist', // Your mongo auth uri goes here token_endpoint: '', auth_endpoint: '', client_secret: '', // this is the Secret you generated when configuring your Web API for instance in Azure AAD loginCallback: '', // this is the Callback URI you entered for APP ID URI when configuring your Web API app, for instance on Azure AAD issuer: '', // this is the URI you entered for APP ID URI when configuring your Web API, for instance in Azure AAD client_id: '' // this is the Client ID you received after configuring your Web API app, for instance in Azure AAD // required options if using Azure AD 
 federation_metadata: '', // this is the metadata URL from the AAD Portal }
Required Windows Azure AD Values FederationMetadata: Enter the URL of the federation metadata document. To find the federation metadata document URL in the Windows Azure Management Portal, click Active Directory, click Integrated Apps, and in the black menu bar at the bottom of the page, click View endpoints. Then, copy the value of the Federation Metadata Document.
NOTE: We will add the code to parse the Federation Metadata in Step 13, known as aadutils.js.
TokenEndpoint This should familiar to you, and is the token endpoint of your authorization server you are using for validating credentials. This is the endpoint the server will call to retrieve the refresh and access tokens for use during authentication. If you are using Windows Azure Active Directory, to find the token endpoint URL in the Windows Azure Management Portal, click Active Directory, click Integrated Apps, and in the black menu bar at the bottom of the page, click View endpoints. Then, copy the value of the OAUTH 2.0 TOKEN ENDPOINT.
AuthEndpoint This should familiar to you, and is the authentication endpoint of your authorization server you are using for validating credentials and receiving an Auth Code which will be present to the token endpoint for an access and refresh token. If you are using Windows Azure Active Directory, to find the token endpoint URL in the Windows Azure Management Portal, click Active Directory, click Integrated Apps, and in the black menu bar at the bottom of the page, click View endpoints. Then, copy the value of the OAUTH 2.0 AUTHORIZATION ENDPOINT.
LoginCallback: Enter the physical address of your app. Windows Azure AD and other authentication servers sends the response for authenticated users to this URL. For this walkthrough, you should have used http://localhost:8888/auth/provider/callback. If you are using Windows Azure Active Directory, to find this value in the Windows Azure Management Portal, click Active Directory, click Integrated Apps, click your app, and click Configure. The App ID URI is at the bottom of the page in the Single Sign-On section. If you used a different LoginCallback when configuring your application in the previous steps, you can change it. See: Updating an App
issuer: Enter the App ID URI of your application, most likely http://localhost:8888/tasks. If you are using Windows Azure Active Directory, to find this value in the Windows Azure Management Portal, click Active Directory, click Integrated Apps, click your app, and click Configure. The App ID URI is at the bottom of the page in the Single Sign-On section. See: Updating an App
ClientID:  Unique identifier you provide to the authentication server to identify your server. If you are using Windows Azure Active Directory, this is the Client ID of your application which you saw in the portal when you first registered your application.
ClientSecret: The secret if you are using this flow for Confidential Client.

Step 12: Add configuration to your server.js file

We need to read these values from the Config file you just created across our application. To do this, we simply add the .config file as a required resource in our application and then set the global variables to those in the config.js document From the command-line, change directories to the azuread folder if not already there: cd azuread Open your server.js file in our favorite editor and add the following information:
var config = require('./config');
Then, add a new section to server.js with the following code:
/** * Setup some configuration */ var serverPort = process.env.PORT || 8888; var serverURI = ( process.env.PORT ) ? config.creds.mongoose_auth_mongohq : config.creds.mongoose_auth_local; var tokenEndpoint = config.creds.token_endpoint; var authEndpoint = config.creds.auth_endpoint; var fedEndpoint = config.creds.federation_metadata; var clientID = config.creds.client_id; var clientSecret = config.creds.client_secret; var callbackURL = config.creds.loginCallback;

Step 13: Create a aadutils.js helper file to aid in parsing metadata/tokens

  NOTE: If you'll never use Windows Azure AD, you can skip this step as you won't have a Federation Metadata Endpoint. Since the goal is to keep only application logic in the server.js file, it makes sense to put some helper methods in a separate file. These methods simply help us parse the Federation Metadata and do not relate to the core scenario. It's better to stuff them away. We will be adding more to this file as we go through the Walkthrough. From the command-line, change directories to the azuread folder if not already there: cd azuread Create a aadutils.js file in our favorite editor and add the following information:
var xml2js = require('xml2js'); var request = require('request'); var aadutils = require('./aadutils'); var async = require('async'); 'use strict'; exports.getElement = function (parentElement, elementName) { if (parentElement['saml:' + elementName]) { return parentElement['saml:' + elementName]; } else if (parentElement['samlp:'+elementName]) { return parentElement['samlp:'+elementName]; } return parentElement[elementName]; }; exports.getFirstElement = function (parentElement, elementName) { var element = null; if (parentElement['saml:' + elementName]) { element = parentElement['saml:' + elementName]; } else if (parentElement['samlp:'+elementName]) { element = parentElement['samlp:'+elementName]; } else { element = parentElement[elementName]; } return Array.isArray(element) ? element[0] : element; }; var Metadata = function (url) { if(!url) { throw new Error("Metadata: url is a required argument"); } this.url = url; this.metadata = null; }; Object.defineProperty(Metadata, 'url', { get: function () { return this.url; } }); Object.defineProperty(Metadata, 'saml', { get: function () { return this.saml; } }); Object.defineProperty(Metadata, 'wsfed', { get: function () { return this.wsfed; } }); Object.defineProperty(Metadata, 'oauth', { get: function () { return this.oauth; } }); Object.defineProperty(Metadata, 'metadata', { get: function () { return this.metadata; } }); exports.getElement = function (parentElement, elementName) { if (parentElement['saml:' + elementName]) { return parentElement['saml:' + elementName]; } else if (parentElement['samlp:'+elementName]) { return parentElement['samlp:'+elementName]; } return parentElement[elementName]; }; exports.getFirstElement = function (parentElement, elementName) { var element = null; if (parentElement['saml:' + elementName]) { element = parentElement['saml:' + elementName]; } else if (parentElement['samlp:'+elementName]) { element = parentElement['samlp:'+elementName]; } else { element = parentElement[elementName]; } return Array.isArray(element) ? element[0] : element; }; Metadata.prototype.updateSamlMetadata = function(doc, next) { try { this.saml = {}; var entity = aadutils.getElement(doc, 'EntityDescriptor'); var idp = aadutils.getElement(entity, 'IDPSSODescriptor'); var signOn = aadutils.getElement(idp[0], 'SingleSignOnService'); var signOff = aadutils.getElement(idp[0], 'SingleLogoutService'); var keyDescriptor = aadutils.getElement(idp[0], 'KeyDescriptor'); this.saml.loginEndpoint = signOn[0].$.Location; this.saml.logoutEndpoint = signOff[0].$.Location; // copy the x509 certs from the metadata this.saml.certs = []; for (var j=0;j<keyDescriptor.length;j++) { this.saml.certs.push(keyDescriptor[j].KeyInfo[0].X509Data[0].X509Certificate[0]); } next(null); } catch (e) { next(new Error('Invalid SAMLP Federation Metadata ' + e.message)); } }; Metadata.prototype.updateWsfedMetadata = function(doc, next) { try { this.wsfed = {}; var entity = aadutils.getElement(doc, 'EntityDescriptor'); var roles = aadutils.getElement(entity, 'RoleDescriptor'); for(var i = 0; i < roles.length; i++) { var role = roles[i]; if(role['fed:SecurityTokenServiceEndpoint']) { var endpoint = role['fed:SecurityTokenServiceEndpoint']; var endPointReference = aadutils.getFirstElement(endpoint[0],'EndpointReference'); this.wsfed.loginEndpoint = aadutils.getFirstElement(endPointReference,'Address'); var keyDescriptor = aadutils.getElement(role, 'KeyDescriptor'); // copy the x509 certs from the metadata this.wsfed.certs = []; for (var j=0;j<keyDescriptor.length;j++) { this.wsfed.certs.push(keyDescriptor[j].KeyInfo[0].X509Data[0].X509Certificate[0]); } break; } } return next(null); } catch (e) { next(new Error('Invalid WSFED Federation Metadata ' + e.message)); } }; Metadata.prototype.updateOAuthMetadata = function(doc, next) { try { this.oauth = {}; var entity = aadutils.getElement(doc, 'EntityDescriptor'); var roles = aadutils.getElement(entity, 'RoleDescriptor'); for(var i = 0; i < roles.length; i++) { var role = roles[i]; if(role['fed:SecurityTokenServiceEndpoint']) { var endpoint = role['fed:SecurityTokenServiceEndpoint']; var endPointReference = aadutils.getFirstElement(endpoint[0],'EndpointReference'); this.oauth.loginEndpoint = aadutils.getFirstElement(endPointReference,'Address'); var keyDescriptor = aadutils.getElement(role, 'KeyDescriptor'); // copy the x509 certs from the metadata this.oauth.certs = []; for (var j=0;j<keyDescriptor.length;j++) { this.oauth.certs.push(keyDescriptor[j].KeyInfo[0].X509Data[0].X509Certificate[0]); } break; } } return next(null); } catch (e) { next(new Error('Invalid OAuth Federation Metadata ' + e.message)); } }; Metadata.prototype.fetch = function(callback) { var self = this; async.waterfall([ // fetch the Federation metadata for the AAD tenant function(next){ request(self.url, function (err, response, body) { if(err) { next(err); } else if(response.statusCode !== 200) { next(new Error("Error:" + response.statusCode + " Cannot get AAD Federation metadata from " + self.url)); } else { next(null, body); } }); }, function(body, next){ // parse the AAD Federation metadata xml var parser = new xml2js.Parser({explicitRoot:true}); // Note: xml responses from Azure AAD have a leading \ufeff which breaks xml2js parser! parser.parseString(body.replace("\ufeff", ""), function (err, data) { self.metatdata = data; next(err); }); }, function(next){ // update the SAML SSO endpoints and certs from the metadata self.updateSamlMetadata(self.metatdata, next); }, function(next){ // update the SAML SSO endpoints and certs from the metadata self.updateWsfedMetadata(self.metatdata, next); }, function(next){ // update the SAML SSO endpoints and certs from the metadata self.updateOAuthMetadata(self.metatdata, next); } ], function (err) { // return err or success (err === null) to callback callback(err); }); }; exports.Metadata = Metadata;
As you can see from the code, it simply takes the FederationMetadata URL you passed in config.js and then parses it for information which we will use in the server.js file. You are more than welcome to investigate this code and expand it if needed.

Load the aadutils.js file in your server.js

We need to tell our server where to get the methods you jus wrote. From the command-line, change directories to the azuread folder if not already there: cd azuread Open your server.js file in our favorite editor and add the following information:
var aadutils = require('./aadutils');
Next, add to the end of the Configuration section this call to send the metadata document in our config.js to the parser we just wrote:
this.aadutils = new aadutils.Metadata(config.creds.federation_metadata);

Step 14: Add The MongoDB Model and Schema Information using Moongoose

Now all this preparation is going to start paying off as we wind these three files together in to a REST API service. For this walkthrough we will be using MongoDB to store our Tasks as discussed in Step 4. If you recall from the config.js file we created in Step 11 we called our database tasklist as that was what we put at the end of our mogoose_auth_local connection URL. You don't need to create this database beforehand in MongoDB, it will create this for us on first run of our server application (assuming it does not already exist). Now that we've told the server what MongoDB database we'd like to use, we need to write some additional code to create the model and schema for our server's Tasks.
Discussion of the model
Our Schema model is very simple, and you expand it as required. NAME - The name of who is assigned to the task. A String TASK - The task itself. A String DATE - The date that the task is due. A DATETIME COMPLETED - If the Task is completed or not. A BOOLEAN
Creating the schema in the code
From the command-line, change directories to the azuread folder if not already there: cd azuread Open your server.js file in our favorite editor and add the following information below the configuration entry:
/** * * Connect to MongoDB */ global.db = mongoose.connect(serverURI); var Schema = mongoose.Schema;
This will connect to the MongoDB server and hand back a Schema object to us.
Using the Schema, create our model in the code
Below the code you wrote above, add the following code:
/** / Here we create a schema to store our tasks. Pretty simple schema for now. */ var TaskSchema = new Schema({ name: String, task: String, completed: Boolean, date: Date }); // Use the schema to register a model mongoose.model('Task', TaskSchema); var Task = mongoose.model('Task');
As you can tell from the code, we create our Schema and then create a model object we will use to store our data throughout the code when we define our Routes.

Step 15: Add our Routes for our Task REST API server

Now that we have a database model to work with, let's add the routes we will use for our REST API server.

About Routes in Restify

Routes work in Restify in the exact same way they do using the Express stack. You define routes using the URI that you expect the client applicaitons to call. Usually, you define your routes in a separate file. For our purposes, we will put our routes in the server.js file. We recommend you factor these in to their own file for production use. A typical pattern for a Restify Route is:
function createObject(req, res, next) { // do work on Object _object.name = req.params.object; // passed value is in req.params under object ///... return next(); // keep the server going } 
....

server.post('/service/:add/:object', createObject); // calls createObject on routes that match this.
This is the pattern at the most basic level. Resitfy (and Express) provide much deeper functionaltiy such as defining application types and doing complex routing across different endpoints. For our purposes, we will keep these routes very simply.

Add default routes to our server

We will now add the basic CRUD routes of Create, Retrieve, Update, and Delete. From the command-line, change directories to the azuread folder if not already there: cd azuread Open your server.js file in our favorite editor and add the following information below the database entries you made above:

 

/** * * APIs */ function createTask(req, res, next) { // Resitify currently has a bug which doesn't allow you to set default headers // This headers comply with CORS and allow us to mongodbServer our response to any origin res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "X-Requested-With"); // Create a new task model, fill it up and save it to Mongodb var _task = new Task(); if (!req.params.task) { req.log.warn('createTask: missing task'); next(new MissingTaskError()); return; } /* if (Task.find(req.params.task)) { req.log.warn('%s already exists', req.params.task); next(new TaskExistsError(req.params.task)); return; }*/ _task.name = req.params.name; _task.task = req.params.task; _task.date = new Date(); _task.save(function (err) { if (err) { req.log.warn(err, 'createTask: unable to save'); next(err); } else { res.send(201, _task); } }); return next(); } /** * Deletes a Task by name */ function removeTask(req, res, next) { Task.remove( { task:req.params.task }, function (err) { if (err) { req.log.warn(err, 'removeTask: unable to delete %s', req.params.task); next(err); } else { res.send(204); next(); } }); } /** * Deletes all Tasks. A wipe */ function removeAll(req, res, next) { Task.remove(); res.send(204); return next(); } }); } /** * * * */ function getTask(req, res, next) { Task.find(req.params.name, function (err, data) { if (err) { req.log.warn(err, 'get: unable to read %s', req.params.name); next(err); return; } res.json(data); }); return next(); } /** * Simple returns the list of TODOs that were loaded. * */ function listTasks(req, res, next) { // Resitify currently has a bug which doesn't allow you to set default headers // This headers comply with CORS and allow us to mongodbServer our response to any origin res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "X-Requested-With"); console.log("server getTasks"); Task.find().limit(20).sort('date').exec(function (err,data) { if (err) return next(err); if (data.length > 0) { console.log(data); } if (!data.length) { console.log('there was a problem'); console.log(err); console.log("There is no tasks in the database. Did you initalize the database as stated in the README?"); } else { res.json(data); } }); return next(); }

 

Add some error handling for the routes

It makes sense to add some error handling so we can communicate back to the client the problem we encountered in a way it can understand. Add the following code underneath the code you've written above:

///--- Errors for communicating something interesting back to the client function MissingTaskError() { restify.RestError.call(this, { statusCode: 409, restCode: 'MissingTask', message: '"task" is a required parameter', constructorOpt: MissingTaskError }); this.name = 'MissingTaskError'; } util.inherits(MissingTaskError, restify.RestError); function TaskExistsError(name) { assert.string(name, 'name'); restify.RestError.call(this, { statusCode: 409, restCode: 'TaskExists', message: name + ' already exists', constructorOpt: TaskExistsError }); this.name = 'TaskExistsError'; } util.inherits(TaskExistsError, restify.RestError); function TaskNotFoundError(name) { assert.string(name, 'name'); restify.RestError.call(this, { statusCode: 404, restCode: 'TaskNotFound', message: name + ' was not found', constructorOpt: TaskNotFoundError }); this.name = 'TaskNotFoundError'; } util.inherits(TaskNotFoundError, restify.RestError);

Step 16: Create your Server!

We have our database defined, we have our routes in place, and the last thing to do is add our server instance that will manage our calls. Restify (and Express) have a lot of deep customization you can do for a REST API server, but again we will use the most basic setup for our purposes.

/**
 * Our Server
 */

var server = restify.createServer({
        name: "Windows Azure Active Directroy TODO Server",
    version: "1.0.0",
    formatters: {
        'application/json': function(req, res, body){
            if(req.params.callback){
                var callbackFunctionName = req.params.callback.replace(/[^A-Za-z0-9_\.]/g, '');
                return callbackFunctionName + "(" + JSON.stringify(body) + ");";
            } else {
                return JSON.stringify(body);
            }
        },
        'text/html': function(req, res, body){
            if (body instanceof Error)
                        return body.stack;

                      if (Buffer.isBuffer(body))
                        return body.toString('base64');

                return util.inspect(body);
        },
        'application/x-www-form-urlencoded': function(req, res, body){
            if (body instanceof Error) {
                    res.statusCode = body.statusCode || 500;
                    body = body.message;
            } else if (typeof (body) === 'object') {
                body = body.task || JSON.stringify(body);
            } else {
                body = body.toString();
            }

        res.setHeader('Content-Length', Buffer.byteLength(body));
        return (body);
        }
    }
});

        // Ensure we don't drop data on uploads
        server.pre(restify.pre.pause());

        // Clean up sloppy paths like //todo//////1//
        server.pre(restify.pre.sanitizePath());

        // Handles annoying user agents (curl)
        server.pre(restify.pre.userAgentConnection());

        // Set a per request bunyan logger (with requestid filled in)
        server.use(restify.requestLogger());

        // Allow 5 requests/second by IP, and burst to 10
        server.use(restify.throttle({
                burst: 10,
                rate: 5,
                ip: true,
        }));

        // Use the common stuff you probably want
        server.use(restify.acceptParser(server.acceptable));
        server.use(restify.dateParser());
        server.use(restify.queryParser());
        server.use(restify.gzipResponse());

        /// Now the real handlers. Here we just CRUD

        server.get('/tasks', listTasks);
        server.head('/tasks', listTasks);
        server.get('/tasks/:name', getTask);
        server.head('/tasks/:name', getTask);
        server.post('/tasks/:name/:task', createTask);
        server.del('/tasks/:name/:task', removeTask);
        server.del('/tasks/:name', removeTask);
        server.del('/tasks', removeAll, function respond(req, res, next) { res.send(204); next(); });

        // Register a default '/' handler

        server.get('/', function root(req, res, next) {
                var routes = [
                        'GET     /',
                        'POST    /tasks/:name/:task',
                        'GET     /tasks',
                        'PUT     /tasks/:name',
                        'GET     /tasks/:name',
                        'DELETE  /tasks/:name/:task'
                ];
                res.send(200, routes);
                next();
        });

  server.listen(serverPort, function() {

  var consoleMessage = '\n Windows Azure Active Directory Tutorial'
  consoleMessage += '\n +++++++++++++++++++++++++++++++++++++++++++++++++++++' 
  consoleMessage += '\n %s server is listening at %s';
  consoleMessage += '\n Open your browser to %s/tasks\n';
  consoleMessage += '+++++++++++++++++++++++++++++++++++++++++++++++++++++ \n'
  consoleMessage += '\n !!! why not try a $curl -isS %s | json to get some ideas? \n'
  consoleMessage += '+++++++++++++++++++++++++++++++++++++++++++++++++++++ \n\n'  

  console.log(consoleMessage, server.name, server.url, server.url, server.url);

});

Step 17: Before we add OAuth support, let's run the server.

It's a good idea to make sure we have no mistakes befor we continue on to the OAuth part of the Walkthrough. The easiest way to do this is by using curl in a command line. Before we do that, we need a simple utility that allows us to parse output as JSON. To do that, install the json tool as all the examples below use that.
$npm install -g jsontool
This installs the JSON tool globally.
Now that we've accomplished that - let's play with the server:
First, make sure that your monogoDB isntance is running..
$sudo mongod
Then, change to the directory and start curling..
$ cd azuread $ node server.js $ curl -isS http://127.0.0.1:8888 | json
HTTP/1.1 200 OK
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 145
Date: Wed, 29 Jan 2014 03:41:24 GMT
[ "GET /",
"POST /tasks/:name/:task",
"GET /tasks",
"DELETE /tasks",
"PUT /tasks/:name",
"GET /tasks/:name",
"DELETE /tasks/:task" ]
Then, we can add a task this way:
$ curl -isS -X POST http://127.0.0.1:8888/tasks/brandon/Hello
The response should be:
HTTP/1.1 201
Created Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: X-Requested-With Content-Type: application/x-www-form-urlencoded
Content-Length: 5
Date: Tue, 04 Feb 2014 01:02:26 GMT
Hello
And we can list tasks for Brandon this way:
$ curl -isS http://127.0.0.1:8888/tasks/brandon/
And we can remove tasks for Brandon this way:
$ curl -isS -X DELETE http://127.0.0.1:8888/tasks/brandon/Hello
If all this works out, we are ready to add OAuth to the REST API server.

Step 18: Add Passport.js code to our REST API Server Now that we have a running REST API (congrats, btw!) let's get to making it useful against Windows Azure AD. From the command-line, change directories to the azuread folder if not already there: cd azuread

Step 1: Add our Passport modules

Open your server.js file in our favorite editor and add the following information below where you previously stated the modules to load. This is towards the top of the file and should be right after the 
 
 
 
 
 
 
 
var aadutils = require('./aadutils');
/* * * Load our old friend Passport for OAuth2 flows */ var passport = require('passport') , OAuth2Strategy = require('passport-oauth').OAuth2Strategy;

Step 2. Tell our server we are using authentication

Open your server.js file in our favorite editor and add the following information below the server.get() where you defined your Routes but above the server.listen() method. We need to tell Restify to begin using its authorizationParser() and look at the contents of the Authorization header.
 server.use(restify.authorizationParser()); server.use(restify.bodyParser({ mapParams: false }));

Step 3. Add the Passport OAuth2 module to our code

Here we use the specific OAuth2 parameters we added to the config.js file. If our aadutils.js file did its job parsing our Federation Metadata document, all these values should be populated for us even if they are blank in the config.js file. If you are not using Windows Azure AD, it will be up to you to put these values in the config.js file yourself and ensure they work.
// Now our own handlers for authentication/authorization // Here we only use Oauth2 from Passport.js passport.use('provider', new OAuth2Strategy({ authorizationURL: authEndpoint, tokenURL: tokenEndpoint, clientID: clientID, clientSecret: clientSecret, callbackURL: callbackURL }, function(accessToken, refreshToken, profile, done) { User.findOrCreate({ UserId: profile.id }, function(err, user) { done(err, user); }); } )); 
// Let's start using Passport.js 
server.use(passport.initialize());

Step 4: Add Routes for OAuth authentication

// Redirect the user to the OAuth 2.0 provider for authentication. When // complete, the provider will redirect the user back to the application at // /auth/provider/callback server.get('/auth/provider', passport.authenticate('provider')); // The OAuth 2.0 provider has redirected the user back to the application. // Finish the authentication process by attempting to obtain an access // token. If authorization was granted, the user will be logged in. // Otherwise, authentication has failed. server.get('/auth/provider/callback', passport.authenticate('provider', { successRedirect: '/', failureRedirect: '/login' }));

Step 5: Add a IsAuthenticated() Helper Method to the Routes

// Simple route middleware to ensure user is authenticated. // Use this route middleware on any resource that needs to be protected. If // the request is authenticated (typically via a persistent login session), // the request will proceed. Otherwise, the user will be redirected to the // login page. var ensureAuthenticated = function(req, res, next) { if (req.isAuthenticated()) { return next(); } res.redirect('/login'); };

Step 6: Add a caching mechnaism for the cookies

// Passport session setup. // To support persistent login sessions, Passport needs to be able to // serialize users into and deserialize users out of the session. Typically, // this will be as simple as storing the user ID when serializing, and finding // the user by ID when deserializing. passport.serializeUser(function(user, done) { done(null, user.email); }); passport.deserializeUser(function(id, done) { findByEmail(id, function (err, user) { done(err, user); }); });

Step 7: Finally, protect some endpoint protection

You protect endpoints by specifying the passport.authenticate() call with the protocol you wish to use. Let's edit our route in our server code to do something more interesting:

server.get('/tasks', passport.authenticate('provider', { session: false }), listTasks);

Step 19: Run your server application again and ensure it rejects you

Let's use curl again to see if we now have OAuth2 protection against our endpoints. We will do this before runnning any of our client SDKs against this endpoint. The headers returned should be enough to tell us we are down the right path. First, make sure that your monogoDB isntance is running..

$sudo mongod

Then, change to the directory and start curling..

$ cd azuread $ node server.js

Try a basic GET:

$ curl -isS http://127.0.0.1:8888/tasks/

HTTP/1.1 302 Moved Temporarily Connection: close Location: https://login.windows.net/cff56d8f-f602-4afd-94e4-c95b76f1c81e/oauth2/authorize?api-version=1.0?response_type=code&redirect_uri=hhttp%3A%2F%2Flocalhost%3A8888%2Fauth%2Fprovider%2Fcallback&client_id=ee5c9d0f-fd45-48d1-9823-9cd370ce15b4 Content-Length: 0 Date: Sun, 09 Feb 2014 03:50:38 GMT

A 302 is the response you are looking for here, as that indicates that the Passport layer is trying to redirect to the authorize endpoint, which is exactly what you want.

Congratulations! You have a REST API Service using OAuth2!

You've went as far as you can with this server without using an OAuth2 compatible client. You will need to go through an additional walkthrough. If you were just looking for information on how to implement a REST API using Restify and OAuth2, you have more than enough code to keep developing your service and learning how to build on this example. If you are interested in the next steps in your Windows Azure Active Directory journey, here are some supported ADAL clients we recommend for you to keep working: ADAL for iOS ADAL for Android ADAL for .Net  

About Brandon

About Brandon

 

I’m Brandon Werner, a software engineer in Seattle. I love good friends, good coffee, and good ideas shared around a room.
 

I work for Microsoft in the Windows Azure team helping design the next generation cloud identity platform for people and businesses around the world. I lead the design of the SDKs for all Non-Microsoft platforms for this effort, including node.js, Java, PHP on the server and Android and iOS on devices. I also drive the open sourcing of Microsoft protocol and identity libraries and work to modernize the development experience for developers at Microsoft.

I joined Microsoft in 2008 as part of the scrappy team to build the competitor to Google Apps, which became Office365.

You can follow me on Twitter.

  • Ya it is working fine but i cant call the api it show "Unauthorized" how should call that means browser query....

  • Thanks for the awesome tutorial!

    I've been trying to piece this process together from various resources across the web.

    Do you have the link to the github repo you mentioned at the start of the article?

  • Here it is: github.com/.../azure-sdk-for-node-restapi-sample

Page 1 of 1 (3 items)