At stage of the project, a user can register, but they are still unable to sign in.
As with the previous Challenge, the authenticate()
method is currently hard coded to accept only the email graphacademy@neo4j.com
and password letmein
.
In this challenge you will rewrite the authenticate()
function of the AuthService
to find the User node with the corresponding email and compare the password before issuing a JWT token.
But first, let’s take a look at how Authentication works in the application. If you prefer, you can skip straight to Implementing Authentication.
Passport & Authentication
This project uses a Local Strategy exported from the passport-local
library to handle the authentication request.
The purpose of a Local Strategy is to authenticate a user using a username and password.
When a user submits the Sign In form from the UI, the following process occurs:
-
A route handler in
src/routes/auth.routes.js
listens for aPOST
request. -
The
passport.authenticate
middleware triggers the local authentication strategy. -
This calls the callback function defined in
src/passport/neo4j.strategy.js
. -
The strategy creates a new instance of the
AuthService
and calls theauthenticate()
method with the username and password defined in the request.
The authenticate()
method performs the following actions:
-
Attempt to find the user by their email address.
-
If the user does not exist, return
false
. -
Compare the encrypted password in the database against the unencrypted password sent with the request.
-
If the passwords do not match, return
false
. -
Otherwise, return an object containing the user’s safe properties, and a JWT token with a set of claims that can be used in the UI.
For this strategy to work correctly, the authenticate()
method must return an object which represents the user on successful login, or return false
if the credentials are incorrect.
If the returned value is anything other than false, the value will be appended to the request, and will be accessible through req.user
on any route handlers that use the passport.authenticate
middleware.
Implementing Authentication
To implement database authentication, you will modify the authenticate
method in the AuthService
.
async authenticate(email, unencryptedPassword) {
// TODO: Authenticate the user from the database
if (email === 'graphacademy@neo4j.com' && unencryptedPassword === 'letmein') {
const { password, ...claims } = user.properties
return {
...claims,
token: jwt.sign(claims, JWT_SECRET)
}
}
return false
}
Your challenge is to update the authenticate()
method to perform the following actions:
src/services/auth.service.js
→
Open a new Session
First, open a new session:
// Open a new session
const session = this.driver.session()
Find the User node within a Read Transaction
Use a MATCH
query to find a :User
node with the email address passed to the method as a parameter.
// Find the user node within a Read Transaction
const res = await session.executeRead(
tx => tx.run(
'MATCH (u:User {email: $email}) RETURN u',
{ email }
)
)
Close the Session
Because the results have already been consumed by the driver, and no more interactions with the database will take place, the session can now be closed.
// Close the session
await session.close()
Verify The User Exists
If no records are returned, you can safely assume that the user does not exist in the database.
In this case, a false
value should be returned.
// Verify the user exists
if ( res.records.length === 0 ) {
return false
}
Compare Passwords
Next, you must verify that the unencrypted password matches the encrypted password saved as a property against the :User
node.
The bcrypt
library used to encrypt the password also includes a compare()
function that can be used to compare a string against a previously encrypted value.
If the compare()
function returns false, the passwords do not match and the method should also return false
// Compare Passwords
const user = res.records[0].get('u')
const encryptedPassword = user.properties.password
const correct = await compare(unencryptedPassword, encryptedPassword)
if ( correct === false ) {
return false
}
Return User Details
As with the register()
method, the UI expects a JWT token to be returned along with the response.
The code is already written to the example, so this can be re-purposed at the end of the method.
return {
...safeProperties,
token: jwt.sign(this.userToClaims(safeProperties), JWT_SECRET),
}
Once you have applied these changes to the authenticate()
method, scroll to Testing to verify that the method works as expected.
Working Solution
Click here to reveal the completed authenticate()
method:
async authenticate(email, unencryptedPassword) {
// Open a new session
const session = this.driver.session()
// Find the user node within a Read Transaction
const res = await session.executeRead(
tx => tx.run(
'MATCH (u:User {email: $email}) RETURN u',
{ email }
)
)
// Close the session
await session.close()
// Verify the user exists
if ( res.records.length === 0 ) {
return false
}
// Compare Passwords
const user = res.records[0].get('u')
const encryptedPassword = user.properties.password
const correct = await compare(unencryptedPassword, encryptedPassword)
if ( correct === false ) {
return false
}
// Return User Details
const { password, ...safeProperties } = user.properties
return {
...safeProperties,
token: jwt.sign(this.userToClaims(safeProperties), JWT_SECRET),
}
}
Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
npm run test 05
The test file is located at test/challenges/05-authentication.spec.js
.
Are you stuck? Click here for help
If you get stuck, you can see a working solution by checking out the 05-authentication
branch by running:
git checkout 05-authentication
You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.
Verifying the Test
This test creates a new :User
node in the database with an email address of authenticated@neo4j.com
using the register()
method on the AuthService
and then attempts to verify the user using the authenticate
method.
Hit the Check Database button below to verify that the test has been successfully run.
Hint
At the end of the test, a piece of code finds the User node and sets the authenticatedAt
property to the current date and time using the Cypher datetime()
function.
As long as this value is within the past 24 hours, the test should pass.
You can run the following query to check for the user within the database.
If the shouldVerify
value returns true, the verification should be successful.
MATCH (u:User {email: 'authenticated@neo4j.com'})
RETURN u.email, u.authenticatedAt,
u.authenticatedAt >= datetime() - duration('PT24H') AS shouldVerify
Solution
The following statement will mimic the behaviour of the test, merging a new :User
node with the email address authenticated@neo4j.com
, assigning a random UUID value to the .userId
property and setting an .authenticatedAt
property to the current date and time.
MERGE (u:User {email: "authenticated@neo4j.com"})
SET u.authenticatedAt = datetime()
Once you have run this statement, click Try again…* to complete the challenge.
Lesson Summary
In this Challenge, you have updated the AuthService
to authenticate a User using the data held in the Sandbox database.
In the next Challenge, you will write ratings to the database and learn how to use the int()
function to save JavaScript integers in Neo4j.