From bbd66794f797e51e7159ab91f9e5f0f5221a6170 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 29 Oct 2024 07:52:56 +0000 Subject: [PATCH 1/3] feat: do not send non-JWTs in Authorization header --- supabase/_async/client.py | 14 +++++++++----- supabase/_sync/client.py | 14 +++++++++----- supabase/lib/helpers.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 supabase/lib/helpers.py diff --git a/supabase/_async/client.py b/supabase/_async/client.py index ad21c7cd..fed0d1ce 100644 --- a/supabase/_async/client.py +++ b/supabase/_async/client.py @@ -16,6 +16,8 @@ from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import AsyncFunctionsClient +from supabase.lib.helpers import is_jwt + from ..lib.client_options import AsyncClientOptions as ClientOptions from .auth_client import AsyncSupabaseAuthClient @@ -278,9 +280,10 @@ def _create_auth_header(self, token: str): def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: if authorization is None: - authorization = self.options.headers.get( - "Authorization", self._create_auth_header(self.supabase_key) - ) + if is_jwt(self.supabase_key): + authorization = self.options.headers.get( + "Authorization", self._create_auth_header(self.supabase_key) + ) """Helper method to get auth headers.""" return { @@ -291,13 +294,14 @@ def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, st def _listen_to_auth_events( self, event: AuthChangeEvent, session: Optional[Session] ): - access_token = self.supabase_key + default_access_token = self.supabase_key if is_jwt(self.supabase_key) else None + access_token = default_access_token if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change self._postgrest = None self._storage = None self._functions = None - access_token = session.access_token if session else self.supabase_key + access_token = session.access_token if session else default_access_token self.options.headers["Authorization"] = self._create_auth_header(access_token) asyncio.create_task(self.realtime.set_auth(access_token)) diff --git a/supabase/_sync/client.py b/supabase/_sync/client.py index 680a7aea..4ba21393 100644 --- a/supabase/_sync/client.py +++ b/supabase/_sync/client.py @@ -15,6 +15,8 @@ from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import SyncFunctionsClient +from supabase.lib.helpers import is_jwt + from ..lib.client_options import SyncClientOptions as ClientOptions from .auth_client import SyncSupabaseAuthClient @@ -277,9 +279,10 @@ def _create_auth_header(self, token: str): def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: if authorization is None: - authorization = self.options.headers.get( - "Authorization", self._create_auth_header(self.supabase_key) - ) + if is_jwt(self.supabase_key): + authorization = self.options.headers.get( + "Authorization", self._create_auth_header(self.supabase_key) + ) """Helper method to get auth headers.""" return { @@ -290,13 +293,14 @@ def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, st def _listen_to_auth_events( self, event: AuthChangeEvent, session: Optional[Session] ): - access_token = self.supabase_key + default_access_token = self.supabase_key if is_jwt(self.supabase_key) else None + access_token = default_access_token if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change self._postgrest = None self._storage = None self._functions = None - access_token = session.access_token if session else self.supabase_key + access_token = session.access_token if session else default_access_token self.options.headers["Authorization"] = self._create_auth_header(access_token) diff --git a/supabase/lib/helpers.py b/supabase/lib/helpers.py new file mode 100644 index 00000000..2a56ef63 --- /dev/null +++ b/supabase/lib/helpers.py @@ -0,0 +1,28 @@ +import re + + +BASE64URL_REGEX = r"^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$" + + +def is_jwt(value: str) -> bool: + if value.startswith("Bearer "): + value = value.replace("Bearer ", "") + + value = value.strip() + if not value: + return False + + parts = value.split(".") + if len(parts) != 3: + return False + + # loop through the parts and test against regex + for part in parts: + if len(part) < 4 or not re.search(BASE64URL_REGEX, part, re.IGNORECASE): + return False + + return True + + +def check_authorization_header(headers): + return True From 3e0934fc67013df2752a0310994f327de3620956 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 29 Oct 2024 08:31:46 +0000 Subject: [PATCH 2/3] chore: code formatting fixed --- supabase/lib/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/supabase/lib/helpers.py b/supabase/lib/helpers.py index 2a56ef63..4fb573ed 100644 --- a/supabase/lib/helpers.py +++ b/supabase/lib/helpers.py @@ -1,6 +1,5 @@ import re - BASE64URL_REGEX = r"^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$" From 13145952a75a9e52f6cf51112a6c59bacb397eff Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Fri, 22 Nov 2024 16:49:30 +0000 Subject: [PATCH 3/3] update is_valid_jwt function --- supabase/_async/client.py | 8 +++++--- supabase/_sync/client.py | 8 +++++--- supabase/lib/helpers.py | 36 +++++++++++++++++++++++++----------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/supabase/_async/client.py b/supabase/_async/client.py index fed0d1ce..f21cd762 100644 --- a/supabase/_async/client.py +++ b/supabase/_async/client.py @@ -16,7 +16,7 @@ from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import AsyncFunctionsClient -from supabase.lib.helpers import is_jwt +from supabase.lib.helpers import is_valid_jwt from ..lib.client_options import AsyncClientOptions as ClientOptions from .auth_client import AsyncSupabaseAuthClient @@ -280,7 +280,7 @@ def _create_auth_header(self, token: str): def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: if authorization is None: - if is_jwt(self.supabase_key): + if is_valid_jwt(self.supabase_key): authorization = self.options.headers.get( "Authorization", self._create_auth_header(self.supabase_key) ) @@ -294,7 +294,9 @@ def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, st def _listen_to_auth_events( self, event: AuthChangeEvent, session: Optional[Session] ): - default_access_token = self.supabase_key if is_jwt(self.supabase_key) else None + default_access_token = ( + self.supabase_key if is_valid_jwt(self.supabase_key) else None + ) access_token = default_access_token if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change diff --git a/supabase/_sync/client.py b/supabase/_sync/client.py index 4ba21393..c1f9cdd5 100644 --- a/supabase/_sync/client.py +++ b/supabase/_sync/client.py @@ -15,7 +15,7 @@ from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import SyncFunctionsClient -from supabase.lib.helpers import is_jwt +from supabase.lib.helpers import is_valid_jwt from ..lib.client_options import SyncClientOptions as ClientOptions from .auth_client import SyncSupabaseAuthClient @@ -279,7 +279,7 @@ def _create_auth_header(self, token: str): def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: if authorization is None: - if is_jwt(self.supabase_key): + if is_valid_jwt(self.supabase_key): authorization = self.options.headers.get( "Authorization", self._create_auth_header(self.supabase_key) ) @@ -293,7 +293,9 @@ def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, st def _listen_to_auth_events( self, event: AuthChangeEvent, session: Optional[Session] ): - default_access_token = self.supabase_key if is_jwt(self.supabase_key) else None + default_access_token = ( + self.supabase_key if is_valid_jwt(self.supabase_key) else None + ) access_token = default_access_token if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change diff --git a/supabase/lib/helpers.py b/supabase/lib/helpers.py index 4fb573ed..0b8b308b 100644 --- a/supabase/lib/helpers.py +++ b/supabase/lib/helpers.py @@ -1,27 +1,41 @@ import re +from typing import Dict BASE64URL_REGEX = r"^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$" -def is_jwt(value: str) -> bool: - if value.startswith("Bearer "): - value = value.replace("Bearer ", "") +def is_valid_jwt(value: str) -> bool: + """Checks if value looks like a JWT, does not do any extra parsing.""" + if not isinstance(value, str): + return False + # Remove trailing whitespaces if any. value = value.strip() - if not value: - return False - parts = value.split(".") - if len(parts) != 3: + # Remove "Bearer " prefix if any. + if value.startswith("Bearer "): + value = value[7:] + + # Valid JWT must have 2 dots (Header.Paylod.Signature) + if value.count(".") != 2: return False - # loop through the parts and test against regex - for part in parts: - if len(part) < 4 or not re.search(BASE64URL_REGEX, part, re.IGNORECASE): + for part in value.split("."): + if not re.search(BASE64URL_REGEX, part, re.IGNORECASE): return False return True -def check_authorization_header(headers): +def check_authorization_header(headers: Dict[str, str]): + authorization = headers.get("Authorization") + if not authorization: + return + + if authorization.startswith("Bearer "): + if not is_valid_jwt(authorization): + raise ValueError( + "create_client called with global Authorization header that does not contain a JWT" + ) + return True