Click to View this Presentation
Note: This lesson is not broken into distinct 1:15 modules. It is designed to be spread throughout a day...
-
Identify the advantages OAuth provides for users and web apps
-
Explain what happens when a user clicks "Login with [OAuth Provider]"
-
Add OAuth authentication to an Express app using PassportJS
-
Use Middleware & PassportJS to provide authorization
- Why OAuth?
- What is OAuth?
- How Does OAuth Work?
- Preview the App
- The App's User Stories
- Review the Starter Code
- Today's Game Plan (11 Steps)
-
In Unit 2, we used Django's built-in username/password authentication.
-
The users of your project had to sign up and log in using a username and a password.
-
What are the pitfalls of username/password authentication from a user's perspective?
Pitfalls from a user prospective:
-
Creating multiple logins requires you to remember and manage all of those login credentials.
-
You will often use the same credentials across multiple sites, so if there's a security breach at one of the sites where you are a member, the hackers know that users often use the same credentials across all of their sites - oh snap!
-
You are tempted to use simple/weak passwords so that you can remember all of them.
What would be the pitfalls from a
business or developer's perspective?
Pitfalls from a website or developer prospective:
-
Managing users' credentials requires carefully crafted security code written by highly-paid devs.
-
Users (customers) are annoyed by having to create dedicated accounts, especially for entertainment or personal interest type websites.
-
Managing credentials makes your business a target for hackers (internal and external) and that brings with it liability.
-
The bottom-line is that the majority of users prefer to use OAuth instead of creating another set of credentials to use your site.
-
When users are your customers, you want to make them as happy as possible!
-
OAuth is hot, so let's use it!
- OAuth provider: A service company such as Google that makes its OAuth authentication service available to third-party applications.
- client application: Our web application! Remember, this is from an OAuth provider's perspective.
- owner: A user of a service such as Facebook, Google, Dropbox, etc.
- resources: An owner's information on a service that may be exposed to client applications. For example, a user of Dropbox may allow access to their files.
- access token: An temporary key that provides access to an owner's resources.
- scope: Determines what resources and rights (read-only, update, etc) a particular token has.
-
OAuth is an open standard that provides client applications access to resources of a service such as Google with the permission of the resources' owner.
-
There are numerous OAuth Providers including:
- GitHub
- Many more...
-
The ultimate goal is for the client application (our server app, not the browser app) to obtain an access token from an OAuth provider that allows the app to access the user's resources from that server's API's.
-
Important: We will only want access to the most basic of resources the user could grant us - their name and email. This is all our app typically cares about, unless it is designed to work with a user's Facebook friends, tweets, Dropbox data, etc.
-
Interestingly, with OAuth, it technically is not required for an application to persist its users in a database. That's right, no user model!
-
However, in most cases web applications want to persist its users in a database because:
- The web app will want to persist additional information about its users not provided by the OAuth provider, for example, storing a user's preferences when using an app.
- They want to track the number of users their app has, and perhaps their usage frequency, etc.
-
OAuth is token based.
-
A token is a generated string of characters.
-
Once a user okays our web app's access, our web app receives a code parameter that is then exchanged for an access token.
-
Each token has a scope that determines what resources an app can access for that user. Again, in this lesson, we will only be interested in accessing our users' basic profile info.
-
If in your Project you would like to access more than a user's profile, you will need to modify the scope - check the specific provider's documentation on how to access additional resources.
-
Yes, OAuth is complex. But not to worry, we don't have to know all of the nitty gritty details in order to take advantage of it in our apps.
-
Plus, we will be using a very popular piece of middleware that will handle most of the OAuth dance for us.
-
True or false - if your site allows users to authenticate via OAuth, you should ensure they create a "strong" password.
-
What are the advantages provided to users by OAuth?
-
The advantages for web sites & developers?
-
What is the client application within the context of an OAuth provider?
-
Today, we are going to take a starter application and add OAuth authentication & authorization to it.
-
The app will allow you, as WDI Students, to list fun facts about yourself and read facts about fellow students, past and present.
-
The app will add you as a student to its database when you log in for the first time using Google's OAuth provider.
-
Allow me to demo what the finished app will look like.
The following stories are COMPLETE in the starter code:
- As a Visitor:
- I want to view fun facts about past and present WDI Students so that I can know more about them.
- I want to be able to search for students by their name so that I don't have to scroll through a long list.
- I want to sort the list of students by cohort or name so that I can more easily find the student I'm looking for.
We will complete these stories today:
- As an Authenticated Student:
- I want to add fun facts about myself so that I can amuse others.
- I want to be able to delete a fact about myself, in case I embarrass myself.
- I want to view the Google avatar instead of the placeholder icon.
- I want to be able to designate what cohort I was a part of.
-
Optionally, copy the
starter-code/wdi-students
folder. -
npm install
to install the app's node modules. -
Open the project in your code editor.
-
Create a
.env
file and add a key ofDATABASE_URL
and assign it a value provided by yours truly. -
nodemon
and browse tolocalhost:3000
to test.
-
Let's discuss the app's structure and introduce a couple of tidbits that you may or may not have seen yet.
-
First, this is a SPA - there is only one server-side index.ejs view. However, logging in/out, triggers a full page refresh.
-
The app was scaffolded using the
express-generator
and the main server script has been renamed toserver.js
.
-
The app uses the Materialize CSS framework based upon Google's Material Design.
-
Materialize's CSS and JavaScript files are being loaded via a CDN.
-
This app depends upon both server-side and client-side rendering.
-
There's a bit of server-side rendering when the user logs in and out.
-
When a fun fact is added or deleted, client-side rendering will kick into action.
-
As usual, EJS will be used for the server-side rendering, however, to prevent a conflict with client-side rendering, there is a new tag we will cover in a bit.
-
In this lesson, we will be performing client-side templating using Underscore, a popular JS library that has a bunch of functional programming helpers.
-
The great thing about underscore templating is that it has a low learning curve because it's very similar to EJS.
-
In fact, it uses the exact same tags (squids) - both with ink and without!
-
Let's open the console with the starter code running and check out how underscore templating works.
-
Copy/paste this object into the console:
var person = { name: 'Brendan Eich', facts: [ 'Computer Scientist', 'Co-founded everything Mozilla', 'Created JavaScript in 10 days' ] };
-
Now let's define a template, which is a string containing markup and EJS-like tags, to render the person object:
var template = ` <div class="card"> <h3><%= name %></h3> <p>Facts:</p> <ul> <% facts.forEach(function(fact) { %> <li><%= fact %></li> <% }); %> </ul> </div>`;
-
Using a template literal allows us to write multi-line strings, which is nice, but not necessary.
- Underscore templating works by:
- "Compiling" a template string into a template function - this only needs to be done once per template.
- Invoking the compiled template function with the data you want rendered passed as an argument.
- What is returned is a string of HTML with the data dynamically rendered. Now you have a string of markup ready to be injected the DOM where you need it.
-
Here's how we compile the template string into a template function:
var renderPerson = _.template(template);
-
As you can see, underscore provides a
template
method that accepts the template string. What is returned is the template function.
-
Now you can call that function, passing to it the data, anytime you need dynamically generated HTML (as a string):
var personMarkup = renderPerson(person);
produces this string...
<div class="card"> <h3>Brendan Eich</h3> <p>Facts:</p> <ul> <li>Computer Scientist</li> <li>Co-founded Mozilla</li> <li>Created JavaScript in 10 days</li> </ul> </div>
-
Test it out with this code:
document.body.innerHTML += renderPerson(person);
-
Since we are using
innerHTML
, for security purposes, we should sanitize the markup if we're not 100% in control of the data being rendered...
-
For example, students will be allowed to enter fun facts about themselves, since we are going to render what they submit, we need to ensure that they aren't able to pull off any shenanigans, such as a Cross-site Scripting Attack.
-
One way to do this is by "escaping" whatever the user entered when squidding it out. This prevents the user from being able to enter
<script>
tags. We escape the output of underscore using this syntax:<%%- fact.text %%>
-
The single "tentacle" tells underscore to escape/sanitize the string being squidded out.
- Cool, now that we have seen how to define a template, compile it into a template function with underscore, and pass data to that function to obtain a string of dynamically generated markup, let's get back to reviewing the starter code...
-
Review
index.ejs
andapp.js
for the implementation details of rendering and responding to user interaction such as searching and sorting. -
Note the use of double-percentages, e.g.,
<%%
, inindex.ejs
. EJS will render<%
in their place - this is necessary to avoid the conflict with underscore using the same tags. See here.
-
A
.env
file is being used to provide environment settings such as the database's connection string. -
Besides avoiding having secrets pushed to GitHub,
.env
allows us to configure our app with different settings locally vs. when deployed. -
The variables within
.env
are loaded on line 9 of server.js allowing its values to be used as shown on lines 3 & 7 ofconfig/database.js
.
-
The connection to MongoDB with Mongoose is done on line 12 of server.js.
-
We are using a hosted MongoDB so that we can see each other's fun facts!
-
Looking at
models/student
module reveals a singleStudent
model. -
Of interest is
factSchema
. This second schema is used to define the structure of the subdocuments embedded in thefacts
field of the Student model. -
The
avatar
property has been defined in advance for implementing a user story as an exercise later today.
-
As you know, in Mongoose, schemas define the structure of documents and only models are mapped to collections in the database.
-
The embedding of a Student's facts is highly improbable to cause any Student document to exceed the 16MB size limit and thus is a perfect use case for embedded docs.
-
Thanks to the
factSchema
, when we push a new fact into thefacts
array, all we do is provide thetext
field, and an_id
will automatically be created in the subdocument for us.
-
We have two separate route files: index.js & api.js.
-
In index.js, there is only the root route used to return our only view.
-
api.js contains our routes that will be accessed directly from the client using AJAX.
-
Always put routes in separate modules if you would like to namespace them using a different base path when mounting them.
-
Examining routes/api.js reveals that we are going to be putting our route handler code in two controllers modules:
controllers/facts
andcontrollers/students
. -
Currently, there is only one method,
index
, in students.js that returns all students in the database. -
In facts.js the
create
anddelete
methods have been stubbed up.
- Step 1: Register our App with Google's OAuth Server
- Step 2: Discuss PassportJS
- Step 3: Install & Configure Session middleware
- Step 4: Install PassportJS
- Step 5: Create a Passport config module
- Step 6: Install a Passport Strategy for OAuth
- Step 7: Configure Passport
- Step 8: Define routes for authentication
- Step 9: Add Login/Logout UI
- Step 10: Code the First User Story
- Step 11: Add Authorization
-
Every OAuth provider requires that our web app be registered with it.
-
When we do so, we obtain a Client ID and a Client Secret that identifies our application (not a user) to the OAuth provider.
-
For this lesson, we are going to use Google's OAuth server - the details of how to do so are here.
-
Time to register our app...
- You must be logged into Google Developers Console:
- Click Select a project, then click the New Project button.
- Type in a Project name, then click the Create button:
- It might take a bit, but once created, make sure the project is selected, then click ENABLE APIS AND SERVICES:
- Search for Google+ and click on Google+ API when it is visible:
- Click ENABLE:
- Now we need to create credentials for the app. Click CREATE CREDENTIALS:
- We're interested in obtaining a client ID - click it:
- Click Configure consent screen to setup the screen users will see in order to obtain their consent:
- Just enter a Project name and click the blue Save button:
-
The next slide shows what to do next.
-
The important thing to note is that you will have to add an additional entry in the Authorized redirect URIs once you have deployed your application to Heroku - something like
https://<your app name>.herokuapp.com/oauth2callback
.
-
After clicking the Save button, we will be presented with our app's credentials!
-
Let's put YOUR credentials, along with that callback we provided, in our
.env
file so that it looks something like this:
DATABASE_URL=mongodb://<dbuser>:<dbpassword>@ds053954.mongolab.com:53954/wdi-students
GOOGLE_CLIENT_ID=245025414219-2r7f4bvh3t88s3shh6hhagrki0f6op8t.apps.googleusercontent.com
GOOGLE_SECRET=Yn9T_2BKzxr4zgprzKDGI5j3
GOOGLE_CALLBACK=http://localhost:3000/oauth2callback
-
With registering our app now completed, just remember that each provider will have its own unique process.
-
Any questions about what we just did?
-
Implementing OAuth is complex. There are redirects going on everywhere, access tokens that only last for a short time, refresh tokens used to obtain a fresh access token, etc.
-
As usual, we will stand on the shoulders of giants that have done much of the hard work for us - enter PassportJS.
-
Passport is by far the most popular authentication framework out there for Express apps.
-
Passport's website states that it provides Simple, unobtrusive authentication for Node.js.
-
Basically this means that it handles much of the mundane tasks related to authentication for us, but leaves the details up to us, for example, not forcing us to configure our user model a certain way.
-
There are numerous types of authentication, if Passport itself was designed to do them all, it would be ginormous!
-
Instead, Passport uses Strategies designed to handle a given type of authentication. Think of them as plug-ins for Passport.
-
Each Express app with Passport can use one or more of these strategies.
-
Passport's site currently shows over 300 strategies available.
-
OAuth, or more specifically, OAuth2, although a standard, can be implemented slightly differently by OAuth providers such as Facebook and Google.
-
As such, there are strategies available for each flavor of OAuth provider.
-
For this lesson, we will be using the passport-google-oauth strategy.
-
Passport is just middleware designed to authenticate requests.
-
Passport's middleware will automatically add a
user
object to thereq
object if the request was made by an authenticated user. -
We will then have that
req.user
object available to our route handler code!
-
Before we install Passport and a strategy, we need to install the
express-session
middleware. -
Sessions, are a server-side way of remembering a user's browser session.
-
It remembers the browser session by setting a cookie that contains a session id. No other data is stored in the cookie, just the id of the session.
-
On the server-side, the application can store data pertaining to the session.
-
Passport will use the session, which is an in-memory data-store by default, to store a nugget of information that will allow us to lookup the user in our database.
-
FYI, since sessions are maintained in memory by default, if we restart our server, session data will be lost. You will see this happen when nodemon restarts the server and we are no longer logged in :)
-
Let's install the module:
$ npm install express-session
-
Next, require it below the
logger
:var logger = require('morgan'); // new code below var session = require('express-session');
-
Finally, we can configure and mount our session middleware below the
cookieParser
:app.use(cookieParser()); // new code below app.use(session({ secret: 'WDIRocks!', resave: false, saveUninitialized: true }));
-
The secret is used to digitally sign the session cookie making it very secure. You can change it to anything you want. Don't worry about the other two settings, they are only being set to suppress deprecation warnings.
-
nodemon
to make sure your server is running. -
Browse to our app at
localhost:3000
. -
Open the Resources tab in DevTools, then expand Cookies in the menu on the left.
-
A cookie named
connect.sid
confirms that the session middleware is doing its job.
####Congrats, the session middleware is now in place!
-
Before a web app can use an OAuth provider, it must first ___________ with it to obtain a ___________ and a client secret.
-
Passport uses ___________ designed to handle specific types of authentication.
-
In your own words, explain what a session is.
-
If there is an authenticated user, the request (
req
) object will have what attached to it by Passport?
-
The Passport middleware is easy to install, but challenging to set up correctly.
-
First the easy part:
$ npm install passport
-
Require it as usual below
express-session
:var session = require('express-session'); // new code below var passport = require('passport');
-
With Passport required, we need to mount it. Be sure to mount it after the session middleware and always before any of your routes are mounted that would need access to the current user:
// app.use(session({... code above app.use(passport.initialize()); app.use(passport.session());
-
The
passport.initialize()
is always required, however,passport.session()
can be removed if you don't need persisted login sessions (like for a pure backend api where credentials are sent with each request).
-
Because it takes a significant amount of code to configure Passport, we will create a separate module so that we don't further pollute server.js.
-
Let's create the file:
$ touch config/passport.js
-
In case you're wondering, although the module is named the same as the
passport
module we've already required, it won't cause a problem because a module's full path uniquely identifies it to Node.
-
Our
config/passport
module is not middleware. -
Its code will basically configure Passport and be done with it. Nor does it need to export any functionality, thus, we don't even need to store the empty object returned by module.exports.
-
Requiring below our database is as good of a place as any in server.js:
require('./config/database'); // new code below require('./config/passport');
-
In our
config/passport.js
module we will certainly need access to thepassport
module:var passport = require('passport');
-
It's important to realize that this
require
returns the very samepassport
object that was required in server.js.
Why is this?
-
Time to install the strategy that will implement Google's flavor of OAuth:
$ npm install passport-google-oauth20
-
This module implements Google's OAuth 2.0 API. It's docs can be found here.
-
Note that OAuth 1.0 does still exist here and there, but it's pretty much obsolete.
-
Now let's require the
passport-google-oauth20
module below that ofpassport
in our passport.js module:var passport = require('passport'); // new code below var GoogleStrategy = require('passport-google-oauth20').Strategy;
-
Note that the variable is named using upper-camel-case.
What does that typically hint at? -
Let's make sure there's no errors before moving on to the fun stuff!
To configure Passport we will:
-
Call the
passport.use
method to plug-in an instance of the OAuth strategy and provide a verify callback function that will be called whenever a user has logged in using OAuth. -
Define a serializeUser method that Passport will call after verify to let Passport know what data we want to store in the session to identify our user.
-
Define a deserializeUser method that Passport will call for every request when a user is logged in. What we return will be assigned to the
req.user
object.
-
Now it's time to call the `passport.use` method to plug-in an instance of the OAuth strategy and provide a _verify_ callback function that will be called whenever a user logs in with OAuth. In _passport.js_:
var GoogleStrategy = require('passport-google-oauth20').Strategy;
// new code below
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK
},
function(accessToken, refreshToken, profile, cb) {
// a user has logged in with OAuth...
}
));
-
Note the settings from the
.env
file being passed to theGoogleStrategy
constructor function.
What is the name of the module we've been using that loads the settings from the.env
file? -
Next we have to code the verify callback function...
-
The callback will be called by Passport when a user has logged in with OAuth.
-
It's called a verify callback because with most other strategies we would have to verify the credentials, but with OAuth, well, there are no credentials!
-
In this callback we must:
- Fetch the user from the database and provide them back to Passport by calling the
cb
callback method, or... - If the user does not exist, we have a new user! We will add them to the database and pass along this new user in the
cb
callback method.
- Fetch the user from the database and provide them back to Passport by calling the
-
But wait, how can we tell what user to lookup?
-
Looking at the callback's signature:
function(accessToken, refreshToken, profile, cb) {
-
We can see that we are being provided the user's profile - this object is the key. It will contain the user's Google Id.
-
However, in order to find a user in our database by their Google Id, we're going to need to add a field to our
Student
model's schema to hold it...
-
Let's add a property for
googleId
to ourstudentSchema
insidemodels/student.js
file:var studentSchema = new mongoose.Schema({ name: String, email: String, cohort: String, avatar: String, facts: [factSchema], googleId: String }, { timestamps: true });
-
Cool, now when we get a new user via OAuth, we can use the Google
profile
object's info to create our new user!
-
Now we need to code our callback!
-
We're going to need access to our
Student
model:var GoogleStrategy = require('passport-google-oauth20').Strategy; // new code below var Student = require('../models/student');
-
Let's do another error check by ensuring our server is running and we can refresh our app.
-
Cool, the next slide contains the entire
passport.use
method.
Copy/paste it, then we'll review it...
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK
},
function(accessToken, refreshToken, profile, cb) {
Student.findOne({ 'googleId': profile.id }, function(err, student) {
if (err) return cb(err);
if (student) {
return cb(null, student);
} else {
// we have a new student via OAuth!
var newStudent = new Student({
name: profile.displayName,
email: profile.emails[0].value,
googleId: profile.id
});
newStudent.save(function(err) {
if (err) return cb(err);
return cb(null, newStudent);
});
}
});
}
));
-
Our
passport.use
method has been coded. Now we need to write two more methods inside ofconfig/passport
module. -
First the callback method we just created is called when a user logs in, then the
passport.serializeUser
method is called in order to set up the session. -
The
passport.deserializeUser
method is called everytime a request comes in from an existing logged in user - it is this method where we return what we want passport to assign to thereq.user
object.
-
First up is the
passport.serializeUser
method that's used to give Passport the nugget of data to put into the session for this authenticated user. Put this below thepassport.use
method:passport.serializeUser(function(student, done) { done(null, student.id); });
-
Passport gives us a full user object when the user logs in, and we give it back the tidbit to stick in the session.
-
Again, this is done for server scalability and performance reasons - a lot of session data sucks.
-
The
passport.deserializeUser
method is used to provide Passport with the user from the db we want assigned to thereq.user
object. Put it below thepassport.serializeUser
method:passport.deserializeUser(function(id, done) { Student.findById(id, function(err, student) { done(err, student); }); });
-
Passport gave us the
id
from the session and we use it to fetch the student to assign toreq.user
. -
Let's do another error check.
-
Our app will provide a link for the user to click to login with Google OAuth. This will require a route on our server to handle this request.
-
Also, we will need to define the route,
/oauth2callback
we told Google to call on our server after the user confirms or denies their OAuth login. -
Lastly, we will need a route for the user to logout.
-
We're going to code these three new auth related routes in our
routes/index
module. -
These new routes will need to access the
passport
module, so let's require it in routes/index.js:var router = require('express').Router(); // new code below var passport = require('passport');
-
In
routes/index.js
, let's add our login route below our root route:// Google OAuth login route router.get('/auth/google', passport.authenticate( 'google', { scope: ['profile', 'email'] } ));
-
The
passport.authenticate
function will take care of coordinating with Google's OAuth server. -
The user will be presented the consent screen if they have not previously consented.
-
Then Google will call our Google callback route...
-
Note that we are specifying that we want passport to use the
google
strategy. Remember, we could have more than one strategy in use. -
We are also specifying the scope that we want access to, in this case,
['profile', 'email']
.
-
Below our login route we just added, let's add the callback route that Google will call after the user confirms:
// Google OAuth callback route router.get('/oauth2callback', passport.authenticate( 'google', { successRedirect : '/', failureRedirect : '/' } ));
-
Note that we can specify the redirects for a successful and unsuccessful login. For this app, we will redirect to the root route in both cases.
-
The last route to add is the route that will logout our user:
// OAuth logout route router.get('/logout', function(req, res){ req.logout(); res.redirect('/'); });
-
Note that the
logout()
method was automatically added to the request (req
) object by Passport! -
Good time to do another error check.
-
Before we can dynamically modify our index.ejs view depending upon whether there's an authenticated user or not, we need to modify our root route to pass
req.user
to it:router.get('/', function(req, res) { res.render('index', { user: req.user }); });
-
Now the logged in student is in a
user
variable that's available inside ofindex.ejs
. If nobody is logged in,user
will be undefined (falsey).
-
We're going to need a link to for the user to click to login/out.
Let's modify ourindex.ejs
:<a href="" class="brand-logo left">WDI Student Fun Facts</a> <!-- new html below --> <ul class="right"> <li> <% if (user) { %> <a href="/logout"><i class="material-icons left">trending_flat</i>Log Out</a> <% } else { %> <a href="/auth/google"><i class="material-icons left">vpn_key</i>Login with Google</a> <% } %> </li> </ul>
-
As a reminder, for EJS server-side processing, we use
<% %>
tags like we just did to dynamically render the nav bar depending upon whether or not a user is logged in. -
However, wherever we want to define a client-side template to be used with underscore, we need to use
<%% %%>
tags that EJS turns into<% %>
tags within the page returned to the browser.
-
We've finally got to the point where you can test out our app's authentication!
-
May the force be with us!
-
Our first user story reads:
I want to add fun facts about myself so that I can amuse others. -
We will want to add an
<input>
for the fact's text and a button element to the logged in student's card only.
-
Let's add some dynamic UI to add a fact. Ensure it's added in the correct location!
<li class="collection-item ... "> <%% }) %%> <!-- new html below --> <%% if (student._id === '<%= user && user.id %>') { %%> <div class="card-action"> <input type="text" id="fact" class="white-text"> <input type="button" class="btn white-text" onclick="addFact()" value="Add Fact"> </div> <%% } %%>
-
Note that we are mixing in a bit of server-side EJS,
<%= user && user.id %>
, used to insert a logged in user's document id.
-
Sprinkle in some AJAX inside of app.js to post our new fact, update our in-memory array, and re-render the view:
function addFact() { if ( !$('#fact').val() ) return; fetch('/api/facts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { fact: $('#fact').val() } ), credentials: 'include' // send the session cookie! }) .then(res => res.json()) .then(data => { // clear the <input> $('#fact').val(''); // find the updated student's index var idx = allStudents.findIndex(s => s._id === data._id); allStudents[idx] = data; render(); }); }
-
Lastly, the server controller code for the route (already defined). In
controllers/facts.js
:function create(req, res) { req.user.facts.push({text: req.body.fact}); req.user.save(function(err) { res.json(req.user); }); }
-
Note that
req.user
IS the Mongoose user document!
-
That should take care of our first user story - try it out!
-
Cool, just one step left!
-
What is authorization?
-
Passport adds a nice method to the request object,
req.isAuthenticated()
that returns true or false depending upon whether there's a logged in user or not. -
We can easily write our own little middleware function to take advantage of
req.isAuthenticated()
.
-
As we know by now, Express's middleware and routing is extremely flexible and powerful.
-
We can actually insert additional middleware functions before a route's final middleware function! Let's modify
routes/api.js
to see this in action:// POST /api/facts router.post('/facts', isLoggedIn, factsCtrl.create);
-
Take note of the inserted
isLoggedIn
middleware function!
-
Our custom
isLoggedIn
middleware function, like all middleware, will either callnext()
, or respond to the request. -
Let's put our new middleware at the very bottom of
routes/api.js
- just above the module.exports:// Insert this middleware for routes that require a logged in user function isLoggedIn(req, res, next) { if ( req.isAuthenticated() ) return next(); res.redirect('/auth/google'); }
-
That's all there is to it!
-
For a challenging practice, complete the remaining three user stories:
- I want to be able to delete a fact about myself, in case I make a mistake.
- I want to be able to designate what cohort I was a part of.
- I want to show the user's Google avatar instead of the current icon.
-
Start a new application from scratch that implements authentication and authorization as shown in this lesson. Remember, this is a requirement of Project 3!