forked from panaverse/learn-generative-ai
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request panaverse#17 from panaverse/junaid
Add OAuth2 Baby Steps 01-04
- Loading branch information
Showing
63 changed files
with
5,261 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
4 changes: 4 additions & 0 deletions
4
08_everything_Is_an_api/13_oauth2/01_basic_auth/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
16
08_everything_Is_an_api/13_oauth2/01_basic_auth/service.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
9 changes: 9 additions & 0 deletions
9
08_everything_Is_an_api/13_oauth2/02_jwt_auth/01_hashing_and_access_tokens/data.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
} |
43 changes: 43 additions & 0 deletions
43
08_everything_Is_an_api/13_oauth2/02_jwt_auth/01_hashing_and_access_tokens/main.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}] |
20 changes: 20 additions & 0 deletions
20
08_everything_Is_an_api/13_oauth2/02_jwt_auth/01_hashing_and_access_tokens/models.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
47 changes: 47 additions & 0 deletions
47
...erything_Is_an_api/13_oauth2/02_jwt_auth/01_hashing_and_access_tokens/readme.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
7 changes: 7 additions & 0 deletions
7
08_everything_Is_an_api/13_oauth2/02_jwt_auth/01_hashing_and_access_tokens/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
fastapi | ||
uvicorn | ||
python-dotenv | ||
python-jose | ||
passlib | ||
python-multipart | ||
pydantic |
Oops, something went wrong.