Skip to content

Commit

Permalink
Merge pull request panaverse#17 from panaverse/junaid
Browse files Browse the repository at this point in the history
Add OAuth2 Baby Steps 01-04
  • Loading branch information
EnggQasim authored Jan 18, 2024
2 parents b8bcba8 + eda7a49 commit 4e527c5
Show file tree
Hide file tree
Showing 63 changed files with 5,261 additions and 1 deletion.
25 changes: 25 additions & 0 deletions 08_everything_Is_an_api/13_oauth2/01_basic_auth/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import TypedDict, Optional

class UserDict(TypedDict):
username: str
full_name: str
email: str
hashed_password: str
disabled: Optional[bool]

fake_users_db: dict[str, UserDict] = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "[email protected]",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
53 changes: 53 additions & 0 deletions 08_everything_Is_an_api/13_oauth2/01_basic_auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Annotated

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

from models import User, UserInDB
from service import fake_decode_token, find_user_dict

app = FastAPI()

def fake_hash_password(password: str):
return "fakehashed" + password


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user


async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user


@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user_dict = find_user_dict(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password)
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")

return {"access_token": user.username, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return current_user
11 changes: 11 additions & 0 deletions 08_everything_Is_an_api/13_oauth2/01_basic_auth/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import BaseModel

class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None


class UserInDB(User):
hashed_password: str
93 changes: 93 additions & 0 deletions 08_everything_Is_an_api/13_oauth2/01_basic_auth/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Simple Login Using Email & Password

Goal: Learn how to build a secure way for the frontend to authenticate with the backend, using a username and password.

1. Clone the project
2. Install requirements `pip install -r requirements.txt`
3. Run the project `uvicorn main:app --reload` and visit `http://localhost:8000/docs`
4. Click on Authenticate to login using username: johndoe & password: secret. Now call the `/users/me` endpoint.

5. Now try out the `/token` endpoint with same credentials.
```
{
"access_token": "johndoe",
"token_type": "bearer"
}
```

To start off we will be creating the same login system and follow along this tutorial :

[FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/first-steps/)
[Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/)

After completing it you will have a simple login system where you can login and get loggedIn User Data.

## Security & FastAPI Utils
FastAPI provides several tools to help you deal with Security easily, rapidly, in a standard way, without having to study and learn all the security specifications.

We can use OAuth2 to build that with FastAPI. What tools fastapi gives us?

## OAuth2PasswordBearer Class

OAuth2 flow for authentication using a bearer token obtained with a password. An instance of it would be used as a dependency.

`pip install python-multipart`

```
# main.py
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/")
async def root():
print(OAuth2PasswordBearer.__doc__)
return {"message": "Hello World"}
@app.get("/token")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
return {"token": token}
```

Add `print(OAuth2PasswordBearer.__doc__)` to see what abstractions it provides.

The oauth2_scheme variable is an instance of OAuth2PasswordBearer, but it is also a "callable".

It could be called as: `oauth2_scheme(some, parameters)` So, it can be used with Depends. (as dependency injection)

### tokenUrl="token"

This parameter doesn't create that endpoint / path operation, but declares that the URL /token will be the one that the client should use to get the token.

That information is used in OpenAPI, and then in the interactive API documentation systems.


## OAuth2PasswordRequestForm Class

Now we need to add a path operation for the user/client to actually send the username and password. For it we will use OAuth2PasswordRequestForm as a dependency:

```
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
```

OAuth2PasswordRequestForm is a class dependency that declares a form body with:

- The username.
- The password.
- An optional scope field as a big string, composed of strings separated by spaces.
- An optional grant_type.
- An optional client_secret
- An optional client_id

It is just a class dependency that you could have written yourself, or you could have declared Form parameters directly. But as it's a common use case, it is provided by FastAPI directly.

Now we have a basic login system where frontend to authenticate with the backend using a username and password.

To enhance the security, we have to implement hashing for the password. This ensures that even if the password is compromised, it cannot be easily decrypted.

Now we will understand jwt tokens and implement OAuth with Password, Bearer with JWT tokens
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fastapi
uvicorn
python-multipart
pydantic
16 changes: 16 additions & 0 deletions 08_everything_Is_an_api/13_oauth2/01_basic_auth/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from data import fake_users_db
from models import UserInDB

def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)

def fake_decode_token(token):
# This doesn't provide any security at all
# Check the next version
user = get_user(fake_users_db, token)
return user

def find_user_dict(username: str):
return fake_users_db.get(username)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from datetime import timedelta
from typing import Annotated

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

from models import Token, User
from data import fake_users_db

from service import authenticate_user, create_access_token, get_current_active_user, ACCESS_TOKEN_EXPIRE_MINUTES

app = FastAPI()

@app.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
user = authenticate_user(fake_users_db, 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": user.username}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")


@app.get("/users/me/", response_model=User)
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return current_user


@app.get("/users/me/items/")
async def read_own_items(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return [{"item_id": "Foo", "owner": current_user.username}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel

class Token(BaseModel):
access_token: str
token_type: str


class TokenData(BaseModel):
username: str | None = None


class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None


class UserInDB(User):
hashed_password: str
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Secure your Login System

Goal: Make the last (step 01) Created Security Flow Secure using JWT and Password Hashing.

1. Clone the project
2. Install requirements `pip install -r requirements.txt`
3. Run the project `uvicorn main:app --reload` and visit `http://localhost:8000/docs` or use Postman.
4. Click on Authenticate to login using username: johndoe & password: secret. Now call the `/users/me` endpoint.

5. Now try out the `/token` endpoint with same credentials.

```
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwiZXhwIjoxNzA1Mzg1MTM1fQ.Nt1M-QoLNO6JK-jftR5lHXnKho8RftKk5I7DJpq8nvU",
"token_type": "bearer"
}
```

We will follow along this tutorial :

[OAuth2 with Password (and hashing), Bearer with JWT tokens](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/)

After completing it you will have a simple login system following `OAuth2 with Password (and hashing), Bearer with JWT tokens`

## Concepts and Flow

Let's use the tools provided by FastAPI to handle security. And they automatically integrated into the interactive documentation system.

1. The user types the username and password in the frontend, and hits Enter.
2. The frontend (running in the user's browser) sends that username and password to a specific URL in our API.
3. The API checks that username and password, and responds with a "token".
- A "token" is just a string with some content that we can use later to verify this user.
- Normally, a token is set to expire after some time.
So, the user will have to log in again at some point later.
And if the token is stolen, the risk is less. It is not like a permanent key that will work forever (in most of the cases).
4. The frontend stores that token temporarily somewhere.
5. User clicks in the frontend to go to another section of the frontend web app.
6. The frontend needs to fetch some more data from the API.
But it needs authentication for that specific endpoint.
So, to authenticate with our API, it sends a header Authorization with a value of Bearer plus the token.


Now we have a secured login system that follows OAuth protocols and provides security for the frontend to authenticate with the backend using a username and password.

To enhance the security, we have implemented hashing for the password. This ensures that even if the password is compromised, it cannot be easily decrypted.

Now we will convert it into a proper Authentication & Authorization system.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fastapi
uvicorn
python-dotenv
python-jose
passlib
python-multipart
pydantic
Loading

0 comments on commit 4e527c5

Please sign in to comment.