Contents


Mastering MEAN

Managing authentication with OAuth and Passport

Content series:

This content is part # of # in the series: Mastering MEAN

Stay tuned for additional content in this series.

This content is part of the series:Mastering MEAN

Stay tuned for additional content in this series.

The User Group List and Information (UGLI) app is beginning to take shape nicely. You can now show the local content that you create via the CRUD screens that you set up in "MEAN and UGLI CRUD with responsive web design." You can also incorporate content from an external site by using the services you developed in "MEAN meets Meetup.com and microdata."

Sharing meeting information with the general public is an important part of this project. But as a user-group leader, I also want to limit some activities to registered members of the group. For example, I might choose to preserve some civility in commenting on our presentations, by turning off anonymous access and requiring a login. So, in this installment, you'll use Meetup.com's OAuth service to provide login capabilities to the UGLI app. See Download to get the sample code.

Creating a new user account

By clicking the Sign up button (shown in Figure 1), your app's users can create a new account that's stored locally in MongoDB. This functionality is built in to your app — no additional programming required.

Figure 1. UGLI sign-up page
Screenshot of the UGLI sign-up page
Screenshot of the UGLI sign-up page

This default behavior is certainly the easiest solution from a development perspective, but it leaves something to be desired in terms of the user experience. Your user-group members already have an account on Meetup.com that they use to RSVP for upcoming meetings. Asking them to create and maintain a duplicate set of credentials isn't only annoying — it's a blatant violation of the Don't Repeat Yourself (DRY) principle.

Luckily, with the MEAN stack you're using, you can set up a distributed authentication and authorization solution that uses OAuth and Passport. In simple terms, your users can log in (authenticate) to the UGLI app with the same credentials they use to log in to Meetup.com. But this can't be done without users' permission; they must allow (authorize) the UGLI app to use their Meetup.com credentials.

Even after the UGLI app is authorized via OAuth, the user credentials aren't shared with the authorized app. You don't store a duplicate set of user names and passwords locally in UGLI. The application hoping to be authorized (UGLI) redirects users to the OAuth provider (Meetup.com), where they provide their credentials (user name and password). After a user authenticates successfully, an access token is returned to the authorized application.

This scheme cuts a huge swath of code and logic out of your application. You no longer need to worry about storing encrypted passwords on your server — that's now the OAuth provider's problem to deal with. Similarly, you no longer must write algorithms to enforce strong passwords, or deal with forgotten passwords, or force users to change their passwords on a periodic basis.

So, OAuth gives your users fewer passwords to memorize and gives you significantly less code to write. If that's not the textbook definition of a win/win scenario, I don't know what is.

Introducing OAuth and Passport

You no longer need to write algorithms to enforce strong passwords, or deal with forgotten passwords, or force users to change their passwords on a periodic basis.

OAuth is an open standard for distributed authentication and authorization. It was developed in 2006 by Twitter and business partner Ma.gnolia to facilitate the creation of desktop widgets that display information from authenticated services. Since then, OAuth has been adopted by hundreds of major websites, from Google to Facebook to Twitter, GitHub, LinkedIn, and more. (See the List of notable OAuth service providers.)

Passport is an OAuth library written for Node.js. Specifically, it is middleware meant to be seamlessly incorporated with Express applications. More than 140 Passport plugins (called strategies) are available, tailor-made for each OAuth provider.

If you open the UGLI app's package.json file in a text editor (as shown in Listing 1), you can see Passport strategies for four major services — Facebook, Twitter, LinkedIn, and Google — along with a local strategy for storing credentials directly in MongoDB.

Listing 1. Passport strategies in package.json
"dependencies": {
    "passport": "~0.2.0",
    "passport-local": "~1.0.0",
    "passport-facebook": "~1.0.2",
    "passport-twitter": "~1.0.2",
    "passport-linkedin": "~0.1.3",
    "passport-google-oauth": "~0.1.5"
}

Using the existing examples as a guide, you'll add a sixth strategy to incorporate Meetup.com as an OAuth provider for UGLI.

Installing the Meetup.com Passport strategy

If you visit Authenticating with the Meetup API, you'll see that Meetup.com offers OAuth services. A quick web search for meetup.com passport.js strategy yields a link to the library you're looking for: passport-meetup.

Type npm install passport-meetup --save to download the library to node_modules and update the dependencies block in package.json.

That was the easy part. With the strategy in place, your next step is to incorporate it into the sign-up and sign-in pages.

Adding a Meetup.com link to the sign-up and sign-in pages

Open public/modules/users/views/signup.client.view.html in a text editor. At the top of the file, you can see links to the various OAuth providers (as shown in Listing 2).

Listing 2. public/modules/users/views/signup.client.view.html
<h3 class="col-md-12 text-center">Sign up using your social accounts</h3>
<div class="col-md-12 text-center">
    <a href="/auth/facebook" class="undecorated-link">
        <img src="/modules/users/img/buttons/facebook.png">
    </a>
    <a href="/auth/twitter" class="undecorated-link">
        <img src="/modules/users/img/buttons/twitter.png">
    </a>
    <a href="/auth/google" class="undecorated-link">
        <img src="/modules/users/img/buttons/google.png">
    </a>
    <a href="/auth/linkedin" class="undecorated-link">
        <img src="/modules/users/img/buttons/linkedin.png">
    </a>
</div>

Replace the existing links with one that points to your yet-to-be-created /auth/meetup route and displays the yet-to-be-downloaded Meetup.com icon (as shown in Listing 3).

Listing 3. Link to auth/meetup
<h3 class="col-md-12 text-center">Sign up using your Meetup.com account</h3>
<div class="col-md-12 text-center">
    <a href="/auth/meetup" class="undecorated-link">
        <img src="/modules/users/img/buttons/meetup.png">
    </a>
</div>

Visit the Meetup Icon page and save the 128x128 pixel image to public/modules/users/img/buttons/, where the other social media icons are stored.

Now that the sign-up page is stubbed out, open public/modules/users/views/signin.client.view.html in a text editor and adjust it the same way you did for the sign-up page (as shown in Listing 4).

Listing 4. public/modules/users/views/signin.client.view.html
<h3 class="col-md-12 text-center">Sign in using your Meetup.com account</h3>
<div class="col-md-12 text-center">
    <a href="/auth/meetup" class="undecorated-link">
        <img src="/modules/users/img/buttons/meetup.png">
    </a>
    </div>

If everything goes as planned, your new sign-up page looks like Figure 2. Of course without the route in place, you'll get a 404 Page Not Found error if you click the link. You'll fix this next.

Figure 2. New UGLI sign-up page
Screenshot of the new UGLI sign-up page
Screenshot of the new UGLI sign-up page

Setting up the server-side auth/meetup routes

The next step is to create the server-side auth/meetup routes. Recall that all server-side logic is stored in the app directory; client-side logic is stored in the public folder.

Open app/routes/users.server.routes.js in a text editor. Find the block of code for Facebook and copy/paste it, replacing facebook with meetup (as shown in Listing 5).

Listing 5. app/routes/users.server.routes.js
// Setting the facebook oauth routes
app.route('/auth/facebook').get(passport.authenticate('facebook', {
    scope: ['email']
}));
app.route('/auth/facebook/callback').get(users.oauthCallback('facebook'));

// Setting the meetup oauth routes
app.route('/auth/meetup').get(passport.authenticate('meetup', {
    scope: ['email']
}));
app.route('/auth/meetup/callback').get(users.oauthCallback('meetup'));

Remember the hyperlinks to auth/meetup that you created on the sign-up and sign-in pages in the previous section? The first route (auth/meetup) will be triggered when the user sends an HTTP GET request to the server by clicking the link. Passport will try to authenticate the user by using the passport-meetup strategy. The results of the login attempt (successful or otherwise) will be sent to the second auth/meetup/callback route asynchronously.

If you click the Meetup link on the sign-up page now, you get a 500 Server Error instead of a 404. That's not exactly an improvement, but at least it's progress. Next up: configuring the Meetup strategy.

Configuring the Meetup strategy

You can find all of the Passport strategies in the eponymous config/strategies directory. Copy facebook.js to meetup.js, then open meetup.js in a text editor.

As you did in the previous section, you'll go through this file and replace all instances of facebook with meetup. But this is more than a simple find/replace operation. You also need to make some minor configuration changes.

To start, change the required library at the top of the file from the Facebook strategy to the Meetup one (as shown in Listing 6).

Listing 6. config/strategies/meetup.js
/**
 * Module dependencies.
 */
var passport = require('passport'),
    url = require('url'),
    MeetupStrategy = require('passport-meetup').Strategy,
    config = require('../config'),
    users = require('../../app/controllers/users');

Next, you need to customize the options block that gets passed into the new strategy. These values vary from strategy to strategy. Listing 7 shows the Facebook strategy options, which won't work for Meetup.

Listing 7. Facebook options that won't work for Meetup
module.exports = function() {
    // Use facebook strategy
    passport.use(new FacebookStrategy({
            clientID: config.facebook.clientID,
            clientSecret: config.facebook.clientSecret,
            callbackURL: config.facebook.callbackURL,
            passReqToCallback: true
        },

Thankfully, the passport-meetup module that you npm installed earlier shipped with example code. Open node_modules/passport-meetup/examples/login/app.js in a text editor. Look for the passport.use function call (shown in Listing 8).

Listing 8. node_modules/passport-meetup/examples/login/app.js
passport.use(new MeetupStrategy({
    consumerKey: MEETUP_KEY,
    consumerSecret: MEETUP_SECRET,
    callbackURL: "http://127.0.0.1:3000/auth/meetup/callback"
  },

Copy this snippet over to meetup.js, overwriting the Facebook code. Next, change the values on the right side of the colon to those shown in Listing 9.

Listing 9. Meetup options that will work
passport.use(new MeetupStrategy({
    consumerKey: config.meetup.consumerKey,
    consumerSecret: config.meetup.consumerSecret,
    callbackURL: config.meetup.callbackURL,
    },

In the next section, you'll get the consumerKey and consumerSecret from Meetup.com and save them in the config.js file. But you need to make a couple of additional changes to the current file before you go.

The function immediately following the new MeetupStrategy constructor is the event handler that receives the response from Meetup.com. You are interested in three key pieces of the response: the access token, the refresh token, and the user profile. (See the OAuth access tokens and refresh tokens sidebar for details about the tokens.)

The access token and refresh token are strings that you pass through, unaltered, to Passport. Although they're crucial to the success of the OAuth operation, they're pretty boring to look at on their own. (Listing 10 includes examples of both.)

The user profile is more interesting. It's a JSON object returned by the OAuth provider that contains information about the user who successfully authenticated. The specific details will vary from OAuth provider to OAuth provider. Listing 10 shows an example of the user profile returned by Meetup.

Listing 10. User profile returned from Meetup.com OAuth provider
{ 
  provider: 'meetup',
  id: 13848777,
  displayName: 'Scott Davis',
  _raw: '{ 
      "results": [{
        "status": "active",
        "link": "http:\\\/\\\/www.meetup.com\\\/members\\\/13848777",
        "photo": {
          "photo_link": "http:\\\/\\\/photos1.meetupstatic.com\\\/photos\\\/member\\\/7\\\/4\\\/d\\\/2\\
\/member_11849906.jpeg",
          "thumb_link": "http:\\\/\\\/photos3.meetupstatic.com\\\/photos\\\/member\\\/7\\\/4\\\/d\\\/2\\
\/thumb_11849906.jpeg",
          "photo_id": 11849906
        },
        "country": "us",
        "state": "CO",        
        "city": "Denver",
        "id": 13848777,
        "joined": 1295844957000,
        "bio": "Scott Davis is the founder of ThirstyHead.com, a training and 
consulting company that specializes in leading-edge technology solutions like 
HTML 5, NoSQL, Groovy, and Grails.",
        "name": "Scott Davis",
        "other_services": {
          "twitter": {
            "identifier": "@scottdavis99"
          }
        }
      }]  
   }',
  _json: { 
     results: [ [Object] ],
     meta: { 
        link: 'https://api.meetup.com/2/members',
        total_count: 1,
        url: 'https://api.meetup.com/2/members?order=name&member_id=13848777&offset=0
&format=json&page=800',
        title: 'Meetup Members v2',
        updated: 1392763702000,
        description: 'API method for accessing members of Meetup Groups',
        method: 'Members',
     },
     accessToken: 'c7b5577bb80aab55439785cd86abcdef',
     refreshToken: '2af98db68950235a1e2519a734abcdef' 
  } 
}

As you can see, Meetup returns user details such as name, place of residence, date joined, profile photos, linked social media accounts, and so on.

The final thing you need to do to complete the customization of the Meetup strategy is to map the Meetup profile fields back onto the User Mongoose object defined in app/models/user.server.model.js. Edit the remaining Facebook block in config/strategies/meetup.js, as shown in Listing 11.

Listing 11. Mapping the OAuth user profile to the User object
// Create the user OAuth profile
var providerUserProfile = {
    firstName: '',
    lastName: '',
    displayName: profile.displayName,
    email: '',
    username: profile.id,
    provider: profile.provider,
    providerIdentifierField: 'id',
    providerData: providerData
};

If you see fields in the Meetup profile JSON that you'd like to add to the User object, this is the perfect time to make your changes. Don't forget to add the new fields to the HTML forms in public/modules/users/views.

Your finished config/strategies/meetup.js should look like Listing 12.

Listing 12. The complete config/strategies/meetup.js
'use strict';

/**
 * Module dependencies.
 */
var passport = require('passport'),
    url = require('url'),
    MeetupStrategy = require('passport-meetup').Strategy,
    config = require('../config'),
    users = require('../../app/controllers/users');

module.exports = function() {
    // Use meetup strategy
    passport.use(new MeetupStrategy({
        consumerKey: config.meetup.clientID,
        consumerSecret: config.meetup.clientSecret,
        callbackURL: config.meetup.callbackURL,
        },
        function(req, accessToken, refreshToken, profile, done) {
            // Set the provider data and include tokens

            var providerData = profile._json;
            providerData.accessToken = accessToken;
            providerData.refreshToken = refreshToken;

            // Create the user OAuth profile
            var providerUserProfile = {
                firstName: '',
                lastName: '',
                displayName: profile.displayName,
                email: '',
                username: profile.id,
                provider: profile.provider,
                providerIdentifierField: 'id',
                providerData: providerData
            };
            
            // Save the user OAuth profile
            users.saveOAuthUserProfile(req, providerUserProfile, done);
        }
    ));
};

Before you can test this code, you need to do one more thing: Get the consumerKey and consumerSecret from Meetup.

Getting the Meetup consumerKey and consumerSecret

I've spent the entire article so far talking about authenticating the user. But before users can use OAuth to log in to UGLI, you (the developer) must provide proof that your organization is what it claims to be. You do this by providing a public key (the consumerKey) to the user. Your application also needs to know what its private key (the consumerSecret) is.

If you've worked with Public Key Infrastructure (PKI) before, you know that it's crucial to keep your private key hidden and secure. If someone else discovers your private key, they can masquerade as your organization. Conversely, if you don't share your public key with users, they can't prove who you are.

If you are the organizer of a user group on Meetup.com, you can generate the consumerKey and consumerSecret from Meetup's Your OAuth Consumers page (see Figure 3). I used HTML5 Denver User Group for the Consumer Name, http://www.meetup.com/HTML5-Denver-Users-Group/ for the Application Website, and http://localhost:3000/auth/meetup/callback for the Redirect URI. After I push the UGLI app into production, I'll change the Application Website to http://html5denver.com and the Redirect URI to http://html5denver.com/auth/meetup/callback.

Figure 3. Generating the consumerKey and consumerSecret for HTML5 Denver
Screenshot of generating the consumerKey and consumerSecret for HTML5 Denver
Screenshot of generating the consumerKey and consumerSecret for HTML5 Denver

If you don't run a user group on Meetup.com, your account isn't authorized to generate OAuth keys on behalf of a group. However, you can still generate tokens for one of your other social media accounts and adjust the steps in this article accordingly. See Implementing Sign in with Twitter to generate application keys for your Twitter account or Access Tokens to use your Facebook account. A quick web search on your social media website oauth keys should yield step-by-step instructions.

Adding the organization OAuth keys to your application

After you have the two keys (public and private), you'll add them to your application via environment variables — much as you changed the PORT in "Tour a MEAN application."

Recall that you can set variables that change based on the mode you are running in: development, production, or test. Environment-specific values are stored in config/env. Open config/env/development.js in a text editor. Copy/paste the Facebook block and adjust it accordingly for Meetup (as shown in Listing 13). Be sure that the attribute names here match the attribute names you used in the passport.use function call in config/strategies/meetup.js.

Listing 13. config/env/development.js
'use strict';

module.exports = {
    db: 'mongodb://localhost/test-dev',
    app: {
        title: 'HTML5 Denver'
    },

    meetup: {
        consumerKey: process.env.MEETUP_KEY || 'APP_ID',
        consumerSecret: process.env.MEETUP_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/meetup/callback'
    },    
    facebook: {
        clientID: process.env.FACEBOOK_ID || 'APP_ID',
        clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/facebook/callback'
    },
    twitter: {
        clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
        clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
        callbackURL: 'http://localhost:3000/auth/twitter/callback'
    },
    google: {
        clientID: process.env.GOOGLE_ID || 'APP_ID',
        clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/google/callback'
    },
    linkedin: {
        clientID: process.env.LINKEDIN_ID || 'APP_ID',
        clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
        callbackURL: 'http://localhost:3000/auth/linkedin/callback'
    }
};

You could replace APP_ID and APP_SECRET with hardcoded values for the consumerKey and consumerSecret that you retrieved in the previous section. But a more secure solution is to provide these values to the UGLI application via environment variables. To start your application with your organization's consumerKey and consumerSecret, type:

MEETUP_KEY=l75fkklhurkack36eelfhhfhjc MEETUP_SECRET=abcdeg316jd3ni43f21u1abcde NODE_ENV=development grunt

Don't forget to make similar adjustments to config/env/production.js before you go live. And if you created a user account earlier, be sure to delete it from the html5-denver-dev database in MongoDB so that you can walk through the new-account creation process all over again.

Conclusion

Larry Wall (creator of the Perl programming language) famously said, "Easy things should be easy, and hard things should be possible." I hope this sentiment nicely summarizes your experience getting OAuth and Passport wired up to use Meetup.com for your distributed authentication and authorization needs.

In the next Mastering MEAN installment, I'll walk you through the testing infrastructure built into the MEAN stack. You'll learn about Mocha for server-side testing, Jasmine for client-side testing, and Karma for running your tests across multiple browsers. Until then, have fun mastering MEAN.


Downloadable resources


Related topics

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Open source
ArticleID=1000509
ArticleTitle=Mastering MEAN: Managing authentication with OAuth and Passport
publish-date=03132015