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 AuthDAO
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.
How Authentication Works
When a user attempts to access an API endpoint that requires authentication, the server checks for a JWT token.
When a user registers or signs in, a JWT token is generated and appended to the User record.
This token is then stored by the UI and appended to Bearer
to create an authorization
header.
The token generation happens in the AuthDAO
using the _generate_token()
method.
When The API receives a request which includes the authorization
header, the Flask-JWT library attempts to decode the value and makes the payload available by importing current_user
from flask_jwt
.
Login Route
When a user submits the login form on the website, a request is sent to http://localhost:3000/api/login
with a username and password.
Implementing Authentication
To implement database authentication, you will modify the authenticate
method in the AuthDAO
.
def authenticate(self, email, plain_password):
# TODO: Implement Login functionality
if email == "graphacademy@neo4j.com" and plain_password == "letmein":
# Build a set of claims
payload = {
"userId": "00000000-0000-0000-0000-000000000000",
"email": email,
"name": "GraphAcademy User",
}
# Generate Token
payload["token"] = self._generate_token(payload)
return payload
else:
return False
Your challenge is to update the authenticate()
method to perform the following actions:
api/dao/auth.py
→
Create a transaction function to find the User
The Transaction function should be a simple query that uses Cypher to look up a :User
node by the email
parameter provided and return a single result.
def get_user(tx, email):
# Get the result
result = tx.run("MATCH (u:User {email: $email}) RETURN u",
email=email)
# Expect a single row
first = result.single()
# No records? Return None
if first is None:
return None
# Get the `u` value returned by the Cypher query
user = first.get("u")
return user
Execute the function within a Read Transaction
After opening up a new session, call the execute_read
method to execute the get_user
function above.
with self.driver.session() as session:
user = session.execute_read(get_user, email=email)
Verify the User exists
If the user does not exist, then get_user
will return None
.
In this case, return False
.
# User not found, return False
if user is None:
return False
Compare Passwords
The authenticate()
method uses the hashpw
function imported from bcrypt
to encrypt the password.
The library also provides a checkpw
function for comparing a plain text value against the previously encrypted value.
If the check fails, return False
.
# Passwords do not match, return false
if bcrypt.checkpw(plain_password.encode('utf-8'), user["password"].encode('utf-8')) is False:
return False
Return User Details
Finally, if the user exists and the password comparison returns true, generate a JWT token and return it along with information about the User.
# Generate JWT Token
payload = {
"userId": user["userId"],
"email": user["email"],
"name": user["name"],
}
payload["token"] = self._generate_token(payload)
return payload
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:
def authenticate(self, email, plain_password):
def get_user(tx, email):
# Get the result
result = tx.run("MATCH (u:User {email: $email}) RETURN u",
email=email)
# Expect a single row
first = result.single()
# No records? Return None
if first is None:
return None
# Get the `u` value returned by the Cypher query
user = first.get("u")
return user
with self.driver.session() as session:
user = session.execute_read(get_user, email=email)
# User not found, return False
if user is None:
return False
# Passwords do not match, return false
if bcrypt.checkpw(plain_password.encode('utf-8'), user["password"].encode('utf-8')) is False:
return False
# Generate JWT Token
payload = {
"userId": user["userId"],
"email": user["email"],
"name": user["name"],
}
payload["token"] = self._generate_token(payload)
return payload
Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
pytest tests/05_authentication__test.py
The test file is located at tests/05_authentication__test.py
.
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 AuthDAO
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 AuthDAO
to authenticate a User using the data held in the Sandbox database.
In the next Challenge, you will save user ratings to the database.