DEV Community

Alexis Tacnet
Alexis Tacnet

Posted on • Updated on • Originally published at kernelpanic.io

Demystifying authentication with FastAPI and a frontend

Authentication is definitely a hard and complicated problem. Protocols like OAuth2 try to make it simpler, but in fact they make it harder to understand for beginners, as the reference is quite complex and there aren't a lot of good practices available online.

The new framework FastAPI is now our go-to web library for all our projects, as it is very efficient to develop with and it supports amazing typing out of the box. The only issue we have is dealing with authentication when using a JS Frontend in front of it. Let's close this debate once and for all by describing the authentication scheme that I think everyone needs for a simple web application with FastAPI, using an external provider.

The authentication plan

First, we need to have a plan for what we want to do. I have always wondered why authentication tutorials cover hundreds of different cases. In fact, I am convinced that a modern web application shall just need one.

What I mean by modern web application here is a FastAPI Python backend (but you can go with whatever you like as long as it outputs JSON) with a JS frontend (with React or Vue for example). What we need to do is therefore authenticate someone navigating on the frontend, and establish a secure connection with the backend to know who is calling your API.

Simple right? And because we are in the 21st century, let's not use a lame email/password form on the frontend : they are not really secure, and SSO (Single Single-On) is clearly a marketing advantage nowadays. We will therefore use a OAuth authentication scheme to get information and authenticate the user on our website.

The goal of OAuth

You may have heard of it, because every major auth provider supports: Google, Twitter, Github, Facebook, ... but you may wonder why it exists in the first place!

The goal of OAuth in general is to streamline and standardize the use of authorization around the web, especially when it comes to multiple parties like our website and Google for example. How do you access the Google information from our website with a Google login? This is what OAuth is made for.

In this tech story, we have as always both sides : the server and the client. In the case of a external provider like Github, Github is the OAuth server, and our application is the client. The goal of OAuth is to enable the client to query the API server attached to the OAuth server in a secure way.

What we want to do is ask Google who the user on the website is to achieve authentication. We will store this user information in a database, and then secure the connection for this user between the frontend and the backend to identify and authorize API calls.

But keep in mind: OAuth2 is made to make it easy to authorize our server to interact with other parties like Google or Github on behalf of an user. You can then ask them who the user on the website is to make authentication, for authenticating the link between the frontend and our API.

Securing the connection between the frontend and our API

What we need in the end is to authenticate our connection between our frontend (usually a SPA or SSR javascript application) and our own API. For that, we will use the famous JWT token that has several advantages and that I really like.

The protocol is the following:

  • Google redirects to our application with a code
  • We ask Google for more information, like the profile picture
  • We then issue a JWT token with this user data that we will share between the frontend and the backend

This token will be used for every API call to our own backend and will therefore identify the user making the calls.

We don't use OAuth directly to secure the connection between the frontend and our API, because we don't need it, and that is not what it's made for. We only issue a small token, and we verify it for each request in the backend. Secure. Simple :)

Authentication pattern

If you already looked at OAuth2, you may have noticed that several schemes are available and are needed for different types of applications. The two that we are interested in are the Implicit one and the Authorization code scheme.

The Implicit scheme

With the Implicit scheme, our user is redirected to the SSO login (1) and then redirected after authentication to our frontend (2) with the access_token needed to access the OAuth resources (profile picture remember). This is typically useful when we need to access this type of resource directly in the frontend, without bothering making the backend in charge of that.

To secure the connection between the frontend and our own API, you need to give this token to the API (3) that will check with the OAuth server that it is valid (4). If the token is valid, then this user is clearly on the website, so let's give him another token (5) (to check it super quickly without asking the OAuth server every time and control its expiration for example).

As mentioned, our frontend is also capable of querying directly the external API (6), with the access_token, to process on its side the resources the user gave access to.

The Authorization code scheme

With the Authorization code scheme, our user is also redirected to the SSO login (1) and then redirected to our frontend (2), but with a mysterious code that we need to give to the backend (3).

The backend will then give this code (along with the OAuth secret client data, like client_id and client_secret) to the OAuth server that will then return an access_token if the code is correct (4).

If the code is correct, we can use the token given by the OAuth server to access user resources directly from the backend (5), to store a profile picture or a Google doc file that belongs to the user for example. Then, our user is authenticated, so let's finally give him his token (6).

The advantage of this scheme is we don't put the frontend in charge of the access_token of the OAuth server, which has a few security advantages:

  • The frontend doesn't have to give this token to the backend, which is better for security as a Man-in-the-middle attack could steal your identity. This is not possible with a code as it needs a client_secret which only the backend is aware of.
  • The backend is the one with the access_token, preventing any malicious extensions from stealing it in your front-end domain.

In our experience, we prefer this scheme for production applications. This is the only one supported by Github for example.

FastAPI implementation

We really like FastAPI at Strio : this framework is simple, efficient, and typing friendly. Creating a authentication scheme on top of it was not that hard, and is really clean. We just have to keep in mind the few tips I described earlier:

  • OAuth is only for external API access
  • Simple JWT encoding and decoding between our frontend and backend

In this example, we will use the Authorization Code scheme, with this code forwarded between our frontend and our backend, and the Github external provider.

URLs

The first thing to configure are the different URLs we will use for this scheme:

  • The LOGIN_URL is the base URL needed to create the URL our frontend will be redirected to
  • The TOKEN_URL is the one our backend will query with the Authorization code to get an access_token.
  • The USER_URL is an api endpoint used to get data about the user, it is a typical example of the external API described earlier.
  • The REDIRECT_URL is the URL of our frontend that Github will need to redirect to, it is useful because it will be passed with the LOGIN_URL.
from app.settings import settings

LOGIN_URL = "https://github.com/login/oauth/authorize"
TOKEN_URL = "https://github.com/login/oauth/access_token"
USER_URL = "https://api.github.com/user"

REDIRECT_URL = f"{settings.app_url}/auth/github"
Enter fullscreen mode Exit fullscreen mode

Login URL creation

The frontend needs to redirect the user's browser to a URL generated from the LOGIN_URL but also with some information specific to our application:

  • The client_id, given by Github for our application
  • The redirect_uri, that we want Github to forward the user to, with the Authorization code
  • The state, a random generated string that Github will check again when we want to create the access_token, for security reasons

The code is quite straightforward, we created a route called /login so that we can tell the frontend which URL to use. We use an APIRouter here because we want to integrate this piece of code directly into an existing FastAPI application.

from urllib.parse import urlencode

from fastapi import APIRouter

from app.settings import settings
from .schemas import Url
from .helpers import generate_token

LOGIN_URL = "https://github.com/login/oauth/authorize"
REDIRECT_URL = f"{settings.app_url}/auth/github"

router = APIRouter()

@router.get("/login")
def get_login_url() -> Url:
    params = {
        "client_id": settings.github_client_id,
        "redirect_uri": REDIRECT_URL,
        "state": generate_token(),
    }
    return Url(url=f"{LOGIN_URL}?{urlencode(params)}")
Enter fullscreen mode Exit fullscreen mode

The schema Url is defined like this:

from pydantic import BaseModel

class Url(BaseModel):
    url: str
Enter fullscreen mode Exit fullscreen mode

Authorization code verification and token creation

Once the frontend receives the Authorization code, it can forward it to a specific API endpoint, here /authorize for the backend to check it and get all it needs from the User API. The first thing we want to define are the schemas that will be used:

from pydantic import BaseModel

class AuthorizationResponse(BaseModel):
    state: str
    code: str

class GithubUser(BaseModel):
    login: str
    name: str
    company: str
    location: str
    email: str
    avatar_url: str

class User(BaseModel):
    id: int
    login: str
    name: str
    company: str
    location: str
    email: str
    picture: str

    class Config:
        orm_mode = True

class Token(BaseModel):
    access_token: str
    token_type: str
    user: User
Enter fullscreen mode Exit fullscreen mode

The AuthorizationResponse is the body of the request made by the frontend with the state and authorization code, while the GithubUser and User represent users from different sources. The Token schema defines what we will send to the frontend to authenticate our requests between our API and the interface.

On the route side, we use httpx to make requests to Github and check the authorization code, as well as retrieving the user's information. We then create a database entry if the user is not yet present in the database, using the sqlachemy ORM. This is a plain implementation of the Github documentation on their auth API.

from urllib.parse import parse_qsl
from typing import Dict

import httpx
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session

from app.database import get_db
from .helpers import create_access_token
from .schemas import AuthorizationResponse, GithubUser, User, Token
from .crud import get_user_by_login, create_user

TOKEN_URL = "https://github.com/login/oauth/access_token"
USER_URL = "https://api.github.com/user"

router = APIRouter()

@router.post("/authorize")
async def verify_authorization(
 body: AuthorizationResponse, db: Session = Depends(get_db)
) -> Token:
    params = {
        "client_id": settings.github_client_id,
        "client_secret": settings.github_client_secret,
        "code": body.code,
        "state": body.state,
    }

    async with httpx.AsyncClient() as client:
        token_request = await client.post(TOKEN_URL, params=params)
        response: Dict[bytes, bytes] = dict(parse_qsl(token_request.content))
        github_token = response[b"access_token"].decode("utf-8")
        github_header = {"Authorization": f"token {github_token}"}
        user_request = await client.get(USER_URL, headers=github_header)
        github_user = GithubUser(**user_request.json())

    db_user = get_user_by_login(db, github_user.login)
    if db_user is None:
        db_user = create_user(db, github_user)

    verified_user = User.from_orm(db_user)
    access_token = create_access_token(data=verified_user)

    return Token(access_token=access_token, token_type="bearer", user=db_user)
Enter fullscreen mode Exit fullscreen mode

The function create_access_token creates a simple JWT token, and is therefore in the helpers file:

from datetime import datetime, timedelta

import jwt

from app.settings import settings
from .schemas import User

def create_access_token(*, data: User, exp: int = None) -> bytes:
    to_encode = data.dict()
    if exp is not None:
        to_encode.update({"exp": exp})
    else:
        expire = datetime.utcnow() + timedelta(minutes=60)
        to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(
        to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm
    )
    return encoded_jwt
Enter fullscreen mode Exit fullscreen mode

Get user from the JWT token

Now that our frontend has a JWT token, we just need to secure our private routes with a FastAPI Dependency that will decode the token and raise an Exception if needed. The dependency is made like this, in its own file:

import jwt
from fastapi import Header, HTTPException, status
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import ValidationError

from app.settings import settings
from .schemas import User

def get_user_from_header(*, authorization: str = Header(None)) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

    scheme, token = get_authorization_scheme_param(authorization)
    if scheme.lower() != "bearer":
        raise credentials_exception

    try:
        payload = jwt.decode(
            token, settings.jwt_secret_key,
            algorithms=[settings.jwt_algorithm]
        )
        try:
            token_data = User(**payload)
            return token_data
        except ValidationError:
            raise credentials_exception
    except jwt.PyJWTError:
        raise credentials_exception
Enter fullscreen mode Exit fullscreen mode

We can use this dependency in some private endpoints, like the common /me that will give information of the user making the request (through its JWT token):

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.database import get_db

from .crud import get_user
from .schemas import User
from .models import User as DbUser
from .dependency import get_user_from_header

router = APIRouter()

@router.get("/me", response_model=User)
def read_profile(
    user: User = Depends(get_user_from_header),
    db: Session = Depends(get_db),
) -> DbUser:
    db_user = get_user(db, user.id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user
Enter fullscreen mode Exit fullscreen mode

Conclusion

Authentification is a very difficult beast to tame, and improvements can be made on our own approach. However, this FastAPI example should give you a good start to implement your own scheme, using whatever external provider you like (Google, Facebook, Twitter, ...).

Top comments (0)