Authenticating a User

At stage of the application, 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 from the fixture users.json.

In this challenge you will rewrite the authenticate() function of the AuthService to do the following:

But first, let’s take a look at how Authentication works in the application. If you prefer, you can skip straight to Implementing Authentication.

Authorization & Authentication

Authorizing users works by checking the email and hashed password against information in the database.

When a user submits the Sign In form from the UI, the following process occurs:

  1. A route handler for the /login route in src/main/java/neoflix/routes/AuthRoutes.java listens for a POST request.

  2. The login route calls the authenticate() method in the AuthService with the username and password from the request.

The authenticate() method performs the following actions:

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

  2. If the user can’t be found, throw a ValidationException.

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

  4. If the passwords do not match, throw a ValidationException.

  5. Otherwise, return an object containing the user’s safe properties (email, name, userId), 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 null or an error if the credentials are incorrect.

Once a user is authorized a JWT token is generated (using the Auth0 JWT library) and passed back to the front-end to be used in the Authorization header until the user signs out or the token expires.

For authenticated users the application uses a pre-processor before() the routes to verify the authentication for each request, which is delegated to: AppUtils.handleAuthAndSetUser.

There the Authorization header is extracted and validated using the Auth0 JWT library. If that is successful the userId is set as a request attribute which can be accessed by the route implementation.

Implementing Authentication

To implement database authentication, you will modify the authenticate method in the AuthService.

java
neoflix/services/AuthService.java
public Map<String,Object> authenticate(String email, String plainPassword) {
    // TODO: Authenticate the user from the database
    var foundUser = users.stream().filter(u -> u.get("email").equals(email)).findAny();
    if (foundUser.isEmpty())
        throw new ValidationException("Incorrect email", Map.of("email","Incorrect email"));
    var user = foundUser.get();
    if (!plainPassword.equals(user.get("password")) && 
        !AuthUtils.verifyPassword(plainPassword,(String)user.get("password"))) { // 
        throw new ValidationException("Incorrect password", Map.of("password","Incorrect password"));
    }
    String sub = (String) user.get("userId");
    String token = AuthUtils.sign(sub, userToClaims(user), jwtSecret);
    return userWithToken(user, token);
}

Your challenge is to update the authenticate() method to perform the following actions:

Open a new Session

First, open a new session:

java
// Open a new session
try (var session = driver.session()) {

  // Do something with the session...

  // Close the session automatically in try-with-resources block
}

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.

java
// Find the User node within a Read Transaction
var user = session.executeRead(tx -> {
    String statement = "MATCH (u:User {email: $email}) RETURN u";
    var res = tx.run(statement, Values.parameters("email", email));
    return res.single().get("u").asMap();

});

Verify The User Exists

If no records are returned, you can safely assume that the user does not exist in the database, then the single() method on Result will throw a NoSuchRecordException.

In this case, an ValidationException is thrown.

java
} catch(NoSuchRecordException e) {
    throw new ValidationException("Incorrect email", Map.of("email","Incorrect email"));
}

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 verify() function that can be used to compare a string against a previously encrypted value, we encapsulate that in AuthUtils.verifyPassword().

If the AuthUtils.verifyPassword() method returns false, the passwords do not match and the method should throw a new ValidationException.

java
// Check password
if (!AuthUtils.verifyPassword(plainPassword, (String)user.get("password"))) {
    throw new ValidationException("Incorrect password", Map.of("password","Incorrect password"));
}

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.

java
String sub = (String)user.get("userId");
String token = AuthUtils.sign(sub, userToClaims(user), jwtSecret);
return userWithToken(user, token);

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:
java
neoflix/services/AuthService.java
public Map<String,Object> authenticate(String email, String plainPassword) {
    // Open a new Session
    try (var session = this.driver.session()) {
        // Find the User node within a Read Transaction
        var user = session.executeRead(tx -> {
            String statement = "MATCH (u:User {email: $email}) RETURN u";
            var res = tx.run(statement, Values.parameters("email", email));
            return res.single().get("u").asMap();

        });

        // Check password
        if (!AuthUtils.verifyPassword(plainPassword, (String)user.get("password"))) {
            throw new ValidationException("Incorrect password", Map.of("password","Incorrect password"));
        }

        String sub = (String)user.get("userId");
        String token = AuthUtils.sign(sub, userToClaims(user), jwtSecret);
        return userWithToken(user, token);
    } catch(NoSuchRecordException e) {
        throw new ValidationException("Incorrect email", Map.of("email","Incorrect email"));
    }
}

Testing

To test that this functionality has been correctly implemented, run the following code in a new terminal session:

sh
Running the test
mvn test -Dtest=neoflix._05_AuthenticationTest#authenticateUser

The test file is located at src/test/java/neoflix/_05_AuthenticationTest.java.

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 save the current user’s movie ratings to the database.