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:
-
A route handler for the
/login
route inNeoflix/Controllers/AuthController.cs
listens for aPOST
request. -
The
login
route calls theAuthenticateAsync()
method in theAuthService
with the username and password from the request.
The AuthenticateAsync()
method performs the following actions:
-
Attempt to find the user by their email address.
-
If the user can’t be found, return
null
. -
Compare the encrypted password in the database against the unencrypted password sent with the request.
-
If the passwords do not match, return
null
-
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
.
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:
// 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.
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
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.
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.
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:
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:
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:
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.
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 save the current user’s movie ratings to the database.