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 AuthenticateAsync() 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 AuthenticateAsync() 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 Neoflix/Controllers/AuthController.cs listens for a POST request.

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

The AuthenticateAsync() method performs the following actions:

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

  2. If the user can’t be found, return null.

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

  4. If the passwords do not match, return null

  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 AuthenticateAsync() 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 AuthenticateSync() method in the AuthService.

c#
Neoflix/Services/AuthService.cs
public Task<Dictionary<string, object>> AuthenticateAsync(string email, string plainPassword)
{
    if (email == "graphacademy@neo4j.com" && plainPassword == "letmein")
    {
        var exampleUser = new Dictionary<string, object>
        {
            ["identity"] = 1,
            ["properties"] = new Dictionary<string, object>
            {
                ["userId"] = 1,
                ["email"] = "graphacademy@neo4j.com",
                ["name"] = "Graph Academy"
            }
        };

        var safeProperties = SafeProperties(exampleUser["properties"] as Dictionary<string, object>);

        safeProperties.Add("token", JwtHelper.CreateToken(GetUserClaims(safeProperties)));

        return Task.FromResult(safeProperties);
    }

    return Task.FromResult<Dictionary<string,object>>(null);
}

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

Open a new Session

First, open a new session:

c#
// Open a new session
using var session = _driver.AsyncSession();
// Do something with the 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.

c#
var cursor = await tx.RunAsync("MATCH (u: User {email: $email}) RETURN u", new { email });

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, return null

c#
if (!await cursor.FetchAsync())
{
    // no records
    return null;
}    

Compare Passwords

Next, you must verify that the unencrypted password matches the encrypted password saved as a property against the :User node.

The BCryptNet library used to encrypt the password also includes a Verify() function that can be used to compare a string against a previously encrypted value.

If the BCryptNet.Verify() function returns false, the passwords do not match and the method should return null.

c#
if (!BCryptNet.Verify(plainPassword, user["password"].As<string>()))
    return null;

Return User Details

As with the RegisterSync() 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.

c#
var safeProperties = SafeProperties(user);
safeProperties.Add("token", JwtHelper.CreateToken(GetUserClaims(safeProperties)));
return safeProperties;    

Once you have applied these changes to the AuthenticateAsync() method, scroll to Testing to verify that the method works as expected.

Working Solution

Click here to reveal the completed AuthenticateAsync() method:
c#
Neoflix/Services/AuthService.cs
public async Task<Dictionary<string, object>> AuthenticateAsync(string email, string plainPassword)
{
    await using var session = _driver.AsyncSession();
    var user = await session.ExecuteReadAsync(async tx =>
    {

        var cursor = await tx.RunAsync("MATCH (u: User {email: $email}) RETURN u", new { email });

        if (!await cursor.FetchAsync())
        {
            // no records
            return null;
        }    

        var record = cursor.Current;
        var userProperties = record["u"].As<INode>().Properties;
        return userProperties.ToDictionary(x => x.Key, x => x.Value);
    });

    if (user == null)
        return null;

    if (!BCryptNet.Verify(plainPassword, user["password"].As<string>()))
        return null;

    var safeProperties = SafeProperties(user);
    safeProperties.Add("token", JwtHelper.CreateToken(GetUserClaims(safeProperties)));
    return safeProperties;    
}

Testing

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

sh
Running the test
dotnet test --logger "console;verbosity=detailed" --filter "Neoflix.Challenges._05_Authentication"

The test file is located at Neoflix.Challenges/05-Authentication.cs.

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 RegisterAsync() 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.