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 anaccess_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 theLOGIN_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"
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 theaccess_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)}")
The schema Url
is defined like this:
from pydantic import BaseModel
class Url(BaseModel):
url: str
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
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)
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
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
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
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)