How authentic JWS with test client Fastapi - python

I'm trying to test a endpoint that depends a jwt authentication but don't know how make this, especially send the authentication or skip, please an advice will be very useful.
My code give me that error :
AssertionError: {"detail":"Not authenticated"}
router:
#router.post(
path="/api/users",
response_model=UsersRespose,
status_code=status.HTTP_201_CREATED,
summary="Create a new Users Survey",
tags = ["Users"]
)
async def create_user_survey(
user:Users,
db: Session = Depends(get_db),
admin: Admins= Depends(get_curret_admin)
):
db_user = await get_users_by_email(db,email=user.users_email)
if db_user:
raise HTTPException(status_code=400, detail="El correo electronico ya existe")
return await create_users_survey(db=db, user=user)
dependency:
async def create_token(admin:Admin):
admin_obj = Admins.from_orm(admin)
token = encode(admin_obj.dict(), JWT_SECRET)
return dict(access_token=token, token_type="bearer")
async def get_curret_admin(db: Session=Depends(get_db), token: str = Depends(oauth2schema)):
try:
payload=decode(token, JWT_SECRET, algorithms=["HS256"])
admin= db.query(Admin).get(payload["id"])
except:
raise HTTPException(
status_code=401,
detail="Correo o password invalido"
)
return Admins.from_orm(admin)
and test
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_create_user():
mail=random_email_user()
name =random_name_user()
password = random_pasword_user()
id=random_int_user()
response=client.post("/api/users", json={"users_email":mail ,"id":id, "users_name": name},)
assert response.status_code == 201, response.text
data = response.json()
assert data["users_email"] == mail

Thanks to #MatsLindh the solution was quite simple
def override_get_currrent_admin():
return {"email":"dummy#dummy.com", "name_admin":"dummy", "id":"5"}
app.dependency_overrides[get_curret_admin]= override_get_currrent_admin

Related

Using Oauth2 with scope, error - "Depends" has no attribute "query" - FastAPI

authentication.py
from datetime import datetime, timedelta
from fastapi import Depends , HTTPException, status, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from jose import JWTError, jwt
from auth.schemas import TokenData, UserBase
from sqlalchemy.orm import Session
from db.database import get_db
from pydantic import ValidationError
from db.models import UserModel
SECRET_KEY = '7f0ef2549adac85716db6ca433a44c79021e829c8c4dd8e7d9363eb9fc800e73'
ALGORITHM = 'HS256'
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login",
scopes={"admin":"create content", "user":"interact with content"}
)
def get_user(email: str,db:Session=Depends(get_db)):
user = db.query(UserModel).filter(UserModel.email == email).first()
if not user:
raise HTTPException(status=status.HTTP_404_NOT_FOUND,
details=f"User with email: {email} not found")
return user
async def get_current_user(
security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(scopes=token_scopes, username=username)
except (JWTError, ValidationError):
raise credentials_exception
user = get_user(email=token_data.email)
if user is None:
raise credentials_exception
for scope in security_scopes.scopes:
if scope not in token_data.scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="not enough permission",
headers={"WWW-Authenticate": authenticate_value},
)
return user
def get_current_active_user(
current_user:UserBase = Security(get_current_user, scopes=["admin"])):
if not current_user:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def create_access_token(data:dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode,SECRET_KEY,algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token:str,credentials_exception):
try:
payload = jwt.decode(token,SECRET_KEY,algorithms=ALGORITHM)
email:str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
return token_data
except JWTError:
raise credentials_exception
login
from fastapi import APIRouter, Depends,HTTPException,status
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from sqlalchemy.orm import Session
from db.models import UserModel
from db.database import get_db
from . import hashing
from auth.oauth import create_access_token
from .hashing import get_password_hash
from .schemas import UserBase
router = APIRouter(tags=['authentication'])
#router.post('/login')
async def login(request:OAuth2PasswordRequestForm=Depends(),db:Session=Depends(get_db)):
user = db.query(UserModel).filter(UserModel.email == request.username).first()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Invalid Credentials")
if not hashing.verify_password(request.password,user.password):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail="Invalid Password")
access_token = create_access_token(data={"sub":user.username,"scopes":request.scopes})
return {"access_token":access_token,"token_type":"bearer"}
#router.post('/register')
async def register(request:UserBase,db:Session=Depends(get_db)):
new_user = UserModel(
email = request.email,
username = request.username,
password = get_password_hash(request.password),
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
endpoints
from fastapi import APIRouter, Depends, Security
from auth.oauth import get_current_active_user
from routers.schemas import BikeBase
from sqlalchemy.orm import Session
from db.database import get_db
from db.models import BikeModel
from .crud import add_bike
from auth.schemas import UserBase
router = APIRouter(
prefix='/bike',
tags = ['bike']
)
#router.post('/add')
def add_bike(request:BikeBase,user:UserBase=Depends(get_current_active_user),db:Session=Depends(get_db)):
bike = db.query(BikeModel).filter(BikeModel.model==request.model).first()
if bike is None:
return add_bike(request,user)
if request.model in bike:
raise Exception("Bike Model Already present")
return add_bike(request,user)
#router.get('/check')
def check(user:UserBase=Security(get_current_active_user, scopes=["admin"])):
return {"data":"scope working"}
def add_bike(request:BikeBase,user=Security(get_current_active_user, scopes=["admin"]),db:Session=Depends(get_db)):
new_bike = BikeBase(
brand = request.brand.lower(),
model = request.model.lower(),
price = request.price,
mileage = request.mileage,
max_power = request.max_power,
rating = request.rating
)
db.add(new_bike)
db.commit()
db.refresh(new_bike)
return new_bike
On the #router.post('/add') getting the error:
File "/home/raj/code/bike-forum-fastapi/./auth/oauth.py", line 24, in get_user
user = db.query(UserModel).filter(UserModel.email == email).first()
AttributeError: 'Depends' object has no attribute 'query'
When i change some endpoints to async the error changes something with coroutine, i cant remember how to produce it
#router.check('/check') endpoint works fine with scope and permission, but produces error at #router.post('/add')
i new to fastapi, the auth code is directly copied from fastapi-docs, but theres a hardcoded fake database for get_user in docs, i cant figure how to do it with a real database

self.scope['user'] always returns AnonymousUser in websocket

I have researched similar questions but can't find an answer that works for me.
I would like to get the username or user_id from a session when a user connects to a websocket.
This is what I have in consumers.py:
class PracticeConsumer(AsyncConsumer):
async def websocket_connect(self, event):
print('session data', self.scope['user'])
await self.send({"type": "websocket.accept", })
...
#database_sync_to_async
def get_user(self, user_id):
try:
return User.objects.get(username=user_id).pk
except User.DoesNotExist:
return AnonymousUser()
This is my asgi.py:
"""
ASGI config for restapi project.
It exposes the ASGI callable as a module-level variable named application.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'signup.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
)
)
})
and the user login function + token sent when user logs in:
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
authenticate_kwargs = {
self.username_field: attrs[self.username_field],
"password": attrs["password"],
}
try:
authenticate_kwargs["request"] = self.context["request"]
except KeyError:
pass
user = authenticate(**authenticate_kwargs)
if not user:
return {
'user': 'Username or password is incorrect',
}
token = RefreshToken.for_user(user)
# customizing token payload
token['username'] = user.username
token['first_name'] = user.first_name
token['last_name'] = user.last_name
token['country'] = user.profile.country
token['city'] = user.profile.city
token['bio'] = user.profile.bio
token['photo'] = json.dumps(str(user.profile.profile_pic))
user_logged_in.send(sender=user.__class__, request=self.context['request'], user=user)
if not api_settings.USER_AUTHENTICATION_RULE(user):
raise exceptions.AuthenticationFailed(
self.error_messages["no_active_account"],
"no_active_account",
)
return {
'refresh': str(token),
'access': str(token.access_token),
}
Whenever I print out self.scope['user'] upon connecting, I get AnonymousUser
UPDATE
I tried writing some custom middleware to handle simple JWT authentication:
#database_sync_to_async
def get_user(validated_token):
try:
user = get_user_model().objects.get(id=validated_token["user_id"])
print(f"{user}")
return user
except User.DoesNotExist:
return AnonymousUser()
class JwtAuthMiddleware(BaseMiddleware):
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
close_old_connections()
token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]
try:
UntypedToken(token)
except (InvalidToken, TokenError) as e:
print(e)
return None
else:
decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=["HS256"])
print(decoded_data)
scope["user"] = await get_user(validated_token=decoded_data)
return await super().__call__(scope, receive, send)
def JwtAuthMiddlewareStack(inner):
return JwtAuthMiddleware(AuthMiddlewareStack(inner))
However, this gives me this error:
File "C:\Users\15512\Desktop\django-project\peerplatform\signup\middleware.py", line 37, in __call__
token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]
KeyError: 'token'

make fastapi middleware returning custom http status instead of AuthenticationError status 400

In the following example when you pass a username in the basic auth field it raise a basic 400 error, but i want to return 401 since it's related to the authentication system.
I did tried Fastapi exceptions classes but they do not raise (i presume since we are in a starlette middleware). Il also tried JSONResponse from starlette but it doesn't work either.
AuthenticationError work and raise a 400 but it's juste an empty class that inherit from Exception so no status code can be given.
Fully working example:
import base64
import binascii
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, HTTPBasic
from starlette.authentication import AuthenticationBackend, AuthCredentials, AuthenticationError, BaseUser
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.responses import JSONResponse
class SimpleUserTest(BaseUser):
"""
user object returned to route
"""
def __init__(self, username: str, test1: str, test2: str) -> None:
self.username = username
self.test1 = test1
self.test2 = test2
#property
def is_authenticated(self) -> bool:
return True
async def jwt_auth(auth: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False))):
if auth:
return True
async def key_auth(apikey_header=Depends(HTTPBasic(auto_error=False))):
if apikey_header:
return True
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, conn):
if "Authorization" not in conn.headers:
return
auth = conn.headers["Authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() == 'bearer':
# check bearer content and decode it
user: dict = {"username": "bearer", "test1": "test1", "test2": "test2"}
elif scheme.lower() == 'basic':
decoded = base64.b64decode(credentials).decode("ascii")
username, _, password = decoded.partition(":")
if username:
# check redis here instead of return dict
print("try error raise")
raise AuthenticationError('Invalid basic auth credentials') # <= raise 400, we need 401
# user: dict = {"username": "basic auth", "test1": "test1", "test2": "test2"}
else:
print("error should raise")
return JSONResponse(status_code=401, content={'reason': str("You need to provide a username")})
else:
return JSONResponse(status_code=401, content={'reason': str("Authentication type is not supported")})
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
raise AuthenticationError('Invalid basic auth credentials')
return AuthCredentials(["authenticated"]), SimpleUserTest(**user)
async def jwt_or_key_auth(jwt_result=Depends(jwt_auth), key_result=Depends(key_auth)):
if not (key_result or jwt_result):
raise HTTPException(status_code=401, detail="Not authenticated")
app = FastAPI(
dependencies=[Depends(jwt_or_key_auth)],
middleware=[Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())]
)
#app.get("/")
async def read_items(request: Request) -> str:
return request.user.__dict__
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=5000, log_level="info")
if we set username in basic auth:
INFO: 127.0.0.1:22930 - "GET / HTTP/1.1" 400 Bad Request
so i ended up using on_error as suggested by #MatsLindh
old app:
app = FastAPI(
dependencies=[Depends(jwt_or_key_auth)],
middleware=[
Middleware(
AuthenticationMiddleware,
backend=BasicAuthBackend(),
)
],
)
new version:
app = FastAPI(
dependencies=[Depends(jwt_or_key_auth)],
middleware=[
Middleware(
AuthenticationMiddleware,
backend=BasicAuthBackend(),
on_error=lambda conn, exc: JSONResponse({"detail": str(exc)}, status_code=401),
)
],
)
I choose to use JSONResponse and return a "detail" key/value to emulate a classic 401 fastapi httperror

Error with asynchronous request in DRF

I need to fulfill a request to two services.
The code looks like this:
async def post1(data):
response = await aiohttp.request('post', 'http://', json=data)
json_response = await response.json()
response.close()
return json_response
async def get2():
response = await aiohttp.request('get', 'http://')
json_response = await response.json()
response.close()
return json_response
async def asynchronous(parameters):
task1 = post1(parameters['data'])
task2 = get2()
result_list = []
for body in await asyncio.gather(task1, task2):
result_list.append(body)
return result_list
If I run the code locally, it's OK. The code looks like this:
if __name__ == "__main__":
ioloop = asyncio.get_event_loop()
parameters = {'data': data}
result = ioloop.run_until_complete(asynchronous(parameters))
ioloop.close()
print(result)
I get the right result. But if I try to execute code from the DRF method, an error occurs:
TypeError: object _SessionRequestContextManager can't be used in
'await' expression
example code that I run:
.....
class MyViewSet(GenericAPIView):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
......
ioloop = asyncio.get_event_loop()
result = ioloop.run_until_complete(asynchronous(serializer.data)) # <<<<< error here
ioloop.close()
......
return Response(serializer.data, status=status.HTTP_201_CREATED)
Please, tell me what the problem may be?
The object returned by aiohttp.request cannot be awaited, it must be used as an async context manager. This code:
response = await aiohttp.request('post', 'http://', json=data)
json_response = await response.json()
response.close()
needs to changed to something like:
async with aiohttp.request('post', 'http://', json=data) as response:
json_response = await response.json()
See the documentation for more usage examples.
Perhaps you have a different aiohttp version on the server where you run DRF, which is why it works locally and fails under DRF.
Try it https://github.com/Skorpyon/aiorestframework
Create some middleware for authentication:
import re
from xml.etree import ElementTree as etree
from json.decoder import JSONDecodeError
from multidict import MultiDict, MultiDictProxy
from aiohttp import web
from aiohttp.hdrs import (
METH_POST, METH_PUT, METH_PATCH, METH_DELETE
)
from aiorestframework import exceptions
from aiorestframework omport serializers
from aiorestframework import Response
from aiorestframework.views import BaseViewSet
from aiorestframework.permissions import set_permissions, AllowAny
from aiorestframework.app import APIApplication
from my_project.settings import Settings # Generated by aiohttp-devtools
TOKEN_RE = re.compile(r'^\s*BEARER\s{,3}(\S{64})\s*$')
async def token_authentication(app, handler):
"""
Authorization middleware
Catching Authorization: BEARER <token> from request headers
Found user in Tarantool by token and bind User or AnonymousUser to request
"""
async def middleware_handler(request):
# Check that `Authorization` header exists
if 'authorization' in request.headers:
authorization = request.headers['authorization']
# Check matches in header value
match = TOKEN_RE.match(authorization)
if not match:
setattr(request, 'user', AnonymousUser())
return await handler(request)
else:
token = match[1]
elif 'authorization_token' in request.query:
token = request.query['authorization_token']
else:
setattr(request, 'user', AnonymousUser())
return await handler(request)
# Try select user auth record from Tarantool by token index
res = await app['tnt'].select('auth', [token, ])
cached = res.body
if not cached:
raise exceptions.AuthenticationFailed()
# Build basic user data and bind it to User instance
record = cached[0]
user = User()
user.bind_cached_tarantool(record)
# Add User to request
setattr(request, 'user', user)
return await handler(request)
return middleware_handler
And for data extraction from request:
DATA_METHODS = [METH_POST, METH_PUT, METH_PATCH, METH_DELETE]
JSON_CONTENT = ['application/json', ]
XML_CONTENT = ['application/xml', ]
FORM_CONTENT = ['application/x-www-form-urlencoded', 'multipart/form-data']
async def request_data_handler(app, handler):
"""
Request .data middleware
Try extract POST data or application/json from request body
"""
async def middleware_handler(request):
data = None
if request.method in DATA_METHODS:
if request.has_body:
if request.content_type in JSON_CONTENT:
# If request has body - try to decode it to JSON
try:
data = await request.json()
except JSONDecodeError:
raise exceptions.ParseError()
elif request.content_type in XML_CONTENT:
if request.charset is not None:
encoding = request.charset
else:
encoding = api_settings.DEFAULT_CHARSET
parser = etree.XMLParser(encoding=encoding)
try:
text = await request.text()
tree = etree.XML(text, parser=parser)
except (etree.ParseError, ValueError) as exc:
raise exceptions.ParseError(
detail='XML parse error - %s' % str(exc))
data = tree
elif request.content_type in FORM_CONTENT:
data = await request.post()
if data is None:
# If not catch any data create empty MultiDictProxy
data = MultiDictProxy(MultiDict())
# Attach extracted data to request
setattr(request, 'data', data)
return await handler(request)
return middleware_handler
Create few serializers:
class UserRegisterSerializer(s.Serializer):
"""Register new user"""
email = s.EmailField(max_length=256)
password = s.CharField(min_length=8, max_length=64)
first_name = s.CharField(min_length=2, max_length=64)
middle_name = s.CharField(default='', min_length=2, max_length=64,
required=False, allow_blank=True)
last_name = s.CharField(min_length=2, max_length=64)
phone = s.CharField(max_length=32, required=False,
allow_blank=True, default='')
async def register_user(self, app):
user = User()
data = self.validated_data
try:
await user.register_user(data, app)
except Error as e:
resolve_db_exception(e, self)
return user
And few ViewSets. It may be nested in bindings['custom']
class UserViewSet(BaseViewSet):
name = 'user'
lookup_url_kwarg = '{user_id:[0-9a-f]{32}}'
permission_classes = [AllowAny, ]
bindings = {
'list': {
'retrieve': 'get',
'update': 'put'
},
'custom': {
'list': {
'set_status': 'post',
'create_new_sip_password': 'post',
'get_registration_domain': 'get',
'report': UserReportViewSet
}
}
}
#staticmethod
async def resolve_sip_host(data, user, app):
sip_host = await resolve_registration_switch(user, app)
data.update({'sip_host': sip_host})
async def retrieve(self, request):
user = User()
await user.load_from_db(request.match_info['user_id'], request.app)
serializer = user_ser.UserProfileSerializer(instance=user)
data = serializer.data
await self.resolve_sip_host(data, user, request.app)
return Response(data=data)
#atomic
#set_permissions([AuthenticatedOnly, IsCompanyMember, CompanyIsEnabled])
async def update(self, request):
serializer = user_ser.UserProfileSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = await serializer.update_user(user_id=request.user.id,
app=request.app)
serializer = user_ser.UserProfileSerializer(instance=user)
data = serializer.data
await self.resolve_sip_host(data, user, request.app)
return Response(data=data)
Register Viewsets and run Application:
def setup_routes(app: APIApplication):
"""Add app routes here"""
# Auth API
app.router.register_viewset('/auth', auth_vws.AuthViewSet())
# User API
app.router.register_viewset('/user', user_vws.UserViewSet())
# Root redirection to Swagger
redirect = app.router.add_resource('/', name='home_redirect')
redirect.add_route('*', swagger_redirect)
def create_api_app():
sentry = get_sentry_middleware(settings.SENTRY_CONNECT_STRING, settings.SENTRY_ENVIRONMENT)
middlewares = [sentry, token_authentication, request_data_handler]
api_app = APIApplication(name='api', middlewares=middlewares,
client_max_size=10*(1024**2))
api_app.on_startup.append(startup.startup_api)
api_app.on_shutdown.append(startup.shutdown_api)
api_app.on_cleanup.append(startup.cleanup_api)
setup_routes(api_app)
if __name__ == '__main__':
app = create_api_app()
web.run_app(app)

Python sqlalchemy telegram bot

I use sqlalchemy and sometimes i have this error:
sqlalchemy.exc.ResourceClosedError: This Connection is closed
I think this is a problem with session but I do not know how to fix it
Code:
engine = create_engine(config['engine'])
connection = engine.connect()
Session = sessionmaker(bind=engine)
session = None
bot = telebot.TeleBot(config['telegram_token'])
app = flask.Flask(__name__)
#app.route('/tg', methods=['POST'])
def webhook():
if flask.request.headers.get('content-type') == 'application/json':
global session
session = Session()
json_string = flask.request.get_data().decode('utf-8')
update = telebot.types.Update.de_json(json_string)
bot.process_new_updates([update])
return ''
else:
flask.abort(403)
#bot.message_handler(func=lambda message: message.chat.id == config['chat_id'], content_types=['text'])
def chat_handler(message):
tg_id = message.from_user.id
in_db = session.query(ChatMessage).filter(
ChatMessage.message_id == message.message_id).count()
if in_db == 0:
die_time = datetime.now().replace(second=0, microsecond=0) + \
timedelta(seconds=config['chat_message_lifetime'])
if message.pinned_message != None:
die_time = None
chat_mess = ChatMessage(
user_id=None, advorder_id=None, message_id=message.message_id, dietime=die_time)
session.add(chat_mess)
session.commit()
bot.remove_webhook()
bot.set_webhook(url=config['webhook_url_base'],
certificate=open(config['webhook_ssl_cert'], 'r'))
app.run()

Categories