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:
-
A route handler for the
/login
route insrc/main/java/neoflix/routes/AuthRoutes.java
listens for aPOST
request. -
The
login
route calls theauthenticate()
method in theAuthService
with the username and password from the request.
The authenticate()
method performs the following actions:
-
Attempt to find the user by their email address.
-
If the user can’t be found, throw a ValidationException.
-
Compare the encrypted password in the database against the unencrypted password sent with the request.
-
If the passwords do not match, throw a ValidationException.
-
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
.
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:
// 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.
// 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.
} 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.
// 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.
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:
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:
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:
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 save the current user’s movie ratings to the database.