HomeLearnHow-toAdding Authentication to Your FARM Stack App

Adding Authentication to Your FARM Stack App

Published: Mar 17, 2021

  • MongoDB
  • Atlas
  • Python
  • ...

By Aaron Bassett

 and Mark Smith

Rate this article

If you have not read my Introduction to FARM stack tutorial, I would urge you to do that now and then come back. This guide assumes you have already read and understood the previous article so some things might be confusing or opaque if you have not.

An important part of many web applications is user management, which can be complex with lots of different scenarios to cover: registration, logging in, logging out, password resets, protected routes, and so on. In this tutorial, we will look at how you can integrate the FastAPI Users package into your FARM stack.

#Prerequisites

  • Python 3.9.0
  • A MongoDB Atlas cluster. Follow the "Get Started with Atlas" guide to create your account and MongoDB cluster. Keep a note of your database username, password, and connection string as you will need those later.
  • A MongoDB Realm App connected to your cluster. Follow the "Create a Realm App (Realm UI)" guide and make a note of your Realm App ID.

#Getting Started

Let's begin by cloning the sample code source from GitHub

1git clone git@github.com:mongodb-developer/FARM-Auth.git

Once you have cloned the repository, you will need to install the dependencies. I always recommend that you install all Python dependencies in a virtualenv for the project. Before running pip, ensure your virtualenv is active. The requirements.txt file is within the back end folder.

1cd FARM-Auth/backend
2pip install -r requirements.txt

It may take a few moments to download and install your dependencies. This is normal, especially if you have not installed a particular package before.

You'll need two new configuration values for this tutorial. To get them, log into Atlas and create a new Realm App by selecting the Realm tab at the top of the page, and then clicking on "Create a New App" on the top-right of the page.

Create a new Realm App

Configure the Realm app to connect to your existing cluster:

Configure a new Realm App

You should see your Realm app's ID at the top of the page. Copy it and keep it somewhere safe. It will be used for your application's REALM_APP_ID value.

Get your app's Id

Click on the "Authentication" option on the left-hand side of the page. Then select the "Edit" button next to "Custom JWT Authentication". Ensure the first option, "Provider Enabled" is set to "On". Check that the Signing Algorithm is set to "HS256". Now you need to create a signing key, which is just a set of 32 random bytes. Fortunately, Python has a quick way to securely create random bytes! In your console, run the following:

1python -c 'import secrets; print(secrets.token_hex(32))'

Running that line of code will print out some random characters to the console. Type "signing_key" into the "Signing Key (Secret Name)" text box and then click "Create 'signing_key'" in the menu that appears underneath. A new text box will appear for the actual key bytes. Paste in the random bytes you generated above. Keep the random bytes safe for the moment. You'll need them for your application's "JWT_SECRET_KEY" configuration value.

Configure a JWT signing key.

Now you have all your configuration values, you need to set the following environment variables (make sure that you substitute your actual credentials).

1export DEBUG_MODE=True
2export DB_URL="mongodb+srv://<username>:<password>@<url>/<db>?retryWrites=true&w=majority"
3export DB_NAME="farmstack"
4export JWT_SECRET_KEY="<secret value>"
5export REALM_APP_ID="<realm id>"

Set these values appropriately for your environment, ensuring that REALM_APP_ID and JWT_SECRET_KEY use the values from above. Remember, anytime you start a new terminal session, you will need to set these environment variables again. I use direnv to make this process easier. Storing and loading these values from a .env file is another popular alternative.

The final step is to start your FastAPI server.

1uvicorn main:app --reload

Once the application has started, you can view it in your browser at http://127.0.0.1:8000/docs.

Screenshot of FARM app Swagger UI

You may notice that we now have a lot more endpoints than we did in the FARM stack Intro. These routes are all provided by the FastAPI Users package. I have also updated the todo app routes so that they are protected. This means that you can no longer access these routes, unless you are logged in.

If you try to access the List Tasks route, for example, it will fail with a 401 Unauthorized error. In order to access any of the todo app routes, we need to first register as a new user and then authenticate. Try this now. Use the /auth/register and /auth/jwt/login routes to create and authenticate as a new user. Once you are successfully logged in, try accessing the List Tasks route again. It should now grant you access and return an HTTP status of 200. Use the Atlas UI to check the new farmstack.users collection and you'll see that there's now a document for your new user.

#Integrating FastAPI Users

The routes and models for our users are within the /backend/apps/user folder. Lets walk through what it contains.

#The User Models

The FastAPI Users package includes some basic User mixins with the following attributes:

  • id (UUID4) – Unique identifier of the user. Default to a UUID4.
  • email (str) – Email of the user. Validated by email-validator.
  • is_active (bool) – Whether or not the user is active. If not, login and forgot password requests will be denied. Default to True.
  • is_superuser (bool) – Whether or not the user is a superuser. Useful to implement administration logic. Default to False.
1from fastapi_users.models import BaseUser, BaseUserCreate, BaseUserUpdate, BaseUserDB
2
3
4class User(BaseUser):
5 pass
6
7
8class UserCreate(BaseUserCreate):
9 pass
10
11
12class UserUpdate(User, BaseUserUpdate):
13 pass
14
15
16class UserDB(User, BaseUserDB):
17 pass

You can use these as-is for your User models, or extend them with whatever additional properties you require. I'm using them as-is for this example.

#The User Routers

The FastAPI Users routes can be broken down into four sections:

  • Registration
  • Authentication
  • Password Reset
  • User CRUD (Create, Read, Update, Delete)
1def get_users_router(app):
2 users_router = APIRouter()
3
4 def on_after_register(user: UserDB, request: Request):
5 print(f"User {user.id} has registered.")
6
7 def on_after_forgot_password(user: UserDB, token: str, request: Request):
8 print(f"User {user.id} has forgot their password. Reset token: {token}")
9
10 users_router.include_router(
11 app.fastapi_users.get_auth_router(jwt_authentication),
12 prefix="/auth/jwt",
13 tags=["auth"],
14 )
15 users_router.include_router(
16 app.fastapi_users.get_register_router(on_after_register),
17 prefix="/auth",
18 tags=["auth"],
19 )
20 users_router.include_router(
21 app.fastapi_users.get_reset_password_router(
22 settings.JWT_SECRET_KEY, after_forgot_password=on_after_forgot_password
23 ),
24 prefix="/auth",
25 tags=["auth"],
26 )
27 users_router.include_router(
28 app.fastapi_users.get_users_router(), prefix="/users", tags=["users"]
29 )
30
31 return users_router

You can read a detailed description of each of the routes in the FastAPI Users' documentation, but there are a few interesting things to note in this code.

#The on_after Functions

These functions are called after a new user registers and after the forgotten password endpoint is triggered.

The on_after_register is a convenience function allowing you to send a welcome email, add the user to your CRM, notify a Slack channel, and so on.

The on_after_forgot_password is where you would send the password reset token to the user, most likely via email. The FastAPI Users package does not send the token to the user for you. You must do that here yourself.

#The get_users_router Wrapper

In order to create our routes we need access to the fastapi_users object, which is part of our app object. Because app is defined in main.py, and main.py imports these routers, we wrap them within a get_users_router function to avoid creating a cyclic import.

#Creating a Custom Realm JWT

Currently, Realm's user management functionality is only supported in the various JavaScript SDKs. However, Realm does support custom JWTs for authentication, allowing you to use the over the wire protocol support in the Python drivers to interact with some Realm services.

The available Realm services, as well as how you would interact with them via the Python driver, are out of scope for this tutorial, but you can read more in the documentation for Users & Authentication, Custom JWT Authentication, and MongoDB Wire Protocol.

Realm expects the custom JWT tokens to be structured in a certain way. To ensure the JWT tokens we generate with FastAPI Users are structured correctly, within backend/apps/user/auth.py we define MongoDBRealmJWTAuthentication which inherits from the FastAPI Users' CookieAuthentication class.

1class MongoDBRealmJWTAuthentication(CookieAuthentication):
2 def __init__(self, *args, **kwargs):
3 super(MongoDBRealmJWTAuthentication, self).__init__(*args, **kwargs)
4 self.token_audience = settings.REALM_APP_ID
5
6 async def _generate_token(self, user):
7 data = {
8 "user_id": str(user.id),
9 "sub": str(user.id),
10 "aud": self.token_audience,
11 "external_user_id": str(user.id),
12 }
13 return generate_jwt(data, self.lifetime_seconds, self.secret, JWT_ALGORITHM)

Most of the authentication code stays the same. However we define a new _generate_token method which includes the additional data Realm expects.

#Protecting the Todo App Routes

Now we have our user models, routers, and JWT token ready, we can modify the todo routes to restrict access only to authenticated and active users.

The todo app routers are defined in backend/apps/todo/routers.py and are almost identical to those found in the Introducing FARM Stack tutorial, with one addition. Each router now depends upon app.fastapi_users.get_current_active_user.

1@router.post( "/", response_description="Add new task", )
2async def create_task( request: Request, user: User = Depends(app.fastapi_users.get_current_active_user), task: TaskModel = Body(...), ):
3 task = jsonable_encoder(task)
4 new_task = await request.app.db["tasks"].insert_one(task)
5 created_task = await request.app.db["tasks"].find_one(
6 {"_id": new_task.inserted_id}
7 )
8
9 return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_task)

Because we have declared this as a dependency, if an unauthenticated or inactive user attempts to access any of these URLs, they will be denied. This does mean, however, that our todo app routers now must also have access to the app object, so as we did with the user routers we wrap it in a function to avoid cyclic imports.

#Creating Our FastAPI App and Including the Routers

The FastAPI app is defined within backend/main.py. This is the entry point to our FastAPI server and has been quite heavily modified from the example in the previous FARM stack tutorial, so let's go through it section by section.

1@app.on_event("startup")
2async def configure_db_and_routes():
3 app.mongodb_client = AsyncIOMotorClient(
4 settings.DB_URL, uuidRepresentation="standard"
5 )
6 app.db = app.mongodb_client[settings.DB_NAME]
7
8 user_db = MongoDBUserDatabase(UserDB, app.db["users"])
9
10 app.fastapi_users = FastAPIUsers(
11 user_db,
12 [jwt_authentication],
13 User,
14 UserCreate,
15 UserUpdate,
16 UserDB,
17 )
18
19 app.include_router(get_users_router(app))
20 app.include_router(get_todo_router(app))

This function is called whenever our FastAPI application starts. Here, we connect to our MongoDB database, configure FastAPI Users, and include our routers. Your application won't start receiving requests until this event handler has completed.

1@app.on_event("shutdown")
2async def shutdown_db_client():
3 app.mongodb_client.close()

The shutdown event handler does not change. It is still responsible for closing the connection to our database.

#Wrapping Up

In this tutorial we have covered one of the ways you can add user authentication to your FARM stack application. There are several other packages available which you might also want to try. You can find several of them in the awesome FastAPI list.

Or, for a more in-depth look at the FastAPI Users package, please check their documentation.

If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.

Rate this article

More from this series

FARM Stack
  • Introducing FARM Stack - FastAPI, React, and MongoDB
  • Adding Authentication to Your FARM Stack App
MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

© MongoDB, Inc.