Providing Twitter login support for the Notes application

If you want your application to hit the big time, it’s a great idea to ease the registration process by using third-party authentication. Websites all over the internet allow you to log in using accounts from other services such as Facebook or Twitter.

Doing so removes hurdles to prospective users signing up for your service. Passport makes it extremely easy to do this.

Authenticating users with Twitter requires installation of TwitterStrategy from the passport-twitter package, registering a new application with Twitter, adding a couple of routes to routes/user.mjs, and making a small change in partials/header.hbs. Integrating other third-party services requires similar steps.

1. Registering an application with Twitter

Twitter, as with every other third-party service, uses OAuth to handle authentication. OAuth is a standard protocol through which an application or a person can authenticate with one website by using credentials they have on another website. We use this all the time on the internet. For example, we might use an online graphics application such as draw.io or Canva by logging in with a Google account, and then the service can save files to our Google Drive.

Any application author must register with any sites you seek to use for authentication. Since we wish to allow Twitter users to log in to Notes using Twitter credentials, we have to register our Notes application with Twitter. Twitter then gives us a pair of authentication keys that will validate the Notes application with Twitter. Any application, whether it is a popular site such as Canva, or a new site such as Joe’s Ascendant Horoscopes, must be registered with any desired OAuth authentication providers. The application author must then be diligent about keeping the registration active and properly storing the authentication keys.

The authentication keys are like a username/password pair. Anyone who gets a hold of those keys could use the service as if they were you, and potentially wreak havoc on your reputation or business.

Our task in this section is to register a new application with Twitter, fulfilling whatever requirements Twitter has.

As you go through this process, you may be shown the following message:

Recall that in recent years, concerns began to arise regarding the misuse of third-party authentication, the potential to steal user information, and the negative results that have occurred thanks to user data being stolen from social networks. As a result, social networks have increased scrutiny over developers using their APIs. It is necessary to sign up for a Twitter developer account, which is an easy process that does not cost anything.

As we go through this, realize that the Notes application needs a minimal amount of data. The ethical approach to this is to request only the level of access required for your application, and nothing more.

Once you’re registered, you can log in to developer.twitter.com/apps and see a dashboard listing the active applications you’ve registered. At this point, you probably do not have any registered applications. At the top is a button marked Create an App. Click on that button to start the process of submitting a request to register a new application.

Every service offering OAuth authentication has an administrative backend similar to developer.twitter.com/apps. The purpose is so that certified application developers can administer the registered applications and authorization tokens. Each such service has its own policies for validating that those requesting authorization tokens have a legitimate purpose and will not abuse the service. The authorization token is one of the mechanisms to verify that API requests come from approved applications. Another mechanism is the URL from which API requests are made.

In the normal case, an application will be deployed to a regular server, and is accessed through a domain name such as MyNotes.xyz. In our case, we are developing a test application on our laptop, and do not have a public IP address, nor is there a domain name associated with our laptop. Not all social networks allow interactions from an application on an untrusted computer—such as a developer’s laptop—to make API requests; however, Twitter does.

At the time of writing, there are several pieces of information requested by the Twitter sign-up process, listed as follows:

  • Name: This is the application name, and it can be anything you like. It would be a good form to use “Test” in the name, in case Twitter’s staff decide to do some checking.
  • Description: Descriptive phrase—and again, it can be anything you like. The description is shown to users during the login process. It’s good form to describe this as a test application.
  • Website: This would be your desired domain name. Here, the help text helpfully suggests If you don’t have a URL yet, just put a placeholder here but remember to change it later.
  • Allow this application to be used to sign in with Twitter: Check this, as it is what we want.
  • Callback URL: This is the URL to return to following successful authentication. Since we don’t have a public URL to supply, this is where we specify a value referring to your laptop. It’s been found that http://localhost:3000 works just fine. macOS users have another option because of the .local domain name that is automatically assigned to their laptop.
  • Tell us how this app will be used: This statement will be used by Twitter to evaluate your request. For the purpose of this project, explain that it is a sample app from a book. It is best to be clear and honest about your intention.

The sign-up process is painless. However, at several points, Twitter reiterated the sensitivity of the information provided through the Twitter API. The last step before granting approval warned that Twitter prohibits the use of its API for various unethical purposes.

The last thing to notice is the extremely sensitive nature of the authentication keys. It’s bad form to check these into a source code repository or otherwise put them in a place where anybody can access the key. We’ll tackle this issue in Chapter 14, Security in Node.js Applications.

2. Storing authentication tokens

The Twitter recommendation is to store configuration values in a .env file. The contents of this file are to somehow become environment variables, which we can then access using process.env, as we’ve done before. Fortunately, there is a third- party Node.js package to do just this, called dotenv.

First, install the package, as follows:

$ npm install dotenv@8.2.x –save 

The documentation says we should load the dotenv package and then call dotenv.config() very early in the start up phase of our application, and that we must do this before accessing any environment variables. However, reading the documentation more closely, it seems best to add the following code to app.mjs:

import dotenv from ‘dotenv/config.js’;

With this approach, we do not have to explicitly call the dotenv.config function. The primary advantage is avoiding issues with referencing environment variables from multiple modules.

The next step is to create a file, .env, in the notes directory. The syntax of this file is very simple, as shown in the following code block:

VARIABLE1=value for variable 1

VARIABLE2=value for variable 2

This is exactly the syntax we’d expect since it is the same as for shell scripts. In this file, we need two variables to be defined, TWITTER_CONSUMER_KEY and TWITTER_CONSUMER_SECRET. We will use these variables in the code we’ll write in the next section. Since we are putting configuration values in the scripts section of package.json, feel free to add those environment variables to .env as well.

The next step is to avoid committing this file to a source code control system such as Git. To ensure that this does not happen, you should already have a .gitignore file in the notes directory, and make sure its contents are something like this:

notes-fs-data

notes.level

chap07.sqlite3

notes-sequelize.sqlite3

package-lock.json

data node_modules

.env

These values mostly refer to database files we generated in the previous chapter. In the end, we’ve added the .env file, and because of this, Git will not commit this file to the repository.

This means that when deploying the application to a server, you’ll have to arrange to add this file to the deployment without it being committed to a source repository.

With an approved Twitter application, and with our authentication tokens recorded in a configuration file, we can move on to adding the required code to Notes.

3. Implementing TwitterStrategy

As with many web applications, we have decided to allow our users to log in using Twitter credentials. The OAuth protocol is widely used for this purpose and is the basis for authentication on one website using credentials maintained by another website.

The application registration process you just followed at developer.twitter.com generated for you a pair of API keys: a consumer key, and a consumer secret. These keys are part of the OAuth protocol and will be supplied by any OAuth service you register with, and the keys should be treated with the utmost care. Think of them as the username and password your service uses to access the OAuth-based service (Twitter et al.). The more people who can see these keys, the more likely it becomes that a miscreant can see them and then cause trouble. Anybody with those secrets can access the service API as if they are you.

Let’s install the package required to use TwitterStrategy, as follows:

$ npm install passport-twitter@1.x –save 

In routes/users.mjs, let’s start making some changes, as follows:

import passportTwitter from ‘passport-twitter’;

const TwitterStrategy = passportTwitter.Strategy;

This imports the package, and then makes its Strategy variable available as TwitterStrategy.

Let’s now install the TwitterStrategy, as follows:

const twittercallback = process.env.TWITTER_CALLBACK_HOST

? process.env.TWITTER_CALLBACK_HOST

: “http://localhost:3000”;

export var twitterLogin;

if (typeof process.env.TWITTER_CONSUMER_KEY !== ‘undefined’

&& process.env.TWITTER_CONSUMER_KEY !== ”

&& typeof process.env.TWITTER_CONSUMER_SECRET !== ‘undefined’

&& process.env.TWITTER_CONSUMER_SECRET !== ”) {

passport.use(new TwitterStrategy({

consumerKey: process.env.TWITTER_CONSUMER_KEY,

consumerSecret: process.env.TWITTER_CONSUMER_SECRET,

callbackURL: ‘${twittercallback}/users/auth/twitter/callback’

},

async function(token, tokenSecret, profile, done) {

try {

done(null, await usersModel.findOrCreate({

id: profile.username, username: profile.username, password: “”,

provider: profile.provider, familyName: profile.displayName, givenName: “”, middleName: “”,

photos: profile.photos, emails: profile.emails

}));

} catch(err) { done(err); }

}));

twitterLogin = true;

} else {

twitterLogin = false;

}

This registers a TwitterStrategy instance with passport, arranging to call the user authentication service as users register with the Notes application. This callback function is called when users successfully authenticate using Twitter.

If the environment variables containing the Twitter tokens are not set, then this code does not execute. Clearly, it would be an error to set up Twitter authentication without the keys, so we avoid the error by not executing the code.

To help other code know whether Twitter support is enabled, we export a flag variable – twitterLogin.

We defined the usersModel.findOrCreate function specifically to handle user registration from third-party services such as Twitter. Its task is to look for the user described in the profile object and, if that user does not exist, to create that user account in Notes.

The callbackURL setting in the TwitterStrategy configuration is a holdover from Twitter’s OAuth1-based API implementation. In OAuth1, the callback URL was passed as part of the OAuth request. Since TwitterStrategy uses Twitter’s OAuth1 service, we have to supply the URL here. We’ll see in a moment where that URL is implemented in Notes.

The callbackURL, consumerKey, and consumerSecret settings are all injected using environment variables. Earlier, we discussed how it is a best practice to not commit the values for consumerKey and consumerSecret to a source repository, and therefore we set up the dotenv package and a .env file to hold those configuration values. In Chapter 10, Deploying Node.js Applications to Linux Servers, we’ll see that these keys can be declared as environment variables in a Dockerfile.

Add the following route declaration:

router.get(‘/auth/twitter’, passport.authenticate(‘twitter’));

To start the user logging in with Twitter, we’ll send them to this URL. Remember that this URL is really /users/auth/twitter and, in the templates, we’ll have to use that URL. When this is called, the passport middleware starts the user authentication and registration process using TwitterStrategy.

Once the user’s browser visits this URL, the OAuth dance begins. It’s called a dance because the OAuth protocol involves carefully designed redirects between several websites. Passport sends the browser over to the correct URL at Twitter, where Twitter asks the user whether they agree to authenticate using Twitter, and then Twitter redirects the user back to your callback URL. Along the way, specific tokens are passed back and forth in a very carefully designed dance between websites.

Once the OAuth dance concludes, the browser lands at the URL designated in the following router declaration:

router.get(‘/auth/twitter/callback’,

passport.authenticate(‘twitter’, { successRedirect: ‘/’,

failureRedirect: ‘/users/login’ }));

This route handles the callback URL, and it corresponds to the callbackURL setting configured earlier. Depending on whether it indicates a successful registration, Passport will redirect the browser to either the home page or back to the /users/login page.

Because router is mounted on /user, this URL is actually /user/auth/twitter/callback. Therefore, the full URL to use in configuring the TwitterStrategy, and to supply to Twitter, is http://localhost:3000/user/auth/twitter/callback.

In the process of handling the callback URL, Passport will invoke the callback function shown earlier. Because our callback uses the usersModel.findOrCreate function, the user will be automatically registered if necessary.

We’re almost ready, but we need to make a couple of small changes elsewhere in Notes.

In partials/header.hbs, make the following changes to the code:

{{else}}

<div class=”collapse navbar-collapse” id=”navbarLogIn”>

<span class=”navbar-text text-dark col”></span>

<a class=”nav-item nav-link btn btn-dark col-auto” href=”/users/login”>

Log in</a>

{{#if twitterLogin}}

<a class=”nav-item nav-link btn btn-dark col-auto” href=”/users/auth/twitter”>

<img width=”15px” src=”/assets/vendor/twitter/Twitter_SocialIcon

_Rounded_Square_Color.png”/> Log in with Twitter</a>

{{/if}}

</div>

{{/if}}

This adds a new button that, when clicked, takes the user to /users/auth/twitter, which—of course—kicks off the Twitter authentication process. The button is enabled only if Twitter support is enabled, as determined by the twitterLogin variable. This means that the router functions must be modified to pass in this variable.

In routes/index.mjs, make the following change:

import { twitterLogin } from ‘./users.mjs’;

router.get(‘/’, async (req, res, next) => {

res.render(‘index’, {

title: ‘Notes’, notelist: notelist,

user: req.user ? req.user : undefined,

twitterLogin: twitterLogin

});

});

This imports the variable, and then, in the data passed to res.render, we add this variable. This will ensure that the value reaches partials/header.hbs.

In routes/notes.mjs, we have a similar change to make in several router functions:

import { twitterLogin } from ‘./users.mjs’;

router.get(‘/add’, ensureAuthenticated, (req, res, next) => {

res.render(‘noteedit’, {

… twitterLogin: twitterLogin, …

});

});

router.get(‘/view’, (req, res, next) => {

res.render(‘noteview’, {

… twitterLogin: twitterLogin, …

});

});

router.get(‘/edit’, ensureAuthenticated, (req, res, next) => {

res.render(‘noteedit’, {

… twitterLogin: twitterLogin, …

});

});

router.get(‘/destroy’, ensureAuthenticated, (req, res, next) => {

res.render(‘notedestroy’, {

… twitterLogin: twitterLogin, …

});

});

This is the same change, importing the variable and passing it to res.render. With these changes, we’re ready to try logging in with Twitter.

Start the user information server as shown previously, and then start the Notes application server, as shown in the following code block:

$ npm start

> notes@0.0.0 start /Users/David/chap08/notes

> DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=sequelize USER_SERVICE_URL=http://localhost:5858 node — experimental-modules ./app.mjs 

notes:server-debug Listening on port 3000 +0ms 

Then, use a browser to visit http://localhost:3000, as follows:

Notice the new button. It looks about right, thanks to having used the official Twitter branding image. The button is a little large, so maybe you want to consult a designer. Obviously, a different design is required if you’re going to support dozens of authentication services.

Run it while leaving out the Twitter token environment variables, and the Twitter login button should not appear.

Clicking on this button takes the browser to /users/auth/twitter, which is meant to start Passport running the OAuth protocol transactions necessary to authenticate. Instead, you may receive an error message that states Callback URL not approved for this client application. Approved callback URLs can be adjusted in your application settings. If this is the case, it is necessary to adjust the application configuration on developer.twitter.com. The error message is clearly saying that Twitter saw a URL being used that was not approved.

On the page for your application, on the App Details tab, click the Edit button. Then, scroll down to the Callback URLs section and add the following entries:

As it explains, this box lists the URLs that are allowed to be used for Twitter OAuth authentication. At the moment, we are hosting the application on our laptop using port 3000. If you are accessing it from other base URLs, such as http://MacBook-Pro-4.local, then that base URL should be used in addition.

Once you have the callback URLs correctly configured, clicking on the Login with Twitter button will take you to a normal Twitter OAuth authentication page. Simply click for approval, and you’ll be redirected back to the Notes application.

And then, once you’re logged in with Twitter, you’ll see something like the following screenshot:

We’re now logged in, and will notice that our Notes username is the same as our Twitter username. You can browse around the application and create, edit, or delete notes. In fact, you can do this to any note you like, even ones created by others. That’s because we did not create any sort of access control or permissions system, and therefore every user has complete access to every note. That’s a feature to put on the backlog.

By using multiple browsers or computers, you can simultaneously log in as different users, one user per browser.

You can run multiple instances of the Notes application by doing what we did earlier, as follows:

“scripts”: {

“start”: “cross-env DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml

NOTES_MODEL=models/notes-sequelize USERS_MODEL=models/users-rest

USER_SERVICE_URL=http://localhost:5858 node ./bin/www”,

“start-server1”: “SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml

NOTES_MODEL=models/notes-sequelize USERS_MODEL=models/users-rest

USER_SERVICE_URL=http://localhost:5858 PORT=3000 node ./bin/www”,

“start-server2”: “SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml

NOTES_MODEL=models/notes-sequelize USERS_MODEL=models/users-rest

USER_SERVICE_URL=http://localhost:5858 PORT=3002 node ./bin/www”,

“dl-minty”: “mkdir -p minty && npm run dl-minty-css && npm run dl- minty-min-css”,

“dl-minty-css”: “wget https://bootswatch.com/4/minty/bootstrap.css

-O minty/bootstrap.css”,

“dl-minty-min-css”: “wget https://bootswatch.com/4/minty/bootstrap.min.css -O minty/bootstrap.min.css”

},

Then, in one command window, run the following command:

$ npm run start-server1

 > notes@0.0.0 start-server1 /Users/David/chap08/notes

> DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=sequelize USER_SERVICE_URL=http://localhost:5858 PORT=3000 node –experimental-modules ./app.mjs

notes:server-debug Listening on port 3000 +0ms 

In another command window, run the following command:

$ npm run start-server2

> notes@0.0.0 start-server2 /Users/David/chap08/notes

> DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=sequelize USER_SERVICE_URL=http://localhost:5858 PORT=3002 node –experimental-modules ./app.mjs

 notes:server-debug Listening on port 3002 +0ms

As previously, this starts two instances of the Notes server, each with a different value in the PORT environment variable. In this case, each instance will use the same user authentication service. As shown here, you’ll be able to visit the two instances at http://localhost:3000 and http://localhost:3002. As before, you’ll be able to start and stop the servers as you wish, see the same notes in each, and see that the notes are retained after restarting the server.

Another thing to try is to fiddle with the session store. Our session data is being stored in the sessions directory. These are just files in the filesystem, and we can take a look with normal tools such as ls, as shown in the following code block:

$ ls -l sessions/ total 32

-rw-r–r– 1 david wheel 139 Jan 25 19:28 –

QOS7eX8ZBAfmK9CCV8Xj8v-3DVEtaLK.json

-rw-r–r– 1 david wheel 139 Jan 25 21:30

T7VT4xt3_e9BiU49OMC6RjbJi6xB7VqG.json

-rw-r–r– 1 david wheel 223 Jan 25 19:27

ermh-7ijiqY7XXMnA6zPzJvsvsWUghWm.json

-rw-r–r– 1 david wheel 139 Jan 25 21:23

uKzkXKuJ8uMN_ROEfaRSmvPU7NmBc3md.json $ cat

sessions/T7VT4xt3_e9BiU49OMC6RjbJi6xB7VqG.json

{“cookie”:{“originalMaxAge”:null,”expires”:null,”httpOnly”:true,”path”

:”/”},” lastAccess”:1516944652270,”passport”:{“user”:”7genblogger”}} 

This is after logging in using a Twitter account. You can see that the Twitter account name is stored here in the session data.

What if you want to clear a session? It’s just a file in the filesystem. Deleting the session file erases the session, and the user’s browser will be forcefully logged out.

The session will time out if the user leaves their browser idle for long enough. One of the session-file-store options, ttl, controls the timeout period, which defaults to 3,600 seconds (an hour). With a timed-out session, the application reverts to a logged-out state.

In this section, we’ve gone through the full process of setting up support for login using Twitter’s authentication service. We created a Twitter developer account and created an application on Twitter’s backend. Then, we implemented the required workflow to integrate with Twitter’s OAuth support. To support this, we integrated the storage of user authorizations from Twitter in the user information service.

Our next task is extremely important: to keep user passwords encrypted.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *