This project contains tasks for learning to create a user authentication service.
- SQLAlchemy 1.3.x
- pycodestyle 2.5
- bcrypt
- python3 3.7
-
0. User model
user.py contains a SQLAlchemy model namedUser
for a database table namedusers
(by using the mapping declaration of SQLAlchemy) and meets the following requirements:- The model will have the following attributes:
id
, the integer primary key.email
, a non-nullable string.hashed_password
, a non-nullable string.session_id
, a nullable string.reset_token
, a nullable string.
- The model will have the following attributes:
-
1. create user
db.py contains a completion of theDB
class provided below to implement theadd_user
method according to the given requirements:-
#!/usr/bin/env python3 """DB module. """ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session from sqlalchemy.ext.declarative import declarative_base from user import Base class DB: """DB class. """ def __init__(self) -> None: """Initialize a new DB instance. """ self._engine = create_engine("sqlite:///a.db", echo=False) Base.metadata.drop_all(self._engine) Base.metadata.create_all(self._engine) self.__session = None @property def _session(self) -> Session: """Memoized session object. """ if self.__session is None: DBSession = sessionmaker(bind=self._engine) self.__session = DBSession() return self.__session
- Note that
DB._session
is a private property and hence should NEVER be used from outside the DB class. - Implement the
add_user
method, which has two required string arguments:email
andhashed_password
, and returns aUser
object. The method should save the user to the database. No validations are required at this stage.
-
-
2. Find user
db.py contains the following updates:- Implement the
DB.find_user_by
method. This method takes in arbitrary keyword arguments and returns the first row found in theusers
table as filtered by the method’s input arguments. No validation of input arguments required at this point. - Make sure that SQLAlchemy’s
NoResultFound
andInvalidRequestError
are raised when no results are found, or when wrong query arguments are passed, respectively. - Warning:
NoResultFound
has been moved fromsqlalchemy.orm.exc
tosqlalchemy.exc
between the version 1.3.x and 1.4.x of SQLAchemy - please make sure you are importing it fromsqlalchemy.orm.exc
.
- Implement the
-
3. update user
db.py contains the following updates:- Implement the
DB.update_user
method that takes as arguments a requireduser_id
integer, an arbitrary keyword arguments, and returnsNone
. - The method will use
find_user_by
to locate the user to update, then will update the user’s attributes as passed in the method’s arguments then commit changes to the database. - If an argument that does not correspond to a user attribute is passed, raise a
ValueError
.
- Implement the
-
4. Hash password
auth.py contains a_hash_password
method that takes in a password string arguments and returns bytes, which is a salted hash of the input password, hashed withbcrypt.hashpw
. -
5. Register user
auth.py contains the following updates:- Implement the
Auth.register_user
in theAuth
class provided below:from db import DB class Auth: """Auth class to interact with the authentication database. """ def __init__(self): self._db = DB()
- Note that
Auth._db
is a private property and should NEVER be used from outside the class. Auth.register_user
should take mandatoryemail
andpassword
string arguments and return aUser
object.- If a user already exist with the passed
email
, raise aValueError
with the messageUser <user's email> already exists
. - If not, hash the password with
_hash_password
, save the user to the database usingself._db
and return theUser
object.
- Implement the
-
6. Basic Flask app
app.py contains a basic Flask app with the following requirements:- Create a Flask app that has a single
GET
route ("/"
) and useflask.jsonify
to return a JSON payload of the form:{"message": "Bienvenue"}
- Add the following code at the end of the module:
if __name__ == "__main__": app.run(host="0.0.0.0", port="5000")
- Create a Flask app that has a single
-
7. Register user
app.py contains the following updates:- Implement the end-point to register a user. Define a
users
function that implements thePOST /users
route. - Import the
Auth
object and instantiate it at the root of the module as such:from auth import Auth AUTH = Auth()
- The end-point should expect two form data fields:
"email"
and"password"
. If the user does not exist, the end-point should register it and respond with the following JSON payload:{"email": "<registered email>", "message": "user created"}
- If the user is already registered, catch the exception and return a JSON payload of the form
and return a 400 status code.
{"message": "email already registered"}
- Remember that you should only use
AUTH
in this app.DB
is a lower abstraction that is proxied byAuth
.
- Implement the end-point to register a user. Define a
-
8. Credentials validation
auth.py contains the following updates:- Implement the
Auth.valid_login
method. It should expectemail
andpassword
required arguments and return a boolean. - Try locating the user by email. If it exists, check the password with
bcrypt.checkpw
. If it matches returnTrue
. In any other case, returnFalse
.
- Implement the
-
9. Generate UUIDs
auth.py contains the following updates:- Implement a
_generate_uuid
function in theauth
module. The function should return a string representation of a new UUID. Use theuuid
module. - Note that the method is private to the
auth
module and should NOT be used outside of it.
- Implement a
-
10. Get session ID
auth.py contains the following updates:- Implement the
Auth.create_session
method. It takes anemail
string argument and returns the session ID as a string. - The method should find the user corresponding to the email, generate a new UUID and store it in the database as the user’s
session_id
, then return the session ID. - Remember that only public methods of
self._db
can be used.
- Implement the
-
11. Log in
app.py contains the following updates:- Implement a
login
function to respond to thePOST /sessions
route. - The request is expected to contain form data with
"email"
and a"password"
fields. - If the login information is incorrect, use
flask.abort
to respond with a 401 HTTP status. - Otherwise, create a new session for the user, store it the session ID as a cookie with key
"session_id"
on the response and return a JSON payload of the form:{"email": "<user email>", "message": "logged in"}
- Implement a
-
12. Find user by session ID
auth.py contains the following updates:- Implement the
Auth.get_user_from_session_id
method. It takes a singlesession_id
string argument and returns the correspondingUser
orNone
. - If the session ID is
None
or no user is found, returnNone
. Otherwise return the corresponding user. - Remember to only use public methods of
self._db
.
- Implement the
-
13. Destroy session
auth.py contains the following updates:- Implement
Auth.destroy_session
. The method takes a singleuser_id
integer argument and returnsNone
. - The method updates the corresponding user’s session ID to
None
. - Remember to only use public methods of
self._db
.
- Implement
-
14. Log out
app.py contains the following updates:- Implement a
logout
function to respond to theDELETE /sessions
route. - The request is expected to contain the session ID as a cookie with key
"session_id"
. - Find the user with the requested session ID. If the user exists destroy the session and redirect the user to
GET /
. If the user does not exist, respond with a 403 HTTP status.
- Implement a
-
15. User profile
app.py contains the following updates:- Implement a
profile
function to respond to theGET /profile
route. - The request is expected to contain a
session_id
cookie. Use it to find the user. If the user exist, respond with a 200 HTTP status and the following JSON payload:{"email": "<user email>"}
- If the session ID is invalid or the user does not exist, respond with a 403 HTTP status.
- Implement a
-
16. Generate reset password token
auth.py contains the following updates:- Implement the
Auth.get_reset_password_token
method. It takes anemail
string argument and returns a string. - Find the user corresponding to the email. If the user does not exist, raise a
ValueError
exception. If it exists, generate a UUID and update the user’sreset_token
database field. Return the token.
- Implement the
-
17. Get reset password token
app.py contains the following updates:- Implement a
get_reset_password_token
function to respond to thePOST /reset_password
route. - The request is expected to contain form data with the
"email"
field. - If the email is not registered, respond with a 403 status code. Otherwise, generate a token and respond with a 200 HTTP status and the following JSON payload:
{"email": "<user email>", "reset_token": "<reset token>"}
- Implement a
-
18. Update password
auth.py contains the following updates:- Implement the
Auth.update_password
method. It takesreset_token
string argument and apassword
string argument and returnsNone
. - Use the
reset_token
to find the corresponding user. If it does not exist, raise aValueError
exception. - Otherwise, hash the password and update the user’s
hashed_password
field with the new hashed password and thereset_token
field toNone
.
- Implement the
-
19. Update password end-point
app.py contains the following updates:- Implement the
update_password
function in theapp
module to respond to thePUT /reset_password
route. - The request is expected to contain form data with fields
"email"
,"reset_token"
and"new_password"
. - Update the password. If the token is invalid, catch the exception and respond with a 403 HTTP code.
- If the token is valid, respond with a 200 HTTP code and the following JSON payload:
{"email": "<user email>", "message": "Password updated"}
- Implement the
-
20. End-to-end integration test
- Start the Flask app you created in the previous tasks. Open a new terminal window.
- Create a new module called main.py. Create one function for each of the following sub tasks. Use the
requests
module to query your web server for the corresponding end-point. Useassert
to validate the response’s expected status code and payload (if any) for each sub task:register_user(email: str, password: str) -> None
.log_in_wrong_password(email: str, password: str) -> None
.log_in(email: str, password: str) -> str
.profile_unlogged() -> None
.profile_logged(session_id: str) -> None
.log_out(session_id: str) -> None
.reset_password_token(email: str) -> str
.update_password(email: str, reset_token: str, new_password: str) -> None
.
- Copy the following code at the end of the
main
module:EMAIL = "guillaume@holberton.io" PASSWD = "b4l0u" NEW_PASSWD = "t4rt1fl3tt3" if __name__ == "__main__": register_user(EMAIL, PASSWD) log_in_wrong_password(EMAIL, NEW_PASSWD) profile_unlogged() session_id = log_in(EMAIL, PASSWD) profile_logged(session_id) log_out(session_id) reset_token = reset_password_token(EMAIL) update_password(EMAIL, reset_token, NEW_PASSWD) log_in(EMAIL, NEW_PASSWD)
- Run
python3 main.py
. If everything is correct, you should see no output.