Python FastAPI: Fixing a Bug in the Authentication Process
In the fourth post of our Python FastAPI learning series, we introduced a bug in the authentication process. In this post, we describe the bug and discuss how to fix it.
🐍 Index of the Complete Series.
Python FastAPI: Fixing a Bug in the Authentication Process |
The code requires Python 3.12.4. Please refer to the following discussion on how to upgrade to Python 3.12.4.
🚀 Please note, complete code for this post can be downloaded from GitHub with:
git clone -b v0.8.0 https://github.com/behai-nguyen/fastapi_learning.git
❶ The bug occurs when we log in using the Swagger UI Authorize
button. The authentication process is successful, but the following error appears on the Swagger UI screen:
auth error SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
Please see the screenshot illustration below:
It is expecting a JSON response, but the response defaults to HTML.
To get this functionality to work correctly, the endpoint handler method for the
https://0.0.0.0:port/auth/token
path should default to return JSON
as per the original example. It should only return HTML (the home page) when
explicitly requested.
❷ After some consideration, I have decided to use an additional hidden field alongside
the username
and password
fields. If this hidden field is
present and its value is text/html
, then we return the HTML home page.
Since we have already used the custom header field
x-expected-format
to check for a
JSON response,
we will adopt this custom header field as our new hidden field as well.
We will change x-expected-format
’s constant name to
RESPONSE_FORMAT
from FORMAT_HEADER
to be more generically appropriate.
❸ We will make changes to only a few files. There are no new files. Below is the list of the updated files.
– Please note, those marked with ★ are updated, and those marked with ☆ are new.
/home/behai/fastapi_learning/
.
├── README.md ★
├── src
│ └── fastapi_learning
│ ├── common
│ │ └── consts.py ★
│ ├── controllers
│ │ ├── auth.py ★
│ │ └── __init__.py ★
│ ├── __init__.py ★
│ └── templates
│ └── auth
│ └── login.html ★
└── tests
└── integration
├── test_admin_itgt.py ★
└── test_auth_itgt.py ★
❹ In this section, we will discuss the code changes.
⓵ common/consts.py
:
We just renamed a constant as previously described.
⓶ fastapi_learning/__init__.py
: We added the classes
Token
and
TokenData
as per the later official tutorials, starting with
OAuth2 with Password (and hashing), Bearer with JWT tokens.
In the Token
class, we have an additional detail
string field for informational purposes. We added them to this package file so
that they are accessible by all other modules in the application.
⓷ controllers/__init__.py
: We added a new method
async def html_req(request: Request)
as
discussed previously. Note that we check both the
request headers and the field list for x-expected-format
, with request headers having higher precedence.
⓸ controllers/auth.py
: We made the following
refactorings to
async def login(...)
:
⑴ Annotated the return value as Union[Token, None]
.
⑵ The sub-method
async def bad_login(...)
refactored to:
async def bad_login(op_status: ResultStatus):
...
if await html_req(request):
return RedirectResponse(url=f"{router.url_path_for('login_page')}?state={op_status.code}",
status_code=status.HTTP_303_SEE_OTHER)
else:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail=message)
Previously, it was:
def bad_login(op_status: ResultStatus):
...
if json_req(request):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail=message)
else:
return RedirectResponse(url=f"{router.url_path_for('login_page')}?state={op_status.code}",
status_code=status.HTTP_303_SEE_OTHER)
⑶ Fixed the two return statements to:
return RedirectResponse(url=router.url_path_for('home_page'), status_code=status.HTTP_303_SEE_OTHER) \
if await html_req(request) else \
Token(access_token=request.session["access_token"], token_type="bearer", detail=LOGGED_IN_SESSION_MSG)
...
return RedirectResponse(url=router.url_path_for('home_page'), status_code=status.HTTP_303_SEE_OTHER) \
if await html_req(request) else Token(access_token=user_username, token_type="bearer", detail="")
The default response is always JSON. It will only return the HTML home page if explicitly requested. In the previous revisions, they were:
return {"detail": LOGGED_IN_SESSION_MSG} if json_req(request) \
else RedirectResponse(url=router.url_path_for('home_page'), status_code=status.HTTP_303_SEE_OTHER)
...
return {"access_token": user_username, "token_type": "bearer"} \
if json_req(request) \
else RedirectResponse(url=router.url_path_for('home_page'), status_code=status.HTTP_303_SEE_OTHER)
⓹ templates/auth/login.html
:
We added the hidden field as previously described.
⓺ tests/integration/test_auth_itgt.py
</a>: We
added the hidden field to the login data in the following methods:
-
test_integration_valid_login_html
-
test_integration_valid_login_twice
-
test_integration_login_bad_email_html
-
test_integration_invalid_username_login_html
-
test_integration_invalid_password_login_html
tests/integration/test_admin_itgt.py
</a>:
We updated the constant name to
RESPONSE_FORMAT
. No other changes.
❺ The screenshots below show that the bug has been fixed:
❻ Thank you for reading. Stay safe, as always.
✿✿✿
Feature image source:
- https://www.omgubuntu.co.uk/2022/09/ubuntu-2210-kinetic-kudu-default-wallpaper
- https://in.pinterest.com/pin/337277459600111737/
- https://www.python.org/downloads/release/python-3124/
- https://fastapi.tiangolo.com/
- https://1000logos.net/download-image/
- https://www.logo.wine/logo/MySQL
- https://icon-icons.com/download/170836/PNG/512/
- https://www.stickpng.com/img/icons-logos-emojis/tech-companies/mariadb-full-logo
- https://www.flaticon.com/free-icon/bug-fixing_15511