Logged User Fixture Does Not Work (FastApi with Pytest) - python

I am making an authenticated client that is a pytest fixture of my fastapi application,the code below shows an authenticated user,that should be able for example to create posts,update and delete them.But when I use it in my tests,it shows an 401 response error (that is returned when user is not logged)
#pytest.fixture()
def authenticated_user(client):
user_data = {
'email': 'julioooo#gmail.com',
'password': '123',
}
res = client.post('/users/create', json={
**user_data,
'name': 'julin',
'job': 'farmer',
})
user_id = res.json() # this returns the user id
# to send a request to api to confirm user creation
user_id = user_id['id']
verify_user = client.post(f'/users/confirm-user/{user_id}').json()
print(verify_user) #print the confirmation message (debugging process)
log_user_res = client.post('/auth/login', json={
**user_data
}) # this returns in the header a httponly cookie...
# response.set_cookie() method
client.headers={**log_user_res.headers}
return client
After some time trying to debug this fixture I thought,well,the headers actually DO HAVE the httponly cookie in it,so the problem is in another part of the code,maybe in the part that checks if the user is logged or not
def get_current_user(
request: Request,
db=Depends(connect)
):
user_id = request.cookies.get('Authorization')
user = User.objects(id=user_id).first()
if not user:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials or Not Logged or User Does Not Exist",
headers={"WWW-Authenticate": "Bearer"},
)
raise credentials_exception
return user
And I couldn't find the problem,then I realized that this dependency of connect function actually returns a connection from the production db, not from the testing one,soo I created a new one that is :
def test_get_current_user(request: Request, db=Depends(test_connect)):
load_dotenv()
test_connection_url = os.getenv('test_connection_url')
user_id = request.cookies.get('Authorization')
client = MongoClient(test_connection_url)
users = client['TestJC']['users']
user = users.find_one({"_id": user_id})
if not user:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials or Not Logged or User Does Not Exist",
headers={"WWW-Authenticate": "Bearer"},
)
raise credentials_exception
return user
PS: In the original function I use mongoengine to be more practical,in this one I use pymongo actually because the test_connect function that returns a pymongo connection instance,is used for tests,because it was the only way I found out to fix a bug that I had in my code that every time I deleted the test db after ending up the tests,it also deleted the production db (which is not cool), because Users.objects().delete() did not work and users.drop() did,I am using this way,but returning to the point....
PS2: If you have seen this question before I updated it,sorry for the bad question,I was very sleepy XD
I even overrided the get_current_user dependency to this test one trying to fix the problem
I even created a "headers version" of this authenticated user fixture where I only returned client.headers and in the request just put in the post method headers={**authenticated_headers}
BUT NOTHING SEEMS TO WORK,every single time is just 401..401...AND MORE 401
Thank you guys for your time here I appreciate A LOT :)

Related

Fastapi How to restrict token creation to defined users

I have a dictionary of users with (username, password) as key, value pair.
i would like to restrict the authorisation creation to only users in my dictionary.
So any other user who is not in the dictionary shouldn't be able to create a token.
I try this but it's not working, I can still create token to a new user.
#api.post("/token")
async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
if not authenticate_user(username=form_data.username,
password=form_data.password):
raise HTTPException(status_code=403, detail='Authentication failed')
else:
return {
"access_token": form_data.username,
"token_type": "bearer"
}
raise HTTPException(status_code=403,
detail='Authentication failed')
You never compare anything against form_data.username - the only thing you do is that you start looping over the user, and you check whether the first users password match - well, the user's password. This will always be true.
Instead, retrieve the user you're looking for and compare the password if present:
#api.post("/token")
async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username)
if user and authenticate_user(form_data=username, password=form_data.password):
return {
"access_token": form_data.username,
"token_type": "bearer"
}
raise HTTPException(status_code=403,
detail='Authentication failed')
I'd also like to point out that you're using the users username as the token. This means that anyone can fake a token by simply supplying the username they want to be authenticated as.
Instead, use secrets.token_urlsafe() to generate a token that can be used as an authentication secret. You'll need to store these tokens somewhere, but in this example it seems like you're storing everything in the currently running Python application for users, so you can do the same for tokens. Define valid_tokens = {} somewhere when initializing your application, then insert rows into this dict to assign a username.
token = secrets.token_urlsafe(32)
valid_tokens[token] = form_data.user_name
return {
"access_token": token,
"token_type": "bearer"
}
You can then verify this token in a separate function and use it to look up the username of the logged in user from the token.

How can I bind the correct user to the correct session when storing a token in redis?

Help me understand how user sessions work based on JWT. Im using fastapi with static Jinja Template for now. But I think the question is not about a specific framework, but about the logic of the whole.
Now I have implemented the following functionality:
When a user logs in, a JWT is created which is written to redis in the following form
{
'SOMe_KEY' : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0.yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
}
The token has the necessary information about the user: id, email, permission_type
now, as i understand it, every request which should check for authorization should either check if the user has a session or redirect to the log_in page
My question:
How to match a user with a token in redis, this query here
session = await redis_cache.get('SOMe_KEY')
How to understand which key to pass in the get('SOMe_KEY') query
And which key to use when generating a token record in the database.
I.e. now i have hardcode for one user, there is a session key in the database and i pass the same key in the query. But what should be the logic for a large quantity of users?
Code sample
#router.post('/sign-in')
async def sign_in(
user_data: OAuth2PasswordRequestForm = Depends(),
service: AuthService = Depends(),
):
token = await service.authenticate_user(
user_data.username,
user_data.password
)
await redis_cache.set('SOMe_KEY', token)
#router.get("/private")
def read_private(username: str = Depends(get_current_user)):
return {"username": username, "private": "get some private data"}
async def get_current_user():
try:
session = await redis_cache.get('SOMe_KEY')
payload = jwt.decode(
session,
config('JWT_SECRET'),
algorithms=config('JWT_ALGORITHM'),
)
return payload['user']
except Exception as e:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid authentication"
)

How to use a jwt from a different issuer?

I am building an flask API that will manipulate data to be used in thingsboard.
Long story short, client logs in via thingsboard, get a jwt from that.
But the API rejects the jwt saying that signature verification failed.
Same secret key is being used on both sides.
Both thingsboard is on a remote server, API is currently local during development.
New to jwt manipulation, what am I missing?
Make sure that JWT_AUTH_USERNAME_KEY fit with the username that the library uses to get the user.
For example, when you get a Thingsboard JWT the username is in the sub key, and many libraries use username by default.
Maybe this piece of code can help you (implementation with JWT Rest Framework:
class CustomAuth(JSONWebTokenAuthentication):
def authenticate_credentials(self, payload):
"""
Returns an active user that matches the payload's user id and email.
"""
User = get_user_model()
# Thingsboard payload
if payload.get('sub') is not None:
username = payload.get('sub')
else:
# Our payload
username = payload.get('username')
if not username:
msg = _('Invalid payload.')
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
msg = _('Invalid signature.')
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.AuthenticationFailed(msg)
return user
Version 3.21.0 of flask-jwt-extended adds a JWT_DECODE_ISSUER option which should solve this for you: https://github.com/vimalloc/flask-jwt-extended/releases/tag/3.21.0

OAUTH2gLogging user out of their session Django

I am using the OAUTH API to verify access to the users google calendar. They are not logging into my system using OAUTH, just accepting my site accessing their data.
The problem is, if the user is logged out of their google account and after they hit verify, it force logs them out of their session and I have no way of linking them back up.
This DOES work if they are already logged into the google account in their browser session and hit accept, they will be redirected to the right page.
I replicate the error when the cache, cookies are clear and they need to relogin into their google account to verify.
I've tried storing the session ID, etc, but the request parameter is not containing the same request data as the initial view, so there is a conflict in the data I am trying to retrieve.
The user is logged in using the standard Django libraries for the credentials model.
CODE
FLOW = flow_from_clientsecrets(
CLIENT_SECRETS,
scope='https://www.googleapis.com/auth/calendar.readonly',
redirect_uri='http://127.0.0.1:8000/oauth2callback')
'''''''''''''''''''''''''''''''''''''''''''''''''''
Main function dealing with auth verification
'''''''''''''''''''''''''''''''''''''''''''''''''''
def index(request):
current_user = User.objects.get(username=request.user.username)
storage = Storage(CredentialsModel, 'id', current_user, 'credential')
credential = storage.get()
if credential is None or credential.invalid == True:
FLOW.params['state'] = xsrfutil.generate_token(settings.SECRET_KEY,
request.user.id)
authorize_url = FLOW.step1_get_authorize_url()
return redirect(authorize_url)
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
User then calls the data function once authenticated
''''''''''''''''''''''''''''''''''''''
def auth_return(request):
print("THE CURRENTLY REQUESTED USER IN THIS SESSION REQUEST IS %s"%(request.user.username))
credential = FLOW.step2_exchange(request.REQUEST)
try:
current_user = User.objects.get(id=request.user.id)
except:
return HttpResponseRedirect("/login")
storage = Storage(CredentialsModel, 'id', current_user, 'credential')
storage.put(credential)
return HttpResponseRedirect("/get_cal")
Ok, so this was a bit more involved than I thought.
I fixed this by adding a state parameter of the currently logged in username.
logged_in_username = request.user.username
user_data = {'var_test' : logged_in_username}
pass_param = urllib.urlencode(user_data)
FLOW.params['state']=pass_param
authorize_url = FLOW.step1_get_authorize_url()
This gave me the ability to query the user from the DB via the User model in Django contrib. I parsed out the state var from the URL:
#Get the currently logged in username
user_variable_data = str(FLOW.params['state'])
#get rid of the var_test= preprended text data for parsing reasons
user_variable_data = user_variable_data[9:]
#Get that user from the database in the form of a user object
current_user = User.objects.get(username=user_variable_data)
and then built a custom backend authentication file to auth the user without a password to maintain the request like nothing weird ever even happened.
user = authenticate(username=user_variable_data)
login(request, user)
print("AUTHENTICATED")
Appended this to settings file
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'procrastinate.auth_backend.PasswordlessAuthBackend',
)
Custom Backend File
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class PasswordlessAuthBackend(ModelBackend):
"""Log in to Django without providing a password.
"""
def authenticate(self, username=None):
try:
return User.objects.get(username=username)
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None

Django Basic HTTP Auth "NoneType" object has no attribute "username"

Here is my middlware:
class BasicAuthMiddleware():
def process_request(self, request):
if request.META.get("HTTP_AUTHORIZATION"):
encoded_auth = request.META.get("HTTP_AUTHORIZATION")
encoded_auth = encoded_auth[6:]
auth = base64.b64decode(encoded_auth)
auth_email = auth.split(":")[0]
auth_password = auth.split(":")[1]
user = authenticate(username=auth_email, password=auth_password)
Here is an example of the rpc/api call we make to login:
#rpc("user", "signin")
#pre("email", validate_email)
def _(request, user, email, password):
user = authenticate(username=email, password=password)
if not user:
raise ApiException(400, "The email or password you entered is incorrect.")
if not user.is_active:
raise ApiException(403, "This account has been deactivated.")
login(request, user)
return user.toDict()
Ihave included the middleware in setting.py
I get this error when using postman trying to an api call to this:
#rpc("reservation", "listCurrent", loggedIn=True)
def _(request, user):
return [dict(r.toDict(), **{ 'cancelationPolicy': evaluated_cancelation_policies(user, r.user, r.resource, r.modality, r.timeFrom, r.timeTo, r.count) }) for r in Reservation.objects.filter(user=user, deleted=False, timeTo__gt=timezone.now()).order_by('-timeFrom', '-timeTo')]
It seems that user variable is equal to none. Since normally it is part of the request but in the middleware process_request can only take 2 arguments like self, request.
I feel like I am not understanding something here. I am pretty new to django so just trying to figure this out. How would I go about using my basic http authentication middleware to authenticate in this case?

Categories