FastAPI Login Features:

  • Provides a simple authorization dependency
  • Support for token in either request headers or as cookie
  • Usable as a middleware to create your own dependencies
  • Support for callbacks when user is unauthorized
  • Support for OAuth2 scopes
  • OpenAPI support

FastAPI Login Installation

$ pip install fastapi-login

Usage

To begin we have to setup our FastAPI app:

from fastapi import FastAPI

SECRET = 'your-secret-key'

app = FastAPI()

To obtain a suitable secret key you can run import os; print(os.urandom(24).hex()).

Now we can import and setup the LoginManager, which will handle the process of encoding and decoding our Json Web Tokens.

from fastapi_login import LoginManager

manager = LoginManager(SECRET, token_url='/auth/token')

For the example we will use a dictionary to represent our user database. In your application this could also be a real database like sqlite or Postgres. It does not matter as you have to provide the function which retrieves the user.

fake_db = {'johndoe@e.mail': {'password': 'hunter2'}}

Now we have to provide the LoginManager with a way to load our user. The user_loader callback should either return your user object or None

@manager.user_loader()
def load_user(email: str):  # could also be an asynchronous function
    user = fake_db.get(email)
    return user

Now we have to define a way to let the user login in our app. Therefore we will create a new route:

from fastapi import Depends
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_login.exceptions import InvalidCredentialsException

# the python-multipart package is required to use the OAuth2PasswordRequestForm
@app.post('/auth/token')
def login(data: OAuth2PasswordRequestForm = Depends()):
    email = data.username
    password = data.password

    user = load_user(email)  # we are using the same function to retrieve the user
    if not user:
        raise InvalidCredentialsException  # you can also use your own HTTPException
    elif password != user['password']:
        raise InvalidCredentialsException
    
    access_token = manager.create_access_token(
        data=dict(sub=email)
    )
    return {'access_token': access_token, 'token_type': 'bearer'}

Now whenever you want your user to be logged in to use a route, you can simply use your LoginManager instance as a dependency.

@app.get('/protected')
def protected_route(user=Depends(manager)):
    ...

If you also want to handle a not authenticated error, you can add your own subclass of Exception to the LoginManager.

from starlette.responses import RedirectResponse

class NotAuthenticatedException(Exception):
    pass

# these two argument are mandatory
def exc_handler(request, exc):
    return RedirectResponse(url='/login')

# This will be deprecated in the future
# set your exception when initiating the instance
# manager = LoginManager(..., custom_exception=NotAuthenticatedException)
manager.not_authenticated_exception = NotAuthenticatedException
# You also have to add an exception handler to your app instance
app.add_exception_handler(NotAuthenticatedException, exc_handler)

To change the expiration date of the token use the expires_delta argument of the create_access_token method with a timedelta. The default is set 15 min. Please be aware that setting a long expiry date is not considered a good practice as it would allow an attacker with the token to use your application as long as he wants.

from datetime import timedelta

data = dict(sub=user.email)

# expires after 15 min
token = manager.create_access_token(
    data=data
)
# expires after 12 hours
long_token = manager.create_access_token(
    data=data, expires=timedelta(hours=12)
)

Usage with cookies

Instead of checking the header for the token. fastapi-login also support access using cookies.

from fastapi_login import LoginManager

manager = LoginManager(SECRET, token_url='/auth/token', use_cookie=True)

Now the manager will check the requests cookies the headers for the access token. The name of the cookie can be set using manager.cookie_name. If you only want to check the requests cookies you can turn the headers off using the use_header argument

For convenience the LoginManager also includes the set_cookie method which sets the cookie to your response, with the recommended HTTPOnly flag and the manager.cookie_name as the key.

from fastapi import Depends
from starlette.responses import Response


@app.get('/auth')
def auth(response: Response, user=Depends(manager)):
    token = manager.create_access_token(
        data=dict(sub=user.email)
    )
    manager.set_cookie(response, token)
    return response
    

 

FastAPI Login Advanced usage

Cookies

By default LoginManager expects the token to be in the Authorization header. However cookie support can be enabled.

from fastapi_login import LoginManager

manager = LoginManager(
    'secret', '/login',
    use_cookie=True
)

 

For convenince a set_cookie method is provided, which sets the cookie, using LoginManager.cookie_name and the recommended HTTPOnly flag.

@app.post('/login')
def login(response: Response):
    ...
    token = manager.create_access_token(
        data=dict(sub=user.email)
    )
    manager.set_cookie(response, token)
    return response

Configuration

By default, LoginManager looks for a cookie with the name access-token, this can be changed using the cookie_name argument, when creating the instance.

manager = LoginManager(
    ...,
    cookie_name='custom-cookie-name'
)

If you only want to support authorization using cookies, use_header can be set to false on initialization.

from fastapi_login import LoginManager

manager = LoginManager(
    'secret', '/login',
    use_cookie=True,
    use_header=False
)

 

Exception handling

Sometimes it is needed to run some code if a user is not authenticated, this can achieved, by setting a custom Exception on the LoginManager instance.

from fastapi import Request
from starlette.responses import RedirectResponse


class NotAuthenticatedException(Exception):
    pass

# Before version 1.7.0
# manager.not_authenticated_exception = NotAuthenticatedException

# The updated way
manager = LoginManager(
    ...,
    custom_exception=NotAuthenticatedException
)

@app.exception_handler(NotAuthenticatedException)
def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
    """
    Redirect the user to the login page if not logged in
    """
    return RedirectResponse(url='/login')

Now whenever there is no, or an invalid token present in the request, your exception will be raised, and the exception handler will be executed.

More to writing exception handlers can be found in the official documentation of FastAPI

Token expiry

By default token's expire after 15 minutes. This can be changed using the expires argument in the create_access_token method.

 

# expires after 12 hours
long_token = manager.create_access_token(
    data=data, expires=timedelta(hours=12)
)

If you want to change the expiry for every token issued the default expiry can be set on initialization

manager = LoginManager(
    ...,
    default_expiry=timedelta(hours=12)
)

 

Middleware

Optionally a LoginManager instance can also be added as a middleware. It's important to note that request.state.user is set to None if no (valid) token is present in the request.

from fastapi.requests import Request

manager.useRequest(app)

@app.route('/showcase')
def showcase(request: Request):
    # None if unauthorized
    user = request.state.user

Using the middleware it's easy to write your own dependencies, that have access to your user object

from fastapi.requests import Request
from fastapi.exceptions import HTTPException

def is_admin(request: Request):
    user = request.state.user
    if user is None:
        raise HTTPException(401)
    # assuming our user object has a is_admin property
    elif not user.is_admin:
        raise HTTPException(401)
    else:
        return user

 

OAuth2 scopes

In addition to normal token authentication, OAuth2 scopes can be used to restrict access to certain routes.

@app.get("/scoped/route")
def scoped_route(user=Security(manager, scopes=["required", "scopes", "here"])):
    ...

Notice how instead of the normally used fastapi.Depends fastapi.Security is used. In order to give your token the required scopes LoginManager.create_access_token has a scopes parameter. In order for the scopes to show up in the OpenAPI docs, your scopes need to be passed as an argument when instantiating LoginManager.

 

Predefining additional user_loader arguments

The LoginManager.user_loader can also take arguments which will be passed on the callback

@manager.user_loader(db_session=...)
def query_user(user_id: str, db_session):
    """
    Get a user from the db
    :param user_id: E-Mail of the user
    :param db_session: The currently active connection to the database
    :return: None or the user object
    """
    return db_session.get(user_id)

 

Callable as extra argument

Before version 1.7.0 empty parentheses were not needed after the decorator. To provide backwards compatibility it's still possible to omit them as of v1.7.1. Because of this it's however not possible to pass a callable object as the first extra argument.

This will not work:

def some_callable_object(...):
    ...

@manager.user_loader(some_callable_object)
def load_user(email, some_callable):
    ...
If you need this functionality you need to use keyword arguments:
@manager.user_loader(some_callable=some_callable_object)
def load_user(email, some_callable)

 

Asymmetric algorithms

Thanks to filwaline in addition to symmetric keys, RSA can also be used to sign the tokens.

Required dependencies

The cryptography packages is required for this. Run the following command to install all the required dependencies.

pip install fastapi-login[asymmetric]

 

Supported algorithms

Currently RS256 is the only asymmetric algorithm supported by the package. If you need another algorithm, please open an issue and I will consider adding it.

To use the asymmetric algorithm choose algorithm="RS256" when initiating LoginManager.

LoginManager(
    secret="...", token_url="...",
    algorithm="RS256"
)

 

If your private key is not password protected the usage of the package stays exactly the same.

Hint

You don't need to pass your public key explicitly, as it can be derived from the private key.

However, if you used a password during the key generation the syntax changes slightly.

manager = LoginManager(
    secret={"private_key": "your_rsa_key", "password": "your_password_for_the_key"},
    token_url="...",
    algorithm="RS256"
)

Note how instead of just using the key, we now have to pass a dictionary with the private_key and the password fields set.

keywords: pythonFastAPI