FastAPI (SlowAPI Dynamic Rate Limit using keyfunc) - python

Currently, I am trying to find a way to use a custom _keyfunc with SlowAPI that will allow me to access user data. The customer function I wanted to pass through looks like this:
async def get_limiter_id(current_user: User = Depends(get_current_user)):
print(current_user)
return str(current_user.retailer_id)
limiter = Limiter(
key_func= get_limiter_id,
default_limits=["200 per day", "50 per hour", "1 per minute"]
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
The problem is that it doesn't accept asynchronous functions, so if I try to pass in: current_user: User = Depends(get_current_user) as an argument, it won't work because either you make the function non async, in which case the current user won't get passed through properly, or you make it async but then it will it just pass the coroutine object through instead because you didn't await the result (which isn't possible with how SlowAPI is written atm).
This is the get_current_user function for clarity sake:
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
print(username)
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(fake_user_db, username=token_data.username)
if user is None:
raise credentials_exception
if user.enabled == 0:
raise HTTPException(status_code=400, detail="Inactive user")
return user
One solution I thought of was to grab to put the retailer_id in the header so that I could have a custom function like this:
def get_retailer_id(request: Request):
token = request.headers.get('retailer_id')
return retailer_id
But I don't know how I can get this user specific information written into the header of all paths at creation of the bearer token. Any thoughts on how I could solve this?

Solved the issue. Didn't realise that I could just store this custom info in the token payload (upon creation of the token) and then bring it down from the header. There is a bit of code duplication here, but it's a good enough solution:
def get_retailer_id(request: Request):
token = request.headers.get('Authorization')
token = token.replace("Bearer ", "")
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token is invalid or payload is corrupt",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
retailer_id: str = payload.get("retailer_id")
return str(retailer_id)
except Exception:
raise credentials_exception

Related

How to retrieve specific data from request in pytest

Hi so what I am trying to do is when I run the test the user will be created (makes an id of an user) and at the end of the test I want to delete that user. The way I decided to go to find that user is to pass the username I gave to that user in this case 'test123'.
So first I need the auth and get_user_by_username to run before delete. But when I put assert on user.status_code I get 422 instead of 200. When I try it in the swagger it works fine and I get 200 response
Here is the code:
def test_delete_new_users():
auth = client.post('/token',
data={'username': 'test123', 'password': 'test123'}
)
access_token = auth.json().get('access_token')
user = client.post(
'/user/get_user_by_username',
json={"username": "test123", }
)
assert user.status_code == 200
response = client.delete(
'/user/delete/' + user.json().get('user_id'),
headers={'Authorization': 'bearer' + access_token}
)
assert response.status_code == 200
When I remove the assert part of the code and just run it I get this error:
FAILED tests_main.py::test_delete_new_users - TypeError: can only concatenate str (not "NoneType") to str
this is get_user_by_username API:
#router.post('/get_user_by_username', response_model=UserDisplay)
async def get_user_by_username(username: str, db: Session = Depends(database.get_db)):
user = db.query(models.User).filter(models.User.username == username).first()
if user is None:
raise HTTPException(status_code=404, detail='Not found')
return user
This is the request and response on swagger:

Unexpected indentation when return in python

when I try to return but I got an error in 2nd return signup_result & return login_result
https://github.com/microsoft/pyright/blob/main/docs/configuration.md#reportUndefinedVariable
"return" can be used only within a functionPylance
here is utils.py
class CognitoResponse(object):
def __init__(self, access_token, refresh_token, cognito_user_id=None):
self.access_token = access_token
self.refresh_token = refresh_token
self.cognito_user_id = cognito_user_id
def cognito_signup(username: str, password: str):
return signup_result
# In order to get the ID and authenticate, use AWS Cognito
client = boto3.client('cognito-idp', region_name=os.environ.get('COGNITO_REGION_NAME'))
try:
response = client.sign_up(
ClientId=os.environ.get('COGNITO_USER_CLIENT_ID'),
Username=username,
Password=password
)
except Exception as e: # Generally, will trigger upon non-unique email
raise HTTPException(status_code=400, detail=f"{e}")
user_sub = response['UserSub']
# This will confirm user registration as an admin without a confirmation code
client.admin_confirm_sign_up(
UserPoolId=os.environ.get('USER_POOL_ID'),
Username=username,
)
# Now authenticate the user and return the tokens
auth_response = client.initiate_auth(
ClientId=os.environ.get('COGNITO_USER_CLIENT_ID'),
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password
}
)
access_token = auth_response['AuthenticationResult']['AccessToken']
refresh_token = auth_response['AuthenticationResult']['RefreshToken']
signup_result = utils.CognitoResponse(
access_token=access_token,
refresh_token=refresh_token,
cognito_user_id=user_sub
)
return signup_result
def cognito_login(username: str, password: str):
return login_result
client = boto3.client('cognito-idp', region_name=os.environ.get('COGNITO_REGION_NAME'))
# Authenticate the user and return the tokens
try:
auth_response = client.initiate_auth(
ClientId=os.environ.get('COGNITO_USER_CLIENT_ID'),
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password
}
)
except Exception as e: # Generally, will trigger upon wrong email/password
raise HTTPException(status_code=400, detail=f"{e}")
access_token = auth_response['AuthenticationResult']['AccessToken']
refresh_token = auth_response['AuthenticationResult']['RefreshToken']
login_result = utils.CognitoResponse(
access_token=access_token,
refresh_token=refresh_token
)
return login_result
I also try to tab 2 times to avoid indentation error in return signup_result & return login_result but still got the same error Unexpected indentationPylance
def cognito_login(username: str, password: str):
return login_result
client = boto3.client('cognito-idp', region_name=os.environ.get('COGNITO_REGION_NAME'))
# Authenticate the user and return the tokens
try:
auth_response = client.initiate_auth(
ClientId=os.environ.get('COGNITO_USER_CLIENT_ID'),
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password
}
)
# lots more code...
The cognito_login() function contains only one line of code return login_result because that is the only code that is indented underneath the function.
The rest of the following code is not indented underneath the function, therefore it is not part of the function.
Indentation is very important in Python.
Your code should likely be formatted as follows:
def cognito_login(username: str, password: str):
#return login_result remove this as the next lines of code need to run
client = boto3.client('cognito-idp', region_name=os.environ.get('COGNITO_REGION_NAME'))
# Authenticate the user and return the tokens
try:
auth_response = client.initiate_auth(
ClientId=os.environ.get('COGNITO_USER_CLIENT_ID'),
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password
}
)
# lots more code...

how to logout from jwt security scheme in fastapi

I am trying to write a logout function in fastapi. For logging out from server side, I am setting the token expiry time to 0 and sending it to client, expecting that this would invalidate the token right at that movement. However, it is not working as expect and even after logout I am able to access the protected APIs. What is wrong here? It this a good approach to logout in jwt scheme? Please help.
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
import uvicorn
import pytz
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_SECONDS = 200 # expire the token, even though user has not logged out. Change the time for your testing.
utc=pytz.UTC
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe#example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
},
"ravi": {
"username": "ravi",
"full_name": "ravi singh",
"email": "ravi.singh#gmail.com",
"hashed_password": "$2b$12$ODf2vUEfanF3P1JykF0CgO7jafMA9RWCuqZxLUAqCcQJ1FYxxFROC",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice#example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
expires: datetime
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# get the current user from auth token
async def get_current_user(token: str = Depends(oauth2_scheme)):
# define credential exception
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# decode token and extract username and expires data
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
exps:int = payload.get("exp")
# validate username
if username is None:
raise credentials_exception
token_data = TokenData(username=username, expires=exps) # exps of int is converted to datetime type
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
# check token expiration
if exps is None:
raise credentials_exception
if utc.localize(datetime.utcnow()) > token_data.expires:
print("Token is expired.\n")
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
#app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()): # login function to get access 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(seconds=ACCESS_TOKEN_EXPIRE_SECONDS)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
#app.post("/logout", response_model=Token)
async def logout(current_user: User = Depends(get_current_active_user)): # logout function to delete access token
token_data = TokenData(username=current_user.username, expires=0)
return token_data
#return "User logout sucessful."
#app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
#app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
return [{"item_id": "Foo", "owner": current_user.username}]
#app.get("/protected_hi")
async def protected_hi(current_user: User = Depends(get_current_active_user)):
return "Hi! How are you? You are in a protected Zone."
#app.get("/unprotected_hi")
async def unprotected_hi():
return "Hi! How are you? You are in an un-protected Zone."
if __name__ == "__main__":
import uvicorn
uvicorn.run('jwt_fourth:app', host="0.0.0.0", port=5000, reload=True)

Fastapi OAuth2PasswordRequestForm - add a custom field 'rentalpoint' into validation form

Is it possible to add a custom field into the OAuth2PasswordRequestForm? I need the content of the field to be returned in the token as "rentalpoint". Now in the return "rentalpoint" holds a fixed string "rentalpoint": "cologne" but i want to replace it with something like this: "rentalpoint": request.rentalpoint. How can i do that?
#router.post('/')
def login(request: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)):
user = db.query(models.User).filter(models.User.email == request.username).first()
if not user:
raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail='Invalid credentials')
if not Hash.verify(user.password, request.password):
raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail='Incorrect password')
access_token = JWTtoken.create_access_token(data={"sub": user.email})
return {"access_token": access_token, "token_type": "bearer", "rentalpoint": "cologne", "user_id": request.username}

fastapi - optional OAuth2 authentication

I need to create a API with a route that is able to recognize if the current user is the one indicated in the request or not (also no auth should be valid)
For the others paths I followed https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/ and everything work with Bearer with JWT tokens like this
user: User = Depends(get_current_active_user)
Modifying the methods provided by the docs I tried with
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth")
async def get_user_or_none(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)):
"""
Return the current active user if is present (using the token Bearer) or None
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
except JWTError:
return None
# check user in db
user = crud.get_user(db, username)
if user is None:
return None
return user
#router.get("/api/{user_id}/structure")
async def get_user_structure(
user_id: int,
user = Depends(get_user_or_none),
db: Session = Depends(get_db)
):
# do_something() if user.id == user_id else do_something_else()
but I receive an error
401 Error: Unauthorized {
"detail": "Not authenticated"
}
You need to use a different OAuth2PasswordBearer for these optionally authenticated endpoints with auto_error=False, e.g.
optional_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth", auto_error=False)
async def get_user_or_none(db: Session = Depends(get_db), token: str = Depends(optional_oauth2_scheme)):
...
Now the token will be None when the Authorization header isn't provided, resulting in your desired behavior.

Categories