- [Instructor] In our last step,
we now have to implement a rather interesting flow.
We also need to extend the database a bit
to support storing the profile ID.
The main challenge
is that GitHub supports multiple email addresses
and that for us, the email address is a mandatory field.
So if we get the profile with two email addresses,
which one should we take?
So it's better to provide a way
for the user to complete their profile
with a valid email address.
And while I'm at it,
I also want to let them set their username.
So I want them to be able to log in using GitHub
but I don't want to force them
to use a given username
or use an arbitrary email address
that might not be the one that they want to use.
So let's kick this off by extending the database
to support arbitrary social profiles.
For that, I open server, models, UserModel
and there somewhere down there
after the verification token,
I now add a structure for the profile ID.
And I want this to be an array
because I want to be able
to maybe store later a Facebook ID
or a Google ID or whatever.
So this should be agnostic to the provider
of the OAuth.
So I call this field oauthprofiles.
And as I said, this is an array,
so that's a notation to create an array structure
in MongoDB and in there,
I can now define the data this array holds.
And first, I want to have a field provider
and this is type: String
and I also want to have a field profileId.
So in our case, this would be the GitHub
and the profileId that we just got
and this is again,
most of the time a number,
but I still think it's better to store it as a string.
And then I also want to define an index.
Why?
Because I have to look up the user
by provider and profile ID
and having an index here really speeds up things,
especially when you have many users
and this is something usually just figure out
when you have many users
but then it's pretty annoying
to create a new index.
So add now userSchema.index
and I create the combined index in MongoDB
and for that, I add oauthprofiles.provider
and it can be ascending,
so that's the direction of the index, the sort order.
And also oauthprofiles.profileId.
So with that, our database is now ready
to deal with OAuth profiles.
So with that, our database is now ready
to deal with social profiles
and I now also have to extend the service,
the user service a little bit,
so open services, UserService.
So as I said before,
I did not want to scaffold everything here.
So now we really have to add two more methods here
and the first is createSocialUser
and for that, we can basically copy the createUser
but instead of the password,
we now accept an argument oauthProfile.
So let's remove this not implemented.
We have to rename the function, of course.
createSocialUser.
And instead of user.password,
we say user.oauthprofiles,
and as this is a new user here,
we can now just store the oauthprofile
that we will pass in then later there.
And there is one more thing.
We also have to somehow set the password
and for that, I will just set a random password.
The user can always use password reset later
to set their own password
and then can log in with username and password,
as well as with the social profile
but by default, the password will be just unknown
to the user and random.
For that, we have to bring in crypto.
So add at the very top const crypto
equals require crypto
and then we use the already familiar function
crypto.randomBytes
and in this case, let's just use 10 bytes here
and let's make it a hex string then.
And this will now create a new user
with a social profile and a random password.
We also need one more function
that finds a user using this oauthProfile information.
So we will need that later to try loading the user.
So I scroll down to put this method where it belongs to.
This is then somewhere here close to findById.
So add static async findByOAuthProfile
and there I get the provider.
And the profileId.
And there I just return UserModel.findOne,
so that's now MongoDB syntax
and then I add oauthprofiles
and colon and then I add .elemMatch,
so this matches the elements of an array
and there I just pass in provider and profileId.
Again, you should know the shortcut syntax,
so technically, this would be the same
as writing something like provider: provider
but no one would do that
if there is this shortcut version of it.
And in the end, we add exec.
That's a little bit of a MongoDB Mongoose-ism
because this then gives us back a real valid promise.
And with that,
we can already extend now our passport function
to try to find the given user.
So in passport, index.js,
I now scroll down again
to the GitHub strategy
and right now, we have not much in there.
And first, I will now set a session variable to null
and you will in a second understand what it does.
First, and now
to request.session.tempOAuthProfile equals null.
So this session variable
will later be used for something.
And in a bit, you will know for what.
Now I will try to load the user first.
So I add const user equals
await UserService.findByOAuthProfile
and there I now use profile.
So this is now what I get
from the authentication OAuth request
through the flow that was covered before.
So in profile, there is a field provider
that is set by OAuth
and it's, in this case, of course, GitHub
and the profile also has this field id.
So we try now to load the user from the database
with that profile.id that we got
and now we have to handle the case
that we don't find the user in the database.
So we will now say if not user.
Then we want to now store this user into the session
because we need it later
because we have a little bit of an extended flow
because we want the user to complete their information,
so we need some way to persist this OAuth information.
So I do request.sessoin.tempOAuthProfile equals
and now I set provider to profile.provider.
I'm lazy, I'm copy, pasting here.
And the profileId is the profile.id, profile.id
that we get here in this request.
And now I can just return done null,
no error and user.
So if we found the user, we will send it.
If we did not find the user, nothing will happen.
But now there comes a key component
because where will we now end up
in case no user was found?
Before, we saw that we get this default,
not authenticated message through passport
but we now want to do something else.
We want to let the user extend the information
because the social login likely was valid
but still, we have no valid user.
So we now have to head into routes,
auth, github.js and there we can first do one thing
because this route of get for get, auth, GitHub,
we will never end up in the body,
in the callback there
because this only starts the GitHub flow,
so we can remove everything here
and just leave it with passport.authenticate.
This starts the GitHub flow
because the real logic then happens
in the callback.
Then starting here in line 13.
And in this callback,
now the, let's say magic happens
because now I'm saying this config argument,
so a second argument after the strategy named GitHub
and now add failureRedirect
and the failureRedirect
is now /auth/github/complete.
I either find the user,
then everything is fine
and the user is authenticated
and we will handle this then in the callback done here now,
starting at line 18.
But if there is any failure
and even if the social login was valid
but we could not find the user,
we will end up in the failure redirect
and we will implement it later.
So first, let's implement what happens
when we end up here.
If we end up here,
we found the user, everything is fine,
so the user was already created and the profile is valid
and they have the social login attached to it.
So we can now say request.session.messages.push
and the text is you are logged in via GitHub now.
And type this success
and then we do a return res.redirect
and as before, with the regular login,
we do a request.session.returnTo.
So this is as before.
If the user tried to access some page
they were not allowed to go,
then the returnTo URL is set on the session
and we send them back there
once they are logged in successfully
or you send them to the index page.
So this is now the callback.
As we know, the real work really happens within passport,
within the strategy.
So this does everything
so that we even get to the profile.
Now we have to implement this complete route.
And there we have actually a gap in the post route
and we start with router.get /complete.
And it's an async function.
Request, response, next.
First, let's do the standard try catch block.
Try catch error, return next error.
And in the try block,
we now evaluate if we have this tempOAuthProfile.
If we don't have it,
then actually, the whole authentication flow has failed
because we did not end even in the callback
with the profile.
So I can now add if not request.session.tempOAuthProfile.
Make sure that you don't have a typo.
It's a pretty long variable here.
And I say request.session.messages.push
and the text is login via GitHub has failed.
And the type is, in this case, danger.
And we can then right away return response.redirect
to /auth/login.
So let's send them back to the login page.
Something went wrong.
Now we come to a rather interesting case
and this is contra linking.
So it's possible that a user is already logged in
and then tries to link their GitHub profile
to their account to in the future use GitHub
for logging in.
So we have to handle that as well
and for that, we have to extend
the correctly logged in user now
with the profile information.
And for that, I now add if request.user.
So we have a logged in user.
We ended up here.
The GitHub login went into the failure state.
We have a tempOAuthprofile
and we have a user.
So this means that this user basically now just needs
to get this profile information attached
and everything is fine.
So I add const user
equals await UserService.findById
and I use request.user.id.
And now I just have to extend this oauthprofile property.
So I say if not user.oauthprofiles,
so there is no property oauthprofiles on this user.
I will now set oauthprofiles to an array.
And then I can simply user.oauthprofiles.push
and there I just push in
this request.session.tempOAuthProfile
because there is the provider and the profileId in there
so we can use that as is.
And then we can just await user.save.
And then let's put out a message.
Let's copy the one from over there
but now make it a success
and the message is your GitHub profile
was successfully linked!
And we can again do this the same
that we did before.
I'm now copying that from line 25.
So we'll have a successful login, one way or the other.
And we can return res.redirect
so session.returnTo or to the next page.
But if all of that is not true,
we can do a response.render
and I created the template for that for you already.
So it's in the view's directory.
There is an auth/complete
and this is now the form that lets you add the username
and the email address
and the page's registration,
just to mark it in the navigation.
So let's look into the browser
and see where we're at right now.
So I'm heading into the browser.
Click on Registration.
It doesn't really matter
because the functionality's always the same actually
with the sign up or log in via GitHub
and I click on that.
And you see that I now end up on this page,
please complete your profile.
And this form now will post its data
to auth/github/complete.
So we now have to create this post route
that receives the data
and then completes the user
and then saves the user.
We will do this in the next video.