Skip to content

Commit

Permalink
adds mfa recipe
Browse files Browse the repository at this point in the history
  • Loading branch information
rishabhpoddar committed Aug 16, 2024
1 parent c746310 commit 57ce38b
Show file tree
Hide file tree
Showing 18 changed files with 686 additions and 229 deletions.
7 changes: 1 addition & 6 deletions supertokens_python/recipe/dashboard/api/list_tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,7 @@ async def handle_list_tenants_api(
final_tenants: List[DashboardListTenantItem] = []

for current_tenant in tenants.tenants:
dashboard_tenant = DashboardListTenantItem(
tenant_id=current_tenant.tenant_id,
emailpassword=current_tenant.emailpassword,
passwordless=current_tenant.passwordless,
third_party=current_tenant.third_party,
)
dashboard_tenant = DashboardListTenantItem(current_tenant)
final_tenants.append(dashboard_tenant)

return DashboardListTenantsGetResponse(final_tenants)
34 changes: 9 additions & 25 deletions supertokens_python/recipe/dashboard/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
from supertokens_python.recipe.multitenancy.interfaces import TenantConfig

from supertokens_python.types import AccountLinkingUser

Expand All @@ -27,12 +28,6 @@
from supertokens_python.recipe.session.interfaces import SessionInformationResult
from supertokens_python.framework import BaseRequest, BaseResponse

from supertokens_python.recipe.multitenancy.interfaces import (
EmailPasswordConfig,
PasswordlessConfig,
ThirdPartyConfig,
)


class SessionInfo:
def __init__(self, info: SessionInformationResult) -> None:
Expand Down Expand Up @@ -109,28 +104,17 @@ def to_json(self) -> Dict[str, Any]:


class DashboardListTenantItem:
def __init__(
self,
tenant_id: str,
emailpassword: EmailPasswordConfig,
passwordless: PasswordlessConfig,
third_party: ThirdPartyConfig,
):
self.tenant_id = tenant_id
self.emailpassword = emailpassword
self.passwordless = passwordless
self.third_party = third_party
def __init__(self, tenant_config: TenantConfig):
self.tenant_config = tenant_config

def to_json(self):
res = {
"tenantId": self.tenant_id,
"emailPassword": self.emailpassword.to_json(),
"passwordless": self.passwordless.to_json(),
"thirdParty": self.third_party.to_json(),
def to_json(self) -> Dict[str, Any]:
return {
"tenantId": self.tenant_config.tenant_id,
"emailPassword": {"enabled": self.tenant_config.email_password_enabled},
"passwordless": {"enabled": self.tenant_config.passwordless_enabled},
"thirdParty": {"enabled": self.tenant_config.third_party_enabled},
}

return res


class DashboardListTenantsGetResponse(APIResponse):
status: str = "OK"
Expand Down
15 changes: 15 additions & 0 deletions supertokens_python/recipe/multifactorauth/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
#
# This software is licensed under the Apache License, Version 2.0 (the
# "License") as published by the Apache Software Foundation.
#
# You may not use this file except in compliance with the License. You may
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from .resync_session_and_fetch_mfa_info import handle_resync_session_and_fetch_mfa_info_api # type: ignore
153 changes: 153 additions & 0 deletions supertokens_python/recipe/multifactorauth/api/implementation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
#
# This software is licensed under the Apache License, Version 2.0 (the
# "License") as published by the Apache Software Foundation.
#
# You may not use this file except in compliance with the License. You may
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, List, Union

from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.multifactorauth.utils import (
update_and_get_mfa_related_info_in_session,
)
from supertokens_python.recipe.multitenancy.asyncio import get_tenant
from ..multi_factor_auth_claim import MultiFactorAuthClaim
from supertokens_python.asyncio import get_user
from supertokens_python.recipe.session.exceptions import (
InvalidClaimsError,
SuperTokensSessionError,
UnauthorisedError,
)

if TYPE_CHECKING:
from supertokens_python.recipe.multifactorauth.interfaces import (
APIInterface,
APIOptions,
)

from supertokens_python.types import GeneralErrorResponse
from ..interfaces import (
APIInterface,
APIOptions,
NextFactors,
ResyncSessionAndFetchMFAInfoPUTOkResult,
)


class APIImplementation(APIInterface):
async def resync_session_and_fetch_mfa_info_put(
self,
api_options: APIOptions,
session: SessionContainer,
user_context: Dict[str, Any],
) -> Union[ResyncSessionAndFetchMFAInfoPUTOkResult, GeneralErrorResponse]:
session_user = await get_user(session.get_user_id(), user_context)

if session_user is None:
raise UnauthorisedError(
"Session user not found",
)

mfa_info = await update_and_get_mfa_related_info_in_session(
input_session=session,
user_context=user_context,
)
factors_setup_for_user = (
await api_options.recipe_implementation.get_factors_setup_for_user(
user=session_user,
user_context=user_context,
)
)
tenant_info = await get_tenant(
session.get_tenant_id(user_context), user_context
)
if tenant_info is None:
raise UnauthorisedError(
"Tenant not found",
)
all_available_secondary_factors = (
await api_options.recipe_instance.get_all_available_secondary_factor_ids(
tenant_info
)
)

factors_allowed_to_setup: List[str] = []

async def get_factors_set_up_for_user():
return factors_setup_for_user

async def get_mfa_requirements_for_auth():
return mfa_info.mfa_requirements_for_auth

for factor_id in all_available_secondary_factors:
try:
await api_options.recipe_implementation.assert_allowed_to_setup_factor_else_throw_invalid_claim_error(
session=session,
factor_id=factor_id,
factors_set_up_for_user=get_factors_set_up_for_user,
mfa_requirements_for_auth=get_mfa_requirements_for_auth,
user_context=user_context,
)
factors_allowed_to_setup.append(factor_id)
except SuperTokensSessionError as err:
if not isinstance(err, InvalidClaimsError):
raise err

next_set_of_unsatisfied_factors = (
MultiFactorAuthClaim.get_next_set_of_unsatisfied_factors(
mfa_info.completed_factors, mfa_info.mfa_requirements_for_auth
)
)

get_emails_for_factors_result = (
await api_options.recipe_instance.get_emails_for_factors(
session_user, session.get_recipe_user_id(user_context)
)
)
get_phone_numbers_for_factors_result = (
await api_options.recipe_instance.get_phone_numbers_for_factors(
session_user, session.get_recipe_user_id(user_context)
)
)
if (
get_emails_for_factors_result.status == "UNKNOWN_SESSION_RECIPE_USER_ID"
or get_phone_numbers_for_factors_result.status
== "UNKNOWN_SESSION_RECIPE_USER_ID"
):
raise UnauthorisedError(
"User no longer associated with the session",
)

next_factors = [
factor_id
for factor_id in next_set_of_unsatisfied_factors.factor_ids
if factor_id in factors_allowed_to_setup
or factor_id in factors_setup_for_user
]

if (
len(next_factors) == 0
and len(next_set_of_unsatisfied_factors.factor_ids) != 0
):
raise Exception(
f"The user is required to complete secondary factors they are not allowed to "
f"({', '.join(next_set_of_unsatisfied_factors.factor_ids)}), likely because of configuration issues."
)
return ResyncSessionAndFetchMFAInfoPUTOkResult(
factors=NextFactors(
next=next_factors,
already_setup=factors_setup_for_user,
allowed_to_setup=factors_allowed_to_setup,
),
emails=get_emails_for_factors_result.factor_id_to_emails_map,
phone_numbers=get_phone_numbers_for_factors_result.factor_id_to_phone_number_map,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
#
# This software is licensed under the Apache License, Version 2.0 (the
# "License") as published by the Apache Software Foundation.
#
# You may not use this file except in compliance with the License. You may
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict

if TYPE_CHECKING:
from ..interfaces import (
APIOptions,
APIInterface,
)

from supertokens_python.utils import send_200_response

from supertokens_python.recipe.session.asyncio import get_session


async def handle_resync_session_and_fetch_mfa_info_api(
tenant_id: str,
api_implementation: APIInterface,
api_options: APIOptions,
user_context: Dict[str, Any],
):
if api_implementation.disable_resync_session_and_fetch_mfa_info_put is True:
return None

session = await get_session(
api_options.request,
override_global_claim_validators=lambda _, __, ___: [],
user_context=user_context,
)

assert session is not None

response = await api_implementation.resync_session_and_fetch_mfa_info_put(
api_options,
session,
user_context,
)

return send_200_response(response.to_json(), api_options.response)
Loading

0 comments on commit 57ce38b

Please sign in to comment.