Authentication with OAuth
Click Here to access recording
Learning Objectives
Students will be able to:
- Explain the difference between Authentication & Authorization
- Identify the advantages OAuth provides for usersand 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
Roadmap
- Intro to Authentication
- 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)
Intro to Authentication
Why We Need Authentication
- An application's functionality usually revolves around a particular user.
- For example, when we use online banking, or more importantly, save songs to our Spotify playlists, the application has to know who we are - and this is where authentication comes in.
What is Authentication?
- Authentication is what enables an application to know the identity of the person using it.
-
There are several types of authentication, here are 3 of the most common ones:
- Third-party providers via OAuth
- Token-based with JSON Web Tokens
- Session-based
Authentication vs. Authorization
- Authentication and authorization are not the same thing...
- Authentication verifies a user's identity.
-
Authorization determines what functionality a given user can access. For example:
- What features a logged in (authenticated) user has vs. an anonymous visitor?- or -
- What features an admin user has vs. some other user role?
Why OAuth?
- Consider applications where we have 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!
What is OAuth? - Vocab
- 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...
How Does OAuth Work?
OAuth 2's Flow
- The ultimate goal is for the client application (our web app) to obtain an access token from an OAuth provider that allows the app to access the user's resources from that provider's API's.
- Usually we only want to access to the most basic of resources the user could grant us - their name, email & maybe their avatar.
- However, it's possible to request access to resources such as a user's Facebook friends, tweets, Dropbox data, 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 - be sure to 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.
OAuth Review Questions
❓ 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?
The App We Will Build Today
- Today, we are going to take a starter application and add OAuth authentication & authorization to it.
- The app will allow you, as SEIR 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 service.
The App's User Stories
This is the only user story that's complete in the starter code:
-
As a Visitor:
- I want to view fun facts about past and present SEIR Students so that I can know more about them.
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.
Setup and Review the Starter Code
Download the starter code to get started
- Install the node modules:
$ npm install
cd
inside the project folder in your code editor.- Use
nodemon
to start the server. -
The app has two server-side views:
- views/index.ejs
- views/students/index.ejs
- The app uses the Materialize CSS framework based upon Google's Material Design.
Review the Starter Code: The Views
- Several Materialize CSS classes are being used for layout and styling.
- EJS is being used to render a "card" for each student.
Review the Starter Code: Models
- There is only a single
Student
Model exported by models/student.js. - A
factSchema
is used to define the structure for the fact subdocuments being embedded within a student document'sfacts
property. - The
avatar
property has been defined in advance for implementing a user story as an exercise later today. - As you know, Mongoose schemas define the structure of documents, but only Models create collections in the database.
- A student's facts is a perfect use case for embedding.
- 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.
Review the Starter Code: Routing
- We have two separate router files: routes/index.js & routes/students.js.
- routes/index.js currently has only the root route defined that immediately renders the Home/Landing "index" page.
-
routes/students.js has three routes defined for the following actions:
Purpose Method Path Display all students GET
/students
Create a fact for a student POST
/facts
Delete a fact DELETE
/facts/:id
- Why aren't we using
POST /students/:id/facts
to create a fact? - In this lecture, we'll learn to access the "logged in" student on the server, therefore we would not make a
POST
request to/students/:id/facts
... think of this as "user-centric" CRUD. - Please note, this is one of the few exceptions we'd make to our typical RESTful routing convention.
Review the Starter Code Controller
- The
index
action in controllers/students.js is querying theStudent
model and providing the array of students to the students/index.ejs view. - NOTE: There are two incomplete controller actions,
addFact
&delFact
that we'll need to work on later; one of these will left to you as a challenge exercise.
Today's OAuth Game Plan
- 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
Step 1 - Register our App
- 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 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...
Step 1.1 - Google Developers Console
- You must be logged into Google Developers Console:
Step 1.2 - Create a Project
- Click on the project selector widget, then click the New Project button.
- Type in a Project name, then click the Create button:
Step 1.3 - Enable the People API
- It might take a bit, but once created, make sure the project is selected in the project selector widget, then click + ENABLE APIS AND SERVICES:
- Search for people and click on Google People API when it is visible:
- Click ENABLE:
Step 1.4 - Obtain Credentials for App
- Now we need to create credentials for the app. Click Create Credentials:
- Then, right below "Add Credentials to your project", click on client ID
- Click Configure consent screen to setup the screen users will see in order to obtain their consent:
Step 1.4 - Obtain Credentials for App
- Just enter a Application name and click the blue Save button:
- Then click on the Credentials option in the side menu, then click on Create credentials then select OAuth client ID
- For this screen, we're going to add the name of our app in the Name field and inside the Authorized redirect URIs, we'll add the following:
http://localhost:3000/oauth2callback
- The important thing to note is that eventually we'll have to come back and add an additional entry in the Authorized redirect URIs once you have deployed your application to Heroku - something like:
https://someappname.herokuapp.com/oauth2callback
.
- After clicking the Create 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:
EXAMPLE ONLY - DO NOT COPY AND PASTE
DATABASE_URI=mongodb+srv://someusername:abc1234@seir-students-1btwt.azure.mongodb.net/students?retryWrites=true
GOOGLE_CLIENT_ID=245025414219-2r7f4bvh3t88s3shh6hhagrki0f6op8t.apps.googleusercontent.com
GOOGLE_SECRET=Yn9T_2BKzxr4zgprzKDGI5j3
GOOGLE_CALLBACK=http://localhost:3000/oauth2callback
Congrats on Registering the App
- With registering our app now completed, just remember that each provider will have its own unique process.
- Any questions about what we just did?
Step 2 - Passport Discussion
- 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 heavy lifting 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 500 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.
- When a request is sent from an authenticated user, Passport's middleware will automatically add a
user
object to thereq
object. - You will then be able to access that
req.user
object in all of our controller actions!
Step 3 - Session Middleware
- 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 the database.
- FYI, since sessions are maintained in memory by default, if the server restarts, session data will be lost. You will see this happen when nodemon restarts the server and you are no longer logged in :)
Step 3.1 - Installing Session Middleware
- Let's install the module:
$ npm install express-session
-
Next, require it below the
morgan
:const morgan = require('morgan'); // new code below const session = require('express-session'); const port = process.env.PORT || 3000;
Step 3.2 - Configure and Mount Session Middleware
-
Now, we can configure and mount the session middleware below our
body-parsing
middleware:app.use(express.urlencoded({ extended: false })); // new code below app.use(session({ secret: 'SEIRRocks!', 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.
Step 3.3 - Verifying Session Middleware
nodemon
to make sure your server is running.- Browse to the app at
localhost:3000
. - Open the Application 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!
Step 4 - Install Passport
- The Passport middleware is easy to install, but challenging to set up correctly.
- First the easy part:
$ npm install passport
-
Require it below
express-session
:const session = require('express-session'); // new code below const passport = require('passport');
Step 4.1 - Mount 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 way
passport
middleware is being mounted is straight from the docs.
Step 5 - Create a Passport Config Module
- Because it takes a significant amount of code to configure Passport, we will create a separate module so that we don't 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.
Step 5.1 - Passport Module's Exports Code
- Our
config/passport
module is not middleware. - Its code will basically configure Passport and be done with it. We're not going to export anything either.
-
Requiring below our database is as good of a place as any in server.js:
require('./config/database'); // new code below require('./config/passport');
Step 5.2 - Require Passport
-
In the config/passport.js module we will certainly need access to the
passport
module:const passport = require('passport');
- This
require
returns the very samepassport
object that was required in server.js - Node modules are singletons.
Step 6 - Install the OAuth Strategy
- Time to install the strategy that will implement Google's flavor of OAuth:
$ npm install passport-google-oauth
- This module implements Google's OAuth 2.0 and 1.0 API.
- Note that OAuth 1.0 does still exist here and there, but it's pretty much obsolete.
Step 6.1 - Require the OAuth Strategy
- Now let's require the
passport-google-oauth
module below that ofpassport
in config/passport.js:
const passport = require('passport');
// new code below
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
- Note that the variable is named using upper-camel-case.We cannot flex on this naming when we use it, we must referenced it exactly like this
- Let's make sure there's no errors before moving on to the fun stuff!
Step 7 - Configuring Passport
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 the verify callback 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 on each request when a user is logged in. What we return will be assigned to the
req.user
object.
Step 7.1 - passport.use
- 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:
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
// 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...
Step 7.2 - The Verify Callback
- 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...
Step 7.3 - Modify the Student Model
-
Let's add a property for
googleId
to ourstudentSchema
insidemodels/student.js
file:const studentSchema = new mongoose.Schema({ name: String, email: String, avatarURL: String, facts: [factSchema], googleId: String // 👈 Let's add this }, { 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!
Step 7.4 - Callback Code
- Now we need to code our callback!
-
We're going to need access to our
Student
model:const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; // new code below const 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.We'll review the verify function as we type it in...
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!
const 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);
});
}
});
}
));
Step 7.5 - de/serializeUser Methods
- 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.
Step 7.6 - deserializeUser Method
-
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.
Step 8 - Define Routes for Authentication
- 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.
Step 8.1 - routes/index Module
- 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:const router = require('express').Router(); // new code below const passport = require('passport');
Step 8.2 - Login Route
-
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']
.
Step 8.3 - Google Callback Route
-
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 : '/students', failureRedirect : '/' } ));
- Note that we can specify the redirects for a successful and unsuccessful login. For this app, we will redirect the user to see the list of students/facts in the case of a successful login and to the root in case of a failure.
Step 8.4 - Logout Route
-
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.
Step 9 - Add Login/Logout UI
-
We want the navbars in views/index.ejs & views/students/index.ejs to update dynamically depending upon whether there's an authenticated user or not:
vs
Step 9 - Add Login/Logout UI
- First we need to update the logic inside of routes/index.js & controllers/students.js to pass in
req.user
:
// inside of ./routes/index.js
router.get('/', function(req, res) {
res.render('index', {
user: req.user
});
});
// inside of ./controllers/students.js
function index(req, res) {
Student.find({}, function(err, students)
res.render('students/index', {
students,
user: req.user
});
});
}
- Now the logged in student will be the
user
variable that's available inside of views/index.ejs & views/students/index.ejs. - If nobody is logged in,
user
will beundefined
(falsey).
Step 9.1 - Add the Login / Logout UI Logic
- We're going to need a link for the user to click to login/out.
- Lets modify views/index.ejs & views/students/index.ejs as follows:
<nav>
<div class="nav-wrapper">
<a href="" class="brand-logo left">SEI Student Fun Facts</a>
<!-- Add login UI here -->
<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>
</div>
</nav>
Step 9 - Try Logging In!
- We've finally got to the point where you can test out our app's authentication!
- May the force be with us!
Step 10 - Code the First User Story
- Our first user story reads:I want to add fun facts about myself so that I can amuse others.
- We're going to need a
<form>
with an<input>
for the fact's text and a submit button. - However, we only want this UI to show within the logged in student's card only.
Step 10.1 - Add Dynamic UI
- Let's add some dynamic UI to add a fact inside of views/students/index.ejs. Ensure it's added in the correct location
-
NOTE: There are pre-defined place holders to show you where to add this.
<!-- More Code Above... --> <li class="collection-item blue-grey-text text-darken-2"><%= fact.text %></li> <% }) %> </ul> <!-- Place Add Fact UI Here --> <!-- new code below --> <% if (student._id.equals(user && user._id)) { %> <div class="card-action"> <form action="/facts" method="POST"> <input type="text" name="text" class="white-text"> <button type="submit" class="btn white-text">Add Fact</button> </form> </div> <% } %> <!-- More code below... -->
- Note how the
equals
method is being used to compare the_id
s - this is necessary because they are objects. Also, the(user && user._id)
prevents an error when there's nouser
logged in.
Step 10.2 - Controller Code
-
Lastly, let's code the
addFact
action in the controllers/students.js controller:function addFact(req, res, next) { req.user.facts.push(req.body); req.user.save(function(err) { res.redirect('/students'); }); }
- Note that
req.user
IS a Mongoose user document!
Step 10 - Code the First User Story
- That should take care of our first user story - try it out!
- Yes, the UX is not that great because of the full-page refresh, but we'll address that when we develop single-page apps with React.
- Cool, just one step left!
Step 11 - Authorization
- What is authorization?
- Passport adds a nice method to the request object,
req.isAuthenticated()
that returnstrue
orfalse
depending upon whether there's a logged in user or not. - We're going to write our own little middleware function to take advantage of
req.isAuthenticated()
to perform some authorization.
Step 11.1 - Authorization Middleware
- 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/students.js to see this in action:
router.get('/students', studentsCtrl.index);
router.post('/facts', isLoggedIn, studentsCtrl.addFact);
router.delete('/facts/:id', isLoggedIn, studentsCtrl.delFact);
- Take note of the inserted
isLoggedIn
middleware function!
Step 11.2 - Authorization Middleware
- 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/students.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!
Congrats!
You have implemented OAuth authentication and authorization!
Review Questions
❓ Before a web app can use an OAuth provider, it must first ___ with it to obtain a ___ and a client secret.
❓ In your own words, explain what a session is.
Practice Exercises
- Now you're ready to start your project by implementing OAuth authentication!
-
For some challenging practice, complete the remaining three user stories:
- I want to show the user's Google avatar instead of the current icon.
- I want to be able to delete a fact about myself, in case I make a mistake.