My FeedDiscussionsHeadless CMS
New
Sign in
Log inSign up
Learn more about Hashnode Headless CMSHashnode Headless CMS
Collaborate seamlessly with Hashnode Headless CMS for Enterprise.
Upgrade ✨Learn more

Adding OAuth2 Authencation In Fast-Api

Sahaj Gupta's photo
Sahaj Gupta
·Apr 21, 2021·

5 min read

Basic Fast-Api App

Hi, In this artical i'll show how i implemented OAuth2 with Password and Bearer ( here is docs for this ) well Fast-Api provide many type of authencation see list but for now let's just focus on OAuth2

here is the basic startup template for fast api(or if you have got your own thats fine too)

api/
    | __init__.py
    | routes.py
    | models.py
    | helper.py
    | auth.py
run.py

Note : For database i'll be using tortoise-orm. use pip install tortoise-orm to install

# api/__init__.py

from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise

app = FastAPI(
    title='Fast-api User Auth',
    description= 'Learing how to `OAuth2` in FastAPI',
    version = '1.1.1'
)

register_tortoise(
    app,
    db_url='sqlite://../database.db',
    modules={"models": ["api.models"]},
    generate_schemas=True,
    add_exception_handlers=True,
)

from api import routes

here is run.py you probabily won't need this but i just have it

# run.py

import unicorn

uvicorn.run('api:app', reload=True, host='127.0.0.1', port=8000)

now you can either use run.py to run api or run it from command line

python3 run.py
#------or-------
uvicorn api:app --reload

Note : Remember to run uvicorn from one level up from api folder

When you run it go to localhost:8000/docs you'll see somrthing like this Fast api start

Lets create model/database table for user in models.py

# api/models.py

from tortoise.models import Model
from tortoise.fields import IntField, CharField 
from tortoise.contrib.pydantic import pydantic_model_creator

from api.hepler import classproperty

class User(Model):
    id = IntField(pk=True)
    username = CharField(max_length=10)
    password = CharField(max_length=20)

    @classproperty
    def pydantic(cls):
        return pydantic_model_creator(cls, 
        name='User', 
        exclude=['password']
        )

    @classproperty
    def login_pydantic(cls):
        return pydantic_model_creator(cls, 
        name='Login User'
        )

Note : Check out helper.py in git Here each user will have a username and a password and off course an id

Now lets create a route for user to register

# api/routes.py

from api import app
from api.models import User

@app.post('/register', response_model = User.pydantic, tags=['User'])
async def register(user : User.login_pydantic):
    user_obj = await User.create(**user.dict(exclude_unset = True))
    return await User.pydantic.from_tortoise_orm(user_obj)

you should see something like Register User Now go ahead and register a user

Adding Authentication

Lets get started with authenticating user and getting access_token in auth.py

from fastapi.security import OAuth2PasswordBearer

login_schema = OAuth2PasswordBearer(tokenUrl="shild")

token_url contain route which will retuen access_token i called it shild (since i am a marvel fan, You can call it jim_gordon. get the reference😏 )

from fastapi.security import OAuth2PasswordRequestForm
from fastapi import Depends, HTTPException, status

async def authnicate_user(username : str, password : str):
    user = await User.get_or_none(username = username)
    if not user :
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail = "User Not Found")

    if not user.password == password:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail = "User Not Found")

    return user

@app.post('/shild', tags=['Authoncation'])
async def get_shild(form_data : OAuth2PasswordRequestForm = Depends()):
    user = await authnicate_user(form_data.username, form_data.password)
    if user:
        user_obj = await User.login_pydantic.from_tortoise_orm(user)
        token = user_obj.json()
        return {'access_token': token, 'token_type' : 'bearer'}

    raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail = "User Not Found")

Note : _authnicate_user is a functation that takes username and password as parameter and return user if exist_

Note : I haven't used any type of password hashing please don't be like me and some sort of hashing to encrypt password

get_shild function will take the username and password from OAuth2PasswordRequestForm > check if provided data is correct and return access_token which here is user object schema in json. If you want and i encourage you to use JWT or some other format which is more secure

By the way this is how OAuth2PasswordRequestForm looks like OAuth2PasswordRequestForm

That's all now you have OAuth2 Authencation with access_token Don't believe? me go ahead and try /shild route and see result

Now lets create a route which requires the authencation i.e it will require access_token that we just created in get_shild function or in /shild route

# api/routes.py

from api.auth import get_current_user

@app.get('/user/me', response_model = User.pydantic, tags=['User'])
async def current_user(user : User.login_pydantic = Depends(get_current_user)):
    return user

This current_user function depends upon get_current_user in auth.py and it returns login_pydantic schema of User

# api/auth.py

async def get_current_user(token: str = Depends(login_schema)):
    if not token:
        raise HTTPException(
            status_code=status.HTTP_406_NOT_ACCEPTABLE,
            detail = "Invalid Token")

    payload = json.loads(token)
    user = await authnicate_user(username = payload.get('name'), password = payload.get('password'))
    user_obj = await User.pydantic.from_tortoise_orm(user)
    return user_obj

Since get_current_user depends upon the login_schema it will requier access_token to run this function and, as i said current_user depends upon get_current_user when then depends upon login_schema thus current_user also depends upon login_schema

its like if a = 5 and b = a then b = 5

Now when you look at localhost:8000/docs you will see a lock right side of current_user route. That means this route is protected and require authentication

the advantage of using get_current_user is which ever route you want to protect with authebtication you can add user : User.login_pydantic = Depends(get_current_user) to that function

There you have it now you have successfully have genrated access_token and created a protected route 🥳🥳

Problem I Faced

Obesely i didn't do it in first try. I too face some problem, here are some of them

  1. return str in token_url :- In auth.py under get_shild function i tried to return just access_token just a str. It turns out it will throw an inter server error whenever you use get_current_user (It took 2hrs to debug this🥵😥)
  2. Converting user_obj to dict :- In auth.py under get_shild function i tried to assign token to user_obj.dict() but it give me error saying that dict requies an argument but i didn't pass any so i just replaced dict to json and it works. So fine by me

luckly those were the major problems i faced, Other were just some typos

Thats all for this artical. Thanks for reading

Note: or you could use fastapi-login

My Links : Instagram : Github : G-mail