Authenticating a User

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:

  1. A route handler in src/routes/auth.routes.js listens for a POST request.

  2. The passport.authenticate middleware triggers the local authentication strategy.

  3. This calls the callback function defined in src/passport/neo4j.strategy.js.

  4. The strategy creates a new instance of the AuthService and calls the authenticate() method with the username and password defined in the request.

The authenticate() method performs the following actions:

  1. Attempt to find the user by their email address.

  2. If the user does not exist, return false.

  3. Compare the encrypted password in the database against the unencrypted password sent with the request.

  4. If the passwords do not match, return false.

  5. 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.

js
src/services/auth.service.js
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:

Open src/services/auth.service.js

Open a new Session

First, open a new session:

js
// 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.

js
// 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.

js
// 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.

js
// 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

js
// 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.

js
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:
js
src/services/auth.service.js
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:

sh
Running the test
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:

sh
Check out the 05-authentication branch
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.

cypher
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.

cypher
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.