From 8d270c118275712453ebdffb0a04152245feef23 Mon Sep 17 00:00:00 2001 From: le_woudar Date: Thu, 15 Jul 2021 21:51:05 +0200 Subject: [PATCH 1/3] fix basic auth security example and docstring --- docs/en/docs/advanced/security/http-basic-auth.md | 3 +++ docs_src/security/tutorial007.py | 8 ++++++-- fastapi/security/oauth2.py | 2 -- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 6c589cd9afe32..da8d9da5c28ca 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -50,6 +50,9 @@ if not (credentials.username == "stanleyjobson") or not (credentials.password == But by using the `secrets.compare_digest()` it will be secure against a type of attacks called "timing attacks". +!!! tip + We are comparing `bytes` (`encode("idna")`) instead of `str` with `compare_digest` because it cannot handle strings with non-ascii characters. + ### Timing Attacks But what's a "timing attack"? diff --git a/docs_src/security/tutorial007.py b/docs_src/security/tutorial007.py index 90b9ac0546ff5..533aa8da089e8 100644 --- a/docs_src/security/tutorial007.py +++ b/docs_src/security/tutorial007.py @@ -9,8 +9,12 @@ def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): - correct_username = secrets.compare_digest(credentials.username, "stanleyjobson") - correct_password = secrets.compare_digest(credentials.password, "swordfish") + correct_username = secrets.compare_digest( + credentials.username.encode("idna"), "stanleyjobson".encode("idna") + ) + correct_password = secrets.compare_digest( + credentials.password.encode("idna"), "swordfish".encode("idna") + ) if not (correct_username and correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 46571ad53762c..c6c23edc86b67 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -16,7 +16,6 @@ class OAuth2PasswordRequestForm: @app.post("/login") def login(form_data: OAuth2PasswordRequestForm = Depends()): - data = form_data.parse() print(data.username) print(data.password) for scope in data.scopes: @@ -66,7 +65,6 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): @app.post("/login") def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): - data = form_data.parse() print(data.username) print(data.password) for scope in data.scopes: From 8f1340a4fae19b5ebc7b608cab3d79dd79e06b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 24 Aug 2022 16:47:30 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=E2=8F=AA=20Revert=20changes=20in=20docstri?= =?UTF-8?q?ng=20that=20should=20go=20in=20another=20PR=20and=20are=20also?= =?UTF-8?q?=20still=20broken=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/security/oauth2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index df477e7c8ab0c..888208c1501fc 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -16,6 +16,7 @@ class OAuth2PasswordRequestForm: @app.post("/login") def login(form_data: OAuth2PasswordRequestForm = Depends()): + data = form_data.parse() print(data.username) print(data.password) for scope in data.scopes: @@ -65,6 +66,7 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): @app.post("/login") def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): + data = form_data.parse() print(data.username) print(data.password) for scope in data.scopes: From fa8fa2603ceff4fd7723b76334f1c41780052537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 24 Aug 2022 16:48:33 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20example=20for?= =?UTF-8?q?=20basic=20auth=20to=20make=20it=20more=20explicit,=20fix=20lin?= =?UTF-8?q?e=20highlighting=20and=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/advanced/security/http-basic-auth.md | 17 ++++++++++------- docs_src/security/tutorial007.py | 14 +++++++++----- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index da8d9da5c28ca..90c516808fc41 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -34,13 +34,19 @@ Here's a more complete example. Use a dependency to check if the username and password are correct. -For this, use the Python standard module `secrets` to check the username and password: +For this, use the Python standard module `secrets` to check the username and password. -```Python hl_lines="1 11-13" +`secrets.compare_digest()` needs to take `bytes` or a `str` that only contains ASCII characters (the ones in English), this means it wouldn't work with characters like `á`, as in `Sebastián`. + +To handle that, we first convert the `username` and `password` to `bytes` encoding them with UTF-8. + +Then we can use `secrets.compare_digest()` to ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. + +```Python hl_lines="1 11-21" {!../../../docs_src/security/tutorial007.py!} ``` -This will ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. This would be similar to: +This would be similar to: ```Python if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): @@ -50,9 +56,6 @@ if not (credentials.username == "stanleyjobson") or not (credentials.password == But by using the `secrets.compare_digest()` it will be secure against a type of attacks called "timing attacks". -!!! tip - We are comparing `bytes` (`encode("idna")`) instead of `str` with `compare_digest` because it cannot handle strings with non-ascii characters. - ### Timing Attacks But what's a "timing attack"? @@ -105,6 +108,6 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: -```Python hl_lines="15-19" +```Python hl_lines="23-27" {!../../../docs_src/security/tutorial007.py!} ``` diff --git a/docs_src/security/tutorial007.py b/docs_src/security/tutorial007.py index 533aa8da089e8..790ee10bc6b1d 100644 --- a/docs_src/security/tutorial007.py +++ b/docs_src/security/tutorial007.py @@ -9,13 +9,17 @@ def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): - correct_username = secrets.compare_digest( - credentials.username.encode("idna"), "stanleyjobson".encode("idna") + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes ) - correct_password = secrets.compare_digest( - credentials.password.encode("idna"), "swordfish".encode("idna") + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes ) - if not (correct_username and correct_password): + if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password",