Skip to content

Commit

Permalink
[Auth] add auth key authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDSM committed Aug 25, 2024
1 parent c02ab0d commit 85b4ad5
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 14 deletions.
4 changes: 4 additions & 0 deletions additional_tests/supabase_backend_tests/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ SUPABASE_BACKEND_KEY=

SUPABASE_BACKEND_CLIENT_1_EMAIL=
SUPABASE_BACKEND_CLIENT_1_PASSWORD=
SUPABASE_BACKEND_CLIENT_1_AUTH_KEY=

SUPABASE_BACKEND_CLIENT_2_EMAIL=
SUPABASE_BACKEND_CLIENT_2_PASSWORD=

SUPABASE_BACKEND_CLIENT_3_EMAIL=
SUPABASE_BACKEND_CLIENT_3_PASSWORD=

SUPABASE_BACKEND_SERVICE_KEY=
4 changes: 4 additions & 0 deletions additional_tests/supabase_backend_tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ def get_backend_client_creds(identifier):
os.getenv(f"SUPABASE_BACKEND_CLIENT_{identifier}_PASSWORD")


def get_backend_client_auth_key(identifier):
return os.getenv(f"SUPABASE_BACKEND_CLIENT_{identifier}_AUTH_KEY")


def _get_backend_service_key():
return os.getenv(f"SUPABASE_BACKEND_SERVICE_KEY")

Expand Down
50 changes: 49 additions & 1 deletion additional_tests/supabase_backend_tests/test_user_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import octobot.community as community
import octobot.community.supabase_backend.enums as supabase_backend_enums
from additional_tests.supabase_backend_tests import authenticated_client_1, authenticated_client_2, \
admin_client, anon_client, get_backend_api_creds, skip_if_no_service_key
admin_client, anon_client, get_backend_api_creds, skip_if_no_service_key, get_backend_client_creds, \
get_backend_client_auth_key


# All test coroutines will be treated as marked.
Expand Down Expand Up @@ -114,3 +115,50 @@ async def test_sign_in_with_otp_token(authenticated_client_1, skip_if_no_service
finally:
if supabase_client:
await supabase_client.aclose()


async def test_sign_in_with_auth_token():
# create new client
backend_url, backend_key = get_backend_api_creds()
email, _ = get_backend_client_creds(1)

config = commons_configuration.Configuration("", "")
config.config = {}
supabase_client = None
try:
supabase_client = community.CommunitySupabaseClient(
backend_url,
backend_key,
community.ASyncConfigurationStorage(config)
)
saved_session = "saved_session"
await supabase_client.auth._storage.set_item(supabase_client.auth._storage_key, saved_session)
# wrong configs
with pytest.raises(authentication.AuthenticationError):
await supabase_client.get_otp_with_auth_key("", "")
with pytest.raises(authentication.AuthenticationError):
await supabase_client.get_otp_with_auth_key(None, "")
with pytest.raises(authentication.AuthenticationError):
await supabase_client.get_otp_with_auth_key(email, None)
with pytest.raises(authentication.AuthenticationError):
await supabase_client.get_otp_with_auth_key(email, "1234")
assert await supabase_client.auth._storage.get_item(supabase_client.auth._storage_key) == saved_session
token = await supabase_client.get_otp_with_auth_key(email, get_backend_client_auth_key(1))
# ensure token is valid

await supabase_client.sign_in_with_otp_token(token)
# save session has been updated
updated_session = await supabase_client.auth._storage.get_item(supabase_client.auth._storage_key)
assert updated_session != saved_session

# ensure new supabase_client is bound to the same user as the previous client
user = await supabase_client.get_user()
assert user[supabase_backend_enums.UserKeys.EMAIL.value] == email

# already consumed token
with pytest.raises(authentication.AuthenticationError):
await supabase_client.sign_in_with_otp_token(token)
assert await supabase_client.auth._storage.get_item(supabase_client.auth._storage_key) == updated_session
finally:
if supabase_client:
await supabase_client.aclose()
25 changes: 17 additions & 8 deletions octobot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
#
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
import time
import argparse
import os
import sys
Expand Down Expand Up @@ -189,13 +188,23 @@ async def _get_authenticated_community_if_possible(config, logger):
community_auth = octobot_community.CommunityAuthentication.create(config)
try:
if not community_auth.is_initialized():
if constants.IS_CLOUD_ENV and constants.USER_ACCOUNT_EMAIL and constants.USER_PASSWORD_TOKEN:
try:
await community_auth.login(
constants.USER_ACCOUNT_EMAIL, None, password_token=constants.USER_PASSWORD_TOKEN
)
except authentication.AuthenticationError as err:
logger.debug(f"Password token auth failure ({err}). Trying with saved session.")
if constants.IS_CLOUD_ENV:
if constants.USER_ACCOUNT_EMAIL and constants.USER_AUTH_KEY:
try:
logger.debug("Attempting auth key authentication")
await community_auth.login(
constants.USER_ACCOUNT_EMAIL, None, auth_key=constants.USER_AUTH_KEY
)
except authentication.AuthenticationError as err:
logger.debug(f"Auth key auth failure ({err}). Trying other methods if available.")
if constants.USER_ACCOUNT_EMAIL and constants.USER_PASSWORD_TOKEN:
try:
logger.debug("Attempting password token authentication")
await community_auth.login(
constants.USER_ACCOUNT_EMAIL, None, password_token=constants.USER_PASSWORD_TOKEN
)
except authentication.AuthenticationError as err:
logger.debug(f"Password token auth failure ({err}). Trying with saved session.")
if not community_auth.is_initialized():
# try with saved credentials if any
has_tentacles = tentacles_manager_api.is_tentacles_architecture_valid()
Expand Down
18 changes: 15 additions & 3 deletions octobot/community/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def __init__(self, config=None, backend_url=None, backend_key=None, use_as_singl
self.user_account = community_user_account.CommunityUserAccount()
self.public_data = community_public_data.CommunityPublicData()
self.successfully_fetched_tentacles_package_urls = False
self.silent_auth = False
self._community_feed = None

self.initialized_event = None
Expand Down Expand Up @@ -305,11 +306,20 @@ def can_authenticate(self):
def must_be_authenticated_through_authenticator(self):
return constants.IS_CLOUD_ENV

async def login(self, email, password, password_token=None, minimal=False):
async def login(
self,
email: str,
password: typing.Optional[str],
password_token: typing.Optional[str] = None,
auth_key: typing.Optional[str] = None,
minimal: bool = False
):
self._ensure_email(email)
self._ensure_community_url()
self._reset_tokens()
with self._login_process():
if auth_key and not password_token:
password_token = await self.supabase_client.get_otp_with_auth_key(email, auth_key)
if password_token:
await self.supabase_client.sign_in_with_otp_token(password_token)
else:
Expand All @@ -331,7 +341,8 @@ async def register(self, email, password):
await self.on_signed_in()

async def on_signed_in(self, minimal=False):
self.logger.info(f"Signed in as {self.get_logged_in_email()}")
if not self.silent_auth:
self.logger.info(f"Signed in as {self.get_logged_in_email()}")
await self._initialize_account(minimal=minimal)

async def _update_account_metadata(self, metadata_update):
Expand Down Expand Up @@ -669,7 +680,8 @@ async def _restore_previous_session(self):
# will raise on failure
await self.supabase_client.restore_session()
await self._on_account_updated()
self.logger.info(f"Signed in as {self.get_logged_in_email()}")
if not self.silent_auth:
self.logger.info(f"Signed in as {self.get_logged_in_email()}")
return self.is_logged_in()

@contextlib.asynccontextmanager
Expand Down
19 changes: 17 additions & 2 deletions octobot/community/supabase_backend/community_supabase_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import httpx
import uuid
import json

import aiohttp
import gotrue.errors
Expand Down Expand Up @@ -211,8 +212,22 @@ async def get_user(self) -> dict:
raise errors.EmailValidationRequiredError(err) from err
raise authentication.AuthenticationError(f"Please re-login to your OctoBot account: {err}") from err

def sync_get_user(self) -> dict:
return self.auth.get_user().user.model_dump()
async def get_otp_with_auth_key(self, user_email: str, auth_key: str) -> str:
try:
resp = await self.functions.invoke(
"create-auth-token",
{
"headers": {
"User-Auth-Token": auth_key
},
"body": {
"user_email": user_email
},
}
)
return json.loads(resp)["token"]
except Exception:
raise authentication.AuthenticationError(f"Invalid auth key authentication details")

async def fetch_bot(self, bot_id) -> dict:
try:
Expand Down
1 change: 1 addition & 0 deletions octobot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
USE_BETA_EARLY_ACCESS = os_util.parse_boolean_environment_var("USE_BETA_EARLY_ACCESS", "false")
USER_ACCOUNT_EMAIL = os.getenv("USER_ACCOUNT_EMAIL", "")
USER_PASSWORD_TOKEN = os.getenv("USER_PASSWORD_TOKEN", None)
USER_AUTH_KEY = os.getenv("USER_AUTH_KEY", None)
COMMUNITY_BOT_ID = os.getenv("COMMUNITY_BOT_ID", "")
IS_DEMO = os_util.parse_boolean_environment_var("IS_DEMO", "False")
IS_CLOUD_ENV = os_util.parse_boolean_environment_var("IS_CLOUD_ENV", "false")
Expand Down

0 comments on commit 85b4ad5

Please sign in to comment.