Fastapi auth with OAuth2PasswordBearer, how to check if an user is connected without raise an exception? - fastapi

For an application, I have followed the fastAPI documentation for the authentification process.
By default, OAuth2PasswordBearer raise an HTTPException with status code 401. So, I can't check if an user is actually connected without return a 401 error to the client.
An example of what I want to do:
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/users/token")
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
settings = get_settings()
payload = jwt.decode(token, settings.secret_key,
algorithms=[settings.algorithm_hash])
email = payload.get("email")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
except jwt.JWTError:
raise credentials_exception
user = UserNode.get_node_with_email(token_data.email)
if user is None:
raise credentials_exception
return user
#app.get('/')
def is_connected(user = Depends(get_current_user)
# here, I can't do anything if the user is not connected,
# because an exception is raised in the OAuth2PasswordBearer __call__ method ...
return
I see OAuth2PasswordBearer class have an "auto_error" attribute, which controls if the function returns None or raises an error:
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
So i think about a workaround:
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/users/token", auto_error=False)
def get_current_user(token: str = Depends(oauth2_scheme)):
if not token:
return None
# [ ... same token decoding logic than before ... ]
return user
#app.get('/')
def is_connected(user = Depends(get_current_user)
return user
It works, but I wonder what other ways there are to do this, is there a more "official" method?

This is a good question and as far as I know, there isn't an "official" answer that is universally agreed upon.
The approach I've seen most often in the FastAPI applications that I've reviewed involves creating multiple dependencies for each use case.
While the code works similarly to the example you've provided, the key difference is that it attempts to parse the JWT every time - and doesn't only raise the credentials exception when it does not exist. Make sure the dependency accounts for malformed JWTs, invalid JWTs, etc.
Here's an example adapted to the general structure you've specified:
# ...other code
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="api/users/token",
auto_error=False
)
auth_service = AuthService() # service responsible for JWT management
async def get_user_from_token(
token: str = Depends(oauth2_scheme),
user_node: UserNode = Depends(get_user_node),
) -> Optional[User]:
try:
email = auth_service.get_email_from_token(
token=token,
secret_key=config.SECRET_KEY
)
user = await user_node.get_node_with_email(email)
return user
except Exception:
# exceptions may include no token, expired JWT, malformed JWT,
# or database errors - either way we ignore them and return None
return None
def get_current_user_required(
user: Optional[User] = Depends(get_user_from_token)
) -> Optional[User]:
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="An authenticated user is required for that action.",
headers={"WWW-Authenticate": "Bearer"},
)
return user
def get_current_user_optional(
user: Optional[User] = Depends(get_user_from_token)
) -> Optional[User]:
return user

Related

how to do async test(pytest) in fastapi with error `There is 1 other session using the database`

While I was writing the Async test code for FASTAPI there is a problem that cannot be solved. this code is for test db. I'm using postgres and in order to user db as a test, I created is_testing function. It drop and create test db.
if is_testing:
db_url = self._engine.url
if db_url.host != "localhost":
raise Exception("db host must be 'localhost' in test environment")
except_schema_db_url = f"{db_url.drivername}://{db_url.username}#{db_url.host}"
schema_name = db_url.database # test
temp_engine = create_engine(except_schema_db_url, echo=echo, pool_recycle=pool_recycle, pool_pre_ping=True)
conn = temp_engine.connect()
try:
conn = conn.execution_options(autocommit=False)
conn.execute("ROLLBACK")
conn.execute(f"DROP DATABASE {schema_name}")
except ProgrammingError:
print(f"could not drop the database, probably does not exist.")
conn.execute("ROLLBACK")
except OperationalError:
print("could not drop database because it's being accessed by other users(psql prompt open?)")
conn.execute("ROLLBACK")
print(f"test db dropped! about to create {schema_name}")
conn.execute(f"CREATE DATABASE {schema_name}")
try:
conn.execute(f"create user test with encrypted password test")
except:
print("User already exist")
temp_engine.dispose()
this is conftest.py
#pytest.fixture(scope="session")
def app():
os.environ["API_ENV"] = "test"
return create_app()
#pytest.fixture(scope="session")
def client(app):
Base.metadata.create_all(db.engine)
# Create tables
client = AsyncClient(app=app, base_url="http://test")
return client
#pytest.fixture(scope="function", autouse=True)
def session():
sess = next(db.session())
yield sess
clear_all_table_data(
session=sess,
metadata=Base.metadata,
except_tables=[]
)
sess.rollback()
def clear_all_table_data(session: Session, metadata, except_tables: List[str] = None):
session.execute("SET session_replication_role = 'replica';")
for table in metadata.sorted_tables:
if table.name not in except_tables:
session.execute(table.delete())
session.execute("SET session_replication_role = 'origin';")
session.commit()
I got error sqlalchemy.exc.OperationalError: (psycopg2.errors.ObjectInUse) database "test" is being accessed by other users DETAIL: There is 1 other session using the database. in elb check test.
and I got error TypeError: 'AsyncClient' object is not callable in another api test.
I modified client function in conftest.py
#pytest.fixture(scope="session")
def client(app):
Base.metadata.create_all(db.engine)
return AsyncClient(app=app, base_url="http://test")
I passed one test, but I received the following error from the second test.
ClientState.OPENED: "Cannot open a client instance more than once.",
ClientState.CLOSED: "Cannot reopen a client instance, once it has been closed.",
how can I fix it?
thank you for reading long question!

Basic token authorization in FastApi

I need help understanding how to process a user-supplied token in my FastApi app.
I have a simple app that takes a user-session key, this may be a jwt or not. I will then call a separate API to validate this token and proceed with the request or not.
Where should this key go in the request:
In the Authorization header as a basic token?
In a custom user-session header key/value?
In the request body with the rest of the required information?
I've been playing around with option 2 and have found several ways of doing it:
Using APIKey as described here:
async def create(api_key: APIKey = Depends(validate)):
Declaring it in the function as described in the docs here
async def create(user_session: str = Header(description="The Users session key")): and having a separate Depends in the router config,
The best approach is to build a custom dependency using any one of the already existing authentication dependencies as a reference.
Example:
class APIKeyHeader(APIKeyBase):
def __init__(
self,
*,
name: str,
scheme_name: Optional[str] = None,
description: Optional[str] = None,
auto_error: bool = True
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.header}, name=name, description=description
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key: str = request.headers.get(self.model.name)
# add your logic here, something like the one below
if not api_key:
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else:
return None
return api_key
After that, just follow this from documentation to use your dependency.

Google login for FastAPI

I am using the code below for google authentication. There is two end points (/login and /auth). At the first time I can sign in with my google account but when I want to change it, it does not ask me for Google credentials, it automatically sign in with my previous account. Is there any help?
Here is the sample code:
#app.route('/login')
async def login(request: Request):
# absolute url for callback
# we will define it below
redirect_uri = request.url_for('auth')
return await oauth.google.authorize_redirect(request, redirect_uri)
#app.route('/auth')
async def auth(request: Request):
token = await oauth.google.authorize_access_token(request)
# <=0.15
# user = await oauth.google.parse_id_token(request, token)
user = token['userinfo']
return user
You can find the full code here:
https://blog.authlib.org/2020/fastapi-google-login
clear your session first
#app.get('/logout')
async def logout(request: Request):
request.session.pop('user', None)
return RedirectResponse(url='/')
or clear your cookie

python fastapi redirect and authenticate

I'm trying to authenticate the user after visiting the registration link
(link example: http://127.0.0.1:8000/confirm-email?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9F)
My code:
#app.get("/confirm-email", status_code=200, )
def confirm_email(
token: str = fastapi.Query(..., min_length=64, max_length=256,
db: Session = fastapi.Depends(database.get_db)):
if user := crud.read_user_by(db, column='current_token', value=token):
if user.created_datetime + timedelta(minutes=30) > datetime.now(): # TODO change minutes to days
return fastapi.responses.RedirectResponse(
url="http://127.0.0.1:8000/users/me",
headers={"access_token": token, "token_type": "bearer"})
else:
raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_410_GONE,
detail="Confirmation link is expired")
else:
raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
detail="Wrong token")
#app.get("/users/me")
def read_users_me(token: str = fastapi.Depends(oauth2_scheme),
db: Session = fastapi.Depends(database.get_db)):
try:
return services.get_user_by_token(db=db, token=token)
except Exception as e:
raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
But every time I'm failing when trying to use /users/me endpoint (getting 401 error, UNAUTHORIZED).
Maybe I put the token in the wrong place or using wrong headers?
If using OAuth 2.0 and wanting to set the access_token in a request, tipically, it goes into the Authorization header like the example in the RFC: Authorization: Bearer mF_9.B5f-4.1JqM - in the example, mF_9.B5f-4.1JqM would be the value of the token.
It seems to me that you are accessing the users/me endpoint with the headers access_token: [token value] and token_type: "bearer". Instead, I believe the following header should be set: Authorization: Bearer [token value]
After a little researching, I figured out that redirection by specification can't have authorization headers (browser/client will just ignore it mainly). So even if headers are correct - it's nonsense. One possible solution to use URL.

Django JWT authentication - user is anonymous in middleware

I am using Django JWT to power up authentication system in my project.
Also, I have a middleware, and the problem is that inside it, the user is anonymous for some reason, while in the view I am able to access the correct user by request.user. This issue is driving me crazy because some time ago this code worked perfectly ! Is this JWT's bug or I am doing something wrong ?
class TimezoneMiddleware(MiddlewareMixin):
def process_request(self, request):
# request.user is ANONYMOUS HERE !!!!
if not request.user.is_anonymous:
tzname = UserProfile.objects.get(user = request.user).tz_name
if tzname:
timezone.activate(pytz.timezone(tzname))
Relevant settings.py module:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_RENDERER_CLASSES': (
'djangorestframework_camel_case.render.CamelCaseJSONRenderer',
# Any other renders
),
'DEFAULT_PARSER_CLASSES': (
'djangorestframework_camel_case.parser.CamelCaseJSONParser',
# Any other parsers
),
}
JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler',
# 'rest_authentication.views.jwt_response_payload_handler',
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': False,
'JWT_LEEWAY': 0,
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
I have also come across resources which helped me to retrieve the actual user, BUT ! I am still unable to set the timezone (timezone.activate(pytz.timezone(tzname)) seems to be ignored.
Yes, this issue is due to the JWT. You can checkout the discussion for it https://github.com/GetBlimp/django-rest-framework-jwt/issues/45 To fix this you will have to create a custom middleware which will set the request.user. Here is one I am using in my code:
from django.contrib.auth.middleware import get_user
from django.utils.functional import SimpleLazyObject
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class JWTAuthenticationMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.user = SimpleLazyObject(lambda:self.__class__.get_jwt_user(request))
return self.get_response(request)
#staticmethod
def get_jwt_user(request):
user = get_user(request)
if user.is_authenticated:
return user
jwt_authentication = JSONWebTokenAuthentication()
if jwt_authentication.get_jwt_value(request):
user, jwt = jwt_authentication.authenticate(request)
return user
Include this in the middlewares. It should come above all the middlewares which are using request.user.
#Atul Mishra: Thank you! Changed your version to the newest drf-jwt package (1.17.2). Seems like the the current github repository moved from this to here
from django.contrib.auth.middleware import get_user
from django.utils.functional import SimpleLazyObject
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class JWTAuthenticationInMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.user = SimpleLazyObject(lambda:self.__class__.get_jwt_user(request))
return self.get_response(request)
#staticmethod
def get_jwt_user(request):
# Already authenticated
user = get_user(request)
if user.is_authenticated:
return user
# Do JTW authentication
jwt_authentication = JSONWebTokenAuthentication()
authenticated = jwt_authentication.authenticate(request)
if authenticated:
user, jwt = authenticated
return user

Resources