FastAPI Login Tutorial: Secure Your API Now!

by Alex Braham 45 views

Hey guys! Let's dive into securing your FastAPI applications. This FastAPI login tutorial will guide you through setting up authentication, ensuring only authorized users access your valuable data. Securing your API is super important, and FastAPI makes it surprisingly straightforward. We will cover everything from handling user credentials to implementing robust authentication flows. Get ready to fortify your applications and keep those pesky unauthorized users out!

Why Secure Your FastAPI Application?

Before we get into the code, let’s talk about why securing your FastAPI application is not just a good idea—it’s a necessity. Think of your API as the front door to your application's data and functionality. Without proper security, you're essentially leaving that door wide open for anyone to waltz in and wreak havoc. Security breaches can lead to data theft, compromised user accounts, and a whole host of other nasty consequences.

Implementing authentication and authorization is your primary defense against these threats. Authentication verifies the identity of a user (are they who they say they are?), while authorization determines what that user is allowed to do (what resources can they access?). By implementing these measures, you ensure that only legitimate users can access your API, and even then, they can only do what they're supposed to do. This principle of least privilege is crucial for maintaining a secure system.

Moreover, securing your API builds trust with your users. Knowing that their data is protected and their interactions are secure fosters confidence and encourages them to continue using your application. In today's world, where data privacy is a major concern, demonstrating a commitment to security is a competitive advantage. Ignoring security is not only risky but can also damage your reputation and erode user trust. So, buckle up, because securing your FastAPI app is an investment in its long-term success and the safety of your users.

Setting Up Your FastAPI Project

Alright, let's get our hands dirty! First, make sure you have Python installed (preferably Python 3.7 or higher). Then, create a new directory for your project and navigate into it using your terminal. We'll start by setting up a virtual environment to keep our project dependencies isolated. This is good practice, trust me!

python3 -m venv venv
source venv/bin/activate  # On Linux/Mac
# venv\Scripts\activate  # On Windows

Now that our virtual environment is activated, let's install FastAPI and Uvicorn. FastAPI is the web framework we'll be using, and Uvicorn is an ASGI server that will run our application.

pip install fastapi uvicorn

With FastAPI and Uvicorn installed, create a file named main.py. This will be the main file for our application. Open main.py in your favorite text editor and add the following code to get started:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def read_root():
    return {"message": "Hello World"}

This simple code creates a FastAPI application and defines a single endpoint at the root path (/). To run the application, use the following command in your terminal:

uvicorn main:app --reload

The --reload flag tells Uvicorn to automatically restart the server whenever you make changes to your code. This is super handy during development! Open your web browser and navigate to http://localhost:8000. You should see the message {"message": "Hello World"} displayed in your browser. Congratulations, you've got a basic FastAPI application up and running! Now, let’s move on to the exciting part: implementing authentication.

Implementing Basic Authentication

Okay, let's add some security! We'll start with a basic username and password setup. This isn't production-ready, but it’s a great way to understand the fundamentals. First, we need to store user credentials. For simplicity, we'll use a dictionary in memory. Don't do this in a real application; use a database! Add the following to your main.py:

users = {
    "john": {"password": "password123"},
    "jane": {"password": "securepass"},
}

Now, let’s create a login endpoint that checks these credentials. We'll use FastAPI's HTTPBasic authentication scheme. Add these imports to the top of your main.py:

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

Next, initialize the HTTPBasic scheme:

security = HTTPBasic()

Now, let's create the /login endpoint. This endpoint will receive the username and password from the Authorization header, check if they match our stored credentials, and return a success message if they do. Add the following code to your main.py:

def authenticate_user(credentials: HTTPBasicCredentials):
    user = users.get(credentials.username)
    if user and user["password"] == credentials.password:
        return True
    return False


async def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    if not authenticate_user(credentials):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/login")
async def login(current_user: str = Depends(get_current_user)):
    return {"message": f"Welcome, {current_user}!"}

In this code:

  • authenticate_user checks if the provided username and password match the stored credentials.
  • get_current_user uses HTTPBasicCredentials and the security scheme to extract the username and password from the Authorization header. It then calls authenticate_user to verify the credentials. If the authentication fails, it raises an HTTPException with a 401 Unauthorized status code.
  • The /login endpoint uses Depends(get_current_user) to ensure that only authenticated users can access it. If the user is successfully authenticated, it returns a welcome message.

Now, restart your server and try accessing http://localhost:8000/login. You'll be prompted for a username and password. Use john as the username and password123 as the password. If you enter the correct credentials, you should see the welcome message. If you enter incorrect credentials, you'll get a 401 Unauthorized error. Congrats, you've implemented basic authentication! Keep in mind, this is just a starting point and is not secure for production environments.

Implementing Token-Based Authentication (JWT)

Alright, let's ditch the basic authentication and move on to something more robust: token-based authentication using JWT (JSON Web Tokens). JWTs are a standard way of representing claims securely between two parties. In our case, the server issues a JWT to the client after successful authentication, and the client includes this JWT in subsequent requests to access protected resources.

First, we need to install the python-jose and passlib libraries. python-jose is for encoding and decoding JWTs, and passlib is for password hashing.

pip install python-jose passlib

Now, add the following imports to your main.py:

from datetime import datetime, timedelta
from typing import Union

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext

Next, let's set up some configuration variables for JWT. These include the secret key, the hashing algorithm, and the token expiration time.

SECRET_KEY = "YOUR_SECRET_KEY"  # Change this to a strong, random key
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

password_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  • SECRET_KEY is a secret key used to sign the JWT. Make sure to use a strong, random key in a real application! Store it securely, perhaps in an environment variable.
  • ALGORITHM specifies the algorithm used to sign the JWT. HS256 is a common choice.
  • ACCESS_TOKEN_EXPIRE_MINUTES determines how long the JWT is valid for. After this time, the user will need to re-authenticate.
  • password_context is used for hashing and verifying passwords.
  • oauth2_scheme is an instance of OAuth2PasswordBearer, which is used to obtain the JWT from the Authorization header.

Now, let's update our users dictionary to store hashed passwords instead of plain text passwords. We'll also add a helper function to hash passwords.

def hash_password(password: str):
    return password_context.hash(password)


def verify_password(password: str, hashed_password: str):
    return password_context.verify(password, hashed_password)


users = {
    "john": {"hashed_password": hash_password("password123")},
    "jane": {"hashed_password": hash_password("securepass")},
}

Now, let's create the /token endpoint, which is responsible for issuing JWTs. This endpoint will receive the username and password from the request body, verify the credentials, and return a JWT if the authentication is successful. Add the following code to your main.py:

async def authenticate_user(username: str, password: str):
    user = users.get(username)
    if not user:
        return False
    if not verify_password(password, user["hashed_password"]):
        return False
    return True


def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = await authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(data={"sub": form_data.username}, expires_delta=access_token_expires)
    return {"access_token": access_token, "token_type": "bearer"}

In this code:

  • authenticate_user now uses verify_password to check if the provided password matches the hashed password stored in the users dictionary.
  • create_access_token creates a JWT with the username as the subject (sub) and an expiration time. It uses the jwt.encode function from the python-jose library to sign the JWT with the secret key and algorithm.
  • The /token endpoint uses OAuth2PasswordRequestForm to receive the username and password from the request body. It then calls authenticate_user to verify the credentials. If the authentication is successful, it calls create_access_token to generate a JWT and returns it in the response.

Finally, let's create a protected endpoint that requires a valid JWT to access. Add the following code to your main.py:

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = users.get(username)
    if user is None:
        raise credentials_exception
    return username


@app.get("/protected")
async def protected_route(current_user: str = Depends(get_current_user)):
    return {"message": f"Hello, {current_user}! This is a protected route."}

In this code:

  • get_current_user uses OAuth2PasswordBearer to extract the JWT from the Authorization header. It then uses jwt.decode to decode the JWT and verify its signature. If the JWT is invalid or expired, it raises an HTTPException. It also checks if the user exists in the users dictionary.
  • The /protected endpoint uses Depends(get_current_user) to ensure that only users with a valid JWT can access it. If the user is successfully authenticated, it returns a welcome message.

Restart your server and try accessing http://localhost:8000/protected. You'll get a 401 Unauthorized error because you haven't provided a JWT. To get a JWT, send a POST request to http://localhost:8000/token with the username and password in the request body, using the application/x-www-form-urlencoded content type. You can use a tool like curl or Postman to do this. Once you have the JWT, include it in the Authorization header of your request to http://localhost:8000/protected, using the Bearer scheme. If you do everything correctly, you should see the welcome message. Congratulations, you've implemented token-based authentication with JWT!

Conclusion

Alright guys, we've covered a lot! You've learned how to secure your FastAPI application using both basic authentication and token-based authentication with JWT. Remember, the basic authentication example is just for demonstration purposes and is not suitable for production environments. JWT is a much more robust and secure solution.

However, what we've covered here is still just the tip of the iceberg. There are many other security best practices to consider, such as using HTTPS, implementing rate limiting, and protecting against common web vulnerabilities like SQL injection and cross-site scripting (XSS). Always stay vigilant and keep learning about security to protect your applications and your users' data.

Securing your API is a continuous process, not a one-time task. As your application evolves and new threats emerge, you'll need to adapt your security measures accordingly. So, keep learning, keep experimenting, and keep building secure and awesome FastAPI applications! Happy coding!