Bootcamp Notes – Day 14 (Tues) – NodeJS: Week 4: Backend Services – Cross-Origin Resource Sharing & OAuth and User Authentication

Cross-Origin Resource Sharing & OAuth and User Authentication

 

Overview of Cross-Origin Resource Sharing

 

SAME-ORIGIN POLICY

Webpages often pull in data from many different sites. For security sake, modern browsers impose a same-origin policy that restricts how documents/scripts from one origin interact with resources from another origin. This prevents a malicious script on one page from accessing sensitive data on another page.

WHAT IS AN ORIGIN?

The origin is a combination of:

  • URI scheme = first part of a URI that defines the protocol, eg. HTTP, HTTPS, FTP, etc (Note: Slight semantic difference but can be use more or less interchangeably with the word protocol)
  • hostname
  • port number

Two resources must match on all three of these (URI scheme, hostname, port number) in order to be considered of the same origin.

The following table gives examples of origin comparisons with the URL http://store.company.com/dir/page.html:

URL Outcome Reason
http://store.company.com/dir2/other.html Same origin Only the path differs
http://store.company.com/dir/inner/another.html Same origin Only the path differs
https://store.company.com/page.html Failure Different protocol
http://store.company.com:81/dir/page.html Failure Different port (http:// is port 80 by default)
http://news.company.com/dir/page.html Failure Different host

CROSS-ORIGIN RESOURCE SHARING (CORS)

When a webpage requires resources from another page that’s not of the same origin, it could be blocked dur to browser’s same-origin policy. In that case, you can explicitly allow a resource from another origin to be shared through CORS from another origin to be shared and that is what CORS is all about.

The CORS standard sets up a mechanism to give web servers the ability to control access to cross-domain resources. The browser and the server will interact to determine whether it’s safe to allow a cross-origin request.

CORS introduced a new set of HTTP response headers which allows servers to send CORS related information to browsers. The most common CORS related information is:

Access-Control-Allow-Origin which lets the browser know if certain origin is allowed to access a resource.

There are several CORS headers used for different purposes such as:

  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Headers
  • Access-Control-Max-Age
  • and more!

SIMPLE REQUESTS

There are two main types of cross-origin requests that can be made: simple requests and pre-flighted requests.

Simple requests are considered to be safe and do not need any extra security measures, but they must fulfill a set of conditions. One is that it must use only GET, POST, HEAD methods.

If it is a POST request, then it can only be one of these three:

  1. application/x-www-form-urlencoded
  2. multipart/form-data
  3. text/plain

Also, with a simple request, custom headers are not allowed and only certain predefined headers are allowed to be manually set, including: Content-Language, Viewport-Width and a few others that are deemed to be safe headers. The CORS standard allows for making simple requests without too much trouble. For example, if a browser sends a GET request to a particular endpoint on a server to fetch some data, the server may wish to set that GET endpoint as acceptable, no matter where the requests originates. In that case the server could send a CORS header to the browser saying; Access-Control-Allow-Origin with wildcard value (*), this tells browser requests from any origin are OK, allow access despite same-origin policy.You can also specifically set a white list of origins to accept if you like.

PREFLIGHTED REQUESTS

PREFLIGHTED REQUESTS (PUT, DELETE) are cross-origin requests that do not meet simple criteria and are considered less safe as they can cause changes in an existing server data. When a browser wants to make a pre-flighted request to a server, it first sends what is called a pre-flight request to a server before actual request. This pre-flight request is made using HTTP method called OPTIONS along with CORS request headers with information about the actual request the client wishes to send for example if the actual request is a delete method then the pre-flight request would include the header Access-Control-Request-Method with the value of delete. Then the server will look at this pre-flight request and decide whether or not to allow the client to send the actual request. If so, then it will send back a status code of 200, along with CORS response headers to give the client information about the server’s course settings, such as: Access-Control-Allow-Origin, Access-Control-Allow-Methods. The client can then decide from this response, whether to send the actual request.


Cross-Origin Resource Sharing Example

  • Install and configure the cors Node module .
  • Update your Express application to support CORS on various endpoints.

Install CORS module

  • To install the cors module, type the following at the prompt:   npm install cors@2.8.5

Configure the server for CORS

  • In the routes folder, add a new file named cors.js and add the following code to it:
const cors = require('cors');

const whitelist = ['http://localhost:3000', 'https://localhost:3443'];
const corsOptionsDelegate = (req, callback) => {
    let corsOptions;
    console.log(req.header('Origin'));
    if(whitelist.indexOf(req.header('Origin')) !== -1) {
        corsOptions = { origin: true };
    } else {
        corsOptions = { origin: false };
    }
    callback(null, corsOptions);
};

exports.cors = cors();
exports.corsWithOptions = cors(corsOptionsDelegate);
  • Then, open campsiteRouter.js and update it as follows:
. . .
const cors = require('./cors');
. . .

campsiteRouter.route('/')
.options(cors.corsWithOptions, (req, res) => res.sendStatus(200))
.get(cors.cors, (req, res, next) => {
. . .
.post(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res, next) => {
. . .
.put(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res) => {
. . .
.delete(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res, next) => {

. . .

campsiteRouter.route('/:campsiteId')
.options(cors.corsWithOptions, (req, res) => res.sendStatus(200))
.get(cors.cors, (req, res, next) => {
. . .
.post(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res) => {
. . .
.put(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res, next) => {
. . .
.delete(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res, next) => {
. . .

campsiteRouter.route('/:campsiteId/comments')
.options(cors.corsWithOptions, (req, res) => res.sendStatus(200))
.get(cors.cors, (req, res, next) => {
. . .
.post(cors.corsWithOptions, authenticate.verifyUser, (req, res, next) => {
. . .
.put(cors.corsWithOptions, authenticate.verifyUser, (req, res) => {
. . .
.delete(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res, next) => {
. . .

campsiteRouter.route('/:campsiteId/comments/:commentId')
.options(cors.corsWithOptions, (req, res) => res.sendStatus(200))
.get(cors.cors, (req, res, next) => {
. . .
.post(cors.corsWithOptions, authenticate.verifyUser, authenticate.verifyAdmin, (req, res) => {
. . .
.put(cors.corsWithOptions, authenticate.verifyUser, (req, res, next) => {
. . .
.delete(cors.corsWithOptions, authenticate.verifyUser, (req, res, next) => {

. . .
  • Make similar updates to these routers: promotionRouter.jspartnerRouter.jsuploadRouter.js, and users.js. That is:
    • Import the cors module that you created to each.
    • Add a preflight request with the .options method to every route for promotionRouter.jspartnerRouter.js, and uploadRouter.js.
    • To all four files, add either the cors.cors middleware to the routing methods for GET requests, or the cors.corsWithOptions middleware to the routing methods for POSTPUSH, and DELETE requests. Exception: In users.js, all methods should use cors.corsWithOptions.
  • Test your server with Postman.
  • Optional: Commit your changes to Git with the message “CORS”.

 


Overview of OAuth and User Authentication

THIRD PARTY AUTHENTICATION

Many websites and mobile apps let you log in with trust third party authentication service providers, such as Google, Facebook, Twitter, Github, and more.

Main authentication protocols are OAuth 2.0 and OpenID. We will focus on OAuth 2.0

OAuth 1 – first protocol to support third party authentication (Evolved from Twitter – IETF RFC 5849, now deprecated)

OAuth 2 – evolved from OAuth 1, simpler on server & client side – IETF RFC 6749 and 6750

We will learn hot to leverage these for user authentication within a web or mobile app. We will be using Facebook as an example.

OAUTH 2

In OAuth 2 a commonly used term is called “resource ownership”, meaning resource means user accounts, the identity of the user.

The Express server will need to contact Facebook’s resource server so the Express server becomes a client making requests.

Does not contact the resource server directly, but will go through Facebook’s OAuth server.

You can think of this as similar to how browsers can contact our express web server and then our web server deals with the MongoDB server. The browser does not contact the MongoDB server directly. But goes through the web server.

Facebook & OAuth – Implicit Grant Flow

There are several different ways that we can use OAuth, known as authorization flows. The way that we will be using is called the implicit grant flow. To begin you need to Register your server application with Facebook. Once registered, Facebook will provide you two random strings called an App ID and App Secret.

Your server can then provide App ID to front end applications such as a React web app so that the user’s browser can access it usually through a button that says login with Facebook or something like that. When the user clicks on this, then if they are not already logging into Facebook they will be prompted to log in. Once they are logged in then the front end app will be able to send a request to the Facebook Oauth server that contains your server’s App ID.

The Facebook’s OAuth server will then validate the APP ID and respond to the front end app with an access token and the front-end app will then pass that access token Express server saying “Here I have this token from Facebook, that I can use to log in.”

Finally, your Express server send the APP ID, APP SECRET and access token to Facebook’s OAuth server. If everything is valid, then the OAuth server grants the Express server access to the resource server to obtain the user’s profile data. If this operation is successful, then the Express server will use this profile data to either create a local user account for that user or if there already is an account locally, it will log them in. Then it will generate a JSON web token that it sends to the user’s client and this is what the user will use to access resources on the Express server from then on.

There is also one other token called a refresh token, that can be used to refresh an expired access token.

Tokens must be sent over HTTPS! Tokens must be encrypted securely so users identities cannot be stolen.

We will be using passport-facebook-token strategy, which implements the implicit grant approach, that we just described.


Using OAuth with Passport and Facebook Example

  • Learn how to configure your server to support user authentication based on a third party OAuth provider.
  • Use Passport’s OAuth support through the passport-facebook-token module to support OAuth-based authentication with Facebook for your users.

Exercise Resources

index.zip (contains index.html)

Instructions

Register your app on Facebook

  • Make sure you have logged into the account you wish to use on Facebook.
  • Go to https://developers.facebook.com/apps/ 
  • If you are redirected to https://developers.facebook.com, that means you will need to register your Facebook account as a developer.
    • Click the “Get Started” link at page upper right.
    • This will guide you through the developer registration process.
    • Once finished, you will be at the Apps page for developers.
  • Once at the Apps page:
    • Click the Create App button.
    • You’ll be asked to choose an App Type. Choose “Build Connected Experiences“.
    • Next, you will be asked to give an app name and to verify your email address. You can name the app “nucampsite” and verify that your email address is correct. You can leave the optional field regarding a Business Manager account as-is, which should be at the “No Business Manager Account selected” option.
    • Click Create App. Your app will then be given an App ID and App Secret. The App Secret can be accessed through Settings -> Basic. You can copy down your App ID and App Secret, or just remember you can access them here when needed.
    • In Settings -> Basic, add https://localhost:3443 to App Domains.
    • Scroll to the bottom and click the Add Platform button. Select “Website” then for the Site URL, add https://localhost:3443 once more. Click Save Changes.
    • Go to Settings -> Advanced and click on Yes to say Native/Desktop app, then Save Changes.

Configure index.html

  • Download the index.zip file provided above. Extract the index.html file from it and move it into the public folder, replacing the index.html file that’s already there.
  • In the index.html file, replace where it says YOUR FACEBOOK APP ID with the Facebook App ID that you obtained for your application, inside the quotes.

Install passport-facebook-token module

  • In the nucampsiteServer folder, install the passport-facebook-token module by typing the following at the prompt:  npm i passport-facebook-token@4.0.0

Update config.js

  • Update config.js with the App ID and App Secret that you obtained earlier as follows:
module.exports = {
    'secretKey': '12345-67890-09876-54321',
    'mongoUrl': 'mongodb://localhost:27017/nucampsite',
    'facebook': {
        clientId: 'YOUR FACEBOOK APP ID HERE',
        clientSecret: 'YOUR FACEBOOK APP SECRET HERE'
    }
}
  • If you are using Git, add config.js to your .gitignore file so that you do not inadvertently push it to a public repository.  That means you need to open your .gitignore file, then add a line that says:
config.js

The next time you add and commit to your git repository, the .gitignore file should prevent the update to your config.js file from being added, so that your App Secret remains secret. If you then git push to your online repository, be sure to confirm that the config.js file with the App Secret was not published. This is best practice so that your App Secret is not made public. You can read Facebook’s warning against this here.

Update user model

  • Open user.js in the models folder and update the userSchema as follows:
const userSchema = new Schema({
  . . .

    facebookId: String,

  . . .
});

Set up Facebook authentication strategy

  • Open authenticate.js and add in the following line to add Facebook strategy:
. . .

const FacebookTokenStrategy = require('passport-facebook-token');

. . .

exports.facebookPassport = passport.use(
    new FacebookTokenStrategy(
        {
            clientID: config.facebook.clientId,
            clientSecret: config.facebook.clientSecret
        }, 
        (accessToken, refreshToken, profile, done) => {
            User.findOne({facebookId: profile.id}, (err, user) => {
                if (err) {
                    return done(err, false);
                }
                if (!err && user) {
                    return done(null, user);
                } else {
                    user = new User({ username: profile.displayName });
                    user.facebookId = profile.id;
                    user.firstname = profile.name.givenName;
                    user.lastname = profile.name.familyName;
                    user.save((err, user) => {
                        if (err) {
                            return done(err, false);
                        } else {
                            return done(null, user);
                        }
                    });
                }
            });
        }
    )
);

Update users.js

  • Open users.js and add the following code to it:
. . .

router.get('/facebook/token', passport.authenticate('facebook-token'), (req, res) => {
    if (req.user) {
        const token = authenticate.getToken({_id: req.user._id});
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json({success: true, token: token, status: 'You are successfully logged in!'});
    }
});
. . .

Test

  • Start your server and test your application.
  • In a browser, open https://localhost:3443/ to open the index.html file. (You may need to go to the index.html file explicitly: https://localhost:3443/index.html). Then click on the Facebook Login button to log into Facebook. At the end of the login process, open your browser’s JavaScript console and then obtain the Access Token from there.
  • Then you can use the access token to contact the server at https://localhost:3443/users/facebook/token and pass in the token using the Authorization header with the value as Bearer to obtain the JWT token from the server.
  • You can also try two other ways to trade the access token from Facebook with a JWT token from the Express server, detailed in the testing section of the video for this lesson.
  •  Also try using the Mongo REPL shell to verify that an account has been created with a FacebookId in the user collection of the nucampsite database.
  • Optional: Save all the changes and make a Git commit with the message “Passport Facebook”.
    • NOTE: If you are pushing this Git repository to an online repository such as GitHub, take steps to conceal your App Secret so that it is not available publicly. To do this, add config.js to your .gitignore file before you add and commit the updates from this exercise.An alternate strategy, if you want to be able to commit the config.js file without the App Secret, is to create a new file that exports a const variable holding the App Secret, add that new file to .gitignore, and use the variable imported from that file in config.js.

Additional Resurces:

You May Also Like