Now that we are able
to create users, it's now time
to work on the authentication functionality.
So on logging users in
and getting that permission, this token
for our logged in users.
And being able to find out if a user is logged in,
both on the client side for changing what we see
and for disabling certain routes,
as well as on the server side in different API routes
we might be adding.
And that's now the part where this NextAuth package,
which we installed earlier already becomes important.
It helps us with authenticating users
and it helps us with finding out
whether a user has that permission
by managing that token creation and storage,
both parts behind the scenes.
Now, to use NextAuth,
we start by adding another API route
because logging a user in, signing a user in,
that, of course, also requires a request
to be sent to a certain API route
where we then look into the database
and find out whether we have that user in the database
and whether the password is correct.
And therefore, I'll create another file
in that api/auth folder
but this file now needs a special name.
It's a square bracket file name.
So a dynamic API route
and it's a dynamic catch all API route,
which catches all unknown routes
that start with api/auth.
Here we can then use ...nextauth written like this.
One word and in here, we now use that NextAuth package.
And we need this catch all route
because the next auth package behind the scenes
will expose multiple routes
for user login and for user logout, for example.
A couple of other routes as well.
And in order to set up its own routes
and handle its own routes,
we need to have a catch all route
so that all those special requests
to these special routes
are automatically handled by the NextAuth package.
We can still also define our own routes,
like the signup route in addition,
as long as we don't override one
of the built-in routes NextAuth exposes.
If you wanna find out which routes it creates,
you can check out the official NextAuth documentation
and click on REST API there
and these are the different routes,
they all start with /api/auth as you can tell,
which will be handled by NextAuth.
So we shouldn't clash with those.
Now, how do we then use NextAuth here?
We, first of all, import it
in this square bracket nextauth.js file.
We import NextAuth from next-auth
and then with this imported,
we export NextAuth as a default
but not like this but we execute it.
So NextAuth here is a function,
which we can execute
and when we execute it,
it returns a new function, a handler function.
It needs to because this is still an API route
and therefore, an API route still needs
to return a function.
It needs to export a function.
But that exported handler function
is created by NextAuth by calling NextAuth.
And when we call NextAuth,
we can pass a configuration object to it.
That's the object which allows us
to configure NextAuth's behavior.
And you can dive into the official docs,
configuration options to learn about all the options you can
and must set here.
Now, in our case, we wanna set the providers option here
and set this to an array.
And here we need to import something else.
We need to import Providers
from next-auth/providers like this
and then here in the providers array,
we use Providers dot
and then here you can choose from the long list
of supported providers.
And again, check out the providers docs
to learn more about the concrete way
of setting up the different providers.
But here, we need the Credentials provider,
which means that we bring our own credentials.
Now, this takes a configuration object itself
and here, we now can configure a couple of things.
For example, we can set the credentials key
and then here define what our credentials are.
Email and password.
We could use that to let NextAuth generate a login
form for us.
Here, however, I don't wanna let NextAuth generate
a form for me.
I already have one
and hence, I'll not set credentials.
The only thing we need to set here actually is authorize
and authorize turns out to be a method.
It's a method which Next.js will call for us
when it receives a incoming login request.
At the moment, we're not sending such requests yet
but we will soon do so.
Authorize is an async function,
so it returns a promise,
and as a argument, we get the credentials
that were submitted.
And that is the object with the data we submitted,
for example, email, password.
In here, we now have to bring our own authorization logic,
check if the credentials are valid
and tell the user if that's not the case.
For example, throw an error if that's not the case.
Therefore, in here we again wanna connect to the database
and hence, import connectToDatabase
from the lib folder.
And get hold of that client here
by awaiting this.
We also wanna close the client later, of course.
So let's already do that
so that we don't forget it.
And then we wanna find out if we have a user
for the entered email
and if we have a user with that password then.
So if that password is correct.
Hence, let's first of all check if we have a user.
For that, I'll have my usersCollection,
which I get access to with client.db.collection users.
Let's do that in one go.
And then we can use the usersCollection
to find one user where the email
is equal to credentials.email and I'm expecting
that credentials will have a email property.
I can expect this
because it will be us who later set those credentials
when we send a request from the client-side code.
So I expect to have a email field
in the credentials object
and then I check if I find a user with that email.
We should await this
because it's an asynchronous task.
Returning a promise
and then here we got our user.
Now, if we don't find a user, if not user,
then we know that there is no user with that email address.
In that case, we can throw a new Error
where we say No user found, or anything like that.
Now, when you throw an error inside of authorize,
authorize will reject that promise,
which it generates
and it will then by default redirect the client
to another page.
But we will be able to override this
so that we stay on this login page
and maybe just show an error there.
But that's something we'll do later.
So here we throw an error.
But if we make it past this if check,
we know that we found a user for the email address.
That's nice but we now also need
to find out whether the password is correct.
Now, keep in mind that we hashed the password,
so we will not be able
to just compare the submitted password
with the stored password.
The stored password will never be equal
because it's hashed.
Now, we can't unhash the password
but we can hash the incoming password again
to find out if it's equal.
But if we just call hashPassword again,
we would also generate a different hash
because it's not always the same.
But bcrypt.js has a method that helps us.
It has the compare function.
And that allows us to find out if a plaintext password
matches a hashedPassword.
So if that plaintext password could result
in the same hashedPassword
and if that's the case,
we know that the passwords are equal.
For this we here export another async function.
verifyPassword could be the function name
and here we then get the plaintext password
and the hashedPassword,
so both passwords.
And then we run compare
and there we pass in the plaintext password
and the hashedPassword.
And await this because compare does return a promise.
And that will return a Boolean,
which is true if the passwords are equal
and false otherwise
and hence here I'll return that Boolean.
Then back in our authorize function,
we can run verifyPassword
and pass in credentials.password,
which is the submitted password
with which the user tries to log in.
And as a second argument,
the password's stores in the database
so in that user object.
So we pass in user.password
as a second argument,
accessing that password field,
which is stored in MongoDB.
We need to await this
because verifyPassword is a async function
and hence returns a promise.
And then we got our isValid result here.
Now, if this is not valid,
if the password is not valid,
then again, we wanna throw an error.
We could say Could not log you in.
Something like this.
If we make it past this if check as well,
we know that we have a user for the given email address
and that the password is valid.
So in that case,
the user is logged in and in that case,
here we should return an object.
If we return an object inside of authorize,
we let nextAuth know that authorization succeeded
so that the user is logged in.
And this object will then actually be encoded
into that JSON Web Token.
Hence here, we could, for example,
include the user email.
We should not pass the entire user object
because we don't wanna include the password.
Even though it's hashed, we don't wanna expose that
to the client.
So that's the object we return
and that will then be encoded in a JSON Web Token.
But speaking of that,
to make sure that a JSON Web Token is created,
we should go to the NextAuth config object
and besides setting up our providers,
also add the sessions option here.
That's another option which you can set up here.
Actually, it's session, not sessions.
It's a object where you can configure
how that session for an authenticated user will be managed
and there, you've got a jwt key,
which should be set to true
so that JSON Web Tokens are being used.
For some other authentication providers,
you have other ways of managing this.
For example, that a session is stored in a database
but for credential-based authentication,
for this provider, you must use JWT
and set this to true.
As you see, it I would be set to true automatically
if you don't specify a database
and we aren't specifying a database
because we handle all database access manually anyways
with this specific provider
but nonetheless, setting it
to true explicitly also isn't too bad.
Now, if you do use other providers,
then you might, of course,
we able to add a database and user session.
It really depends on which authentication provider you use.
For example, if you use the email authentication provider
where you get an email with the magic link sent to you,
then you need to add a database
in which the emails will be stored
and you don't need to use JWT.
But that's just a side note.
Here we are using JWT.
We are using credentials and hence we need
to configure it like this.
Now, before using this API route though,
before sending requests,
there's one thing I wanna do.
Client.close, of course, should be called
before we return
and also should be called if we throw an error
so that we still close that database connection.
That is something we should do here.
And actually I just saw, this should be hashedPassword,
of course, not hashPassword.
So that's one thing.
That's now the backend.
That's the authentication API route using NextAuth.
Let's now find out what we do on the front end
on the client to log users in.