Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨♻️ improving 2FA (OPS ⚠️) #5668

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
411d09a
improving 2FA
matusdrobuliak66 Apr 11, 2024
d6eea9d
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 11, 2024
3ec38f2
adding LOGIN_2FA_REQUIRED to static json
matusdrobuliak66 Apr 12, 2024
7b2d6b8
fix generatioon of static-frontend file
matusdrobuliak66 Apr 12, 2024
02fafae
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 12, 2024
eaefde7
improve error handling
matusdrobuliak66 Apr 12, 2024
45fe755
improve error handling + fix tests
matusdrobuliak66 Apr 14, 2024
48f9466
improve error type
matusdrobuliak66 Apr 14, 2024
ce807b4
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 14, 2024
19e0bb1
fix pylint
matusdrobuliak66 Apr 14, 2024
4ba2553
Merge branch 'is1315/2fa-improvements' of github.com:matusdrobuliak66…
matusdrobuliak66 Apr 14, 2024
dd9fa19
extanding test
matusdrobuliak66 Apr 14, 2024
406d64b
fix
matusdrobuliak66 Apr 15, 2024
ed43c03
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 15, 2024
45db22d
patchPreferenceField
odeimaiz Apr 15, 2024
655c34d
LOGIN_2FA_REQUIRED in statics
odeimaiz Apr 15, 2024
c857b3a
expose twoFAPreference
odeimaiz Apr 15, 2024
8bb1461
lowercase
odeimaiz Apr 15, 2024
c32f29f
uppercase
odeimaiz Apr 15, 2024
35f4304
Enums are uppercase
matusdrobuliak66 Apr 15, 2024
b91f7d7
Enums are uppercase
matusdrobuliak66 Apr 15, 2024
c869cd5
discourage messages
odeimaiz Apr 15, 2024
b8eafe6
back to last value
odeimaiz Apr 15, 2024
416bb35
minor
odeimaiz Apr 15, 2024
97ebce5
extractRetryAfter
odeimaiz Apr 15, 2024
8b73528
unused
odeimaiz Apr 15, 2024
d400645
cosmetics
odeimaiz Apr 15, 2024
8537c7d
rename
odeimaiz Apr 15, 2024
32bba46
UX
odeimaiz Apr 15, 2024
633e72e
review @GitHK
matusdrobuliak66 Apr 15, 2024
ca9a134
review @prespov
matusdrobuliak66 Apr 15, 2024
27b0c32
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 15, 2024
90504f3
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 15, 2024
1672be4
fix
matusdrobuliak66 Apr 15, 2024
6b4ae31
Merge branch 'is1315/2fa-improvements' of github.com:matusdrobuliak66…
matusdrobuliak66 Apr 15, 2024
87b5ab1
Merge branch 'master' into odeimaiz-is1315/2fa-improvements
odeimaiz Apr 15, 2024
7fe6c2a
Merge branch 'master' into is1315/2fa-improvements
odeimaiz Apr 15, 2024
f1c4f78
Merge branch 'is1315/2fa-improvements' of github.com:matusdrobuliak66…
odeimaiz Apr 15, 2024
d89b698
ux
odeimaiz Apr 15, 2024
a9e63d0
minor
odeimaiz Apr 15, 2024
5c1b732
name instead of code
odeimaiz Apr 15, 2024
2bc01b4
extractMessage
odeimaiz Apr 15, 2024
f07e33a
refactoring
odeimaiz Apr 16, 2024
fa26ce4
minor
odeimaiz Apr 16, 2024
e6b67ef
minor
odeimaiz Apr 16, 2024
9566393
message from backend
odeimaiz Apr 16, 2024
cba3ad8
smsEnabled
odeimaiz Apr 16, 2024
7300d89
no retry
odeimaiz Apr 16, 2024
6bd8705
FetchButton
odeimaiz Apr 16, 2024
333db8f
refactoring
odeimaiz Apr 16, 2024
314e750
retryAfter
odeimaiz Apr 16, 2024
e771a2b
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 16, 2024
8f420e3
minors
odeimaiz Apr 16, 2024
0a77636
pass userEmail
odeimaiz Apr 16, 2024
384302d
fix sequence
odeimaiz Apr 16, 2024
43e3562
minor
odeimaiz Apr 16, 2024
4df9bb5
fix
odeimaiz Apr 16, 2024
e250e96
last updates
odeimaiz Apr 16, 2024
d911fc1
Merge pull request #2 from odeimaiz/odeimaiz-is1315/2fa-improvements
matusdrobuliak66 Apr 16, 2024
f3e437e
Update services/static-webserver/client/source/class/osparc/auth/Mana…
odeimaiz Apr 17, 2024
6dd6ab8
Merge branch 'master' into is1315/2fa-improvements
matusdrobuliak66 Apr 17, 2024
60b1fee
Merge branch 'is1315/2fa-improvements' of github.com:matusdrobuliak66…
matusdrobuliak66 Apr 17, 2024
e185bd6
review @sanderegg
matusdrobuliak66 Apr 17, 2024
a0a6acd
fix tests + remove deprecated fields
matusdrobuliak66 Apr 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/specs/web-server/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
from models_library.generics import Envelope
from pydantic import BaseModel, Field, confloat
from simcore_service_webserver._meta import API_VTAG
from simcore_service_webserver.login._2fa_handlers import Resend2faBody
from simcore_service_webserver.login._auth_handlers import (
LoginBody,
LoginNextPage,
LoginTwoFactorAuthBody,
LogoutBody,
)
from simcore_service_webserver.login.handlers_2fa import Resend2faBody
from simcore_service_webserver.login.handlers_change import (
ChangeEmailBody,
ChangePasswordBody,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import auto

from .utils.enums import StrAutoEnum


class TwoFactorAuthentificationMethod(StrAutoEnum):
SMS = auto()
EMAIL = auto()
DISABLED = auto()
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,10 @@ def to_client_statics(self) -> dict[str, Any]:
"SIMCORE_VCS_RELEASE_URL": True,
"SWARM_STACK_NAME": True,
"WEBSERVER_PROJECTS": {"PROJECTS_MAX_NUM_RUNNING_DYNAMIC_NODES"},
"WEBSERVER_LOGIN": {"LOGIN_ACCOUNT_DELETION_RETENTION_DAYS"},
"WEBSERVER_LOGIN": {
"LOGIN_ACCOUNT_DELETION_RETENTION_DAYS",
"LOGIN_2FA_REQUIRED",
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
},
"WEBSERVER_SESSION": {"SESSION_COOKIE_MAX_AGE"},
},
exclude_none=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
import logging

from aiohttp import web
from models_library.users import UserID
from pydantic import BaseModel, Field
from servicelib.logging_utils import log_decorator
from servicelib.error_codes import create_error_code
from servicelib.logging_utils import LogExtra, get_log_record_extra, log_decorator
from servicelib.utils_secrets import generate_passcode
from settings_library.twilio import TwilioSettings
from twilio.base.exceptions import TwilioException
from twilio.rest import Client

from ..login.errors import SendingVerificationEmailError, SendingVerificationSmsError
from ..products.api import Product
from ..redis import get_redis_validation_code_client
from .utils_email import get_template_path, send_email_from_template
Expand Down Expand Up @@ -95,33 +99,46 @@ async def send_sms_code(
twilio_messaging_sid: str,
twilio_alpha_numeric_sender: str,
first_name: str,
user_id: UserID | None = None,
):
create_kwargs = {
"messaging_service_sid": twilio_messaging_sid,
"to": phone_number,
"body": f"Dear {first_name}, your verification code is {code}",
}
if twilo_auth.is_alphanumeric_supported(phone_number):
create_kwargs["from_"] = twilio_alpha_numeric_sender

def _sender():
log.info(
"Sending sms code to %s from product %s",
f"{phone_number=}",
twilio_alpha_numeric_sender,
try:
create_kwargs = {
"messaging_service_sid": twilio_messaging_sid,
"to": phone_number,
"body": f"Dear {first_name}, your verification code is {code}",
}
if twilo_auth.is_alphanumeric_supported(phone_number):
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
create_kwargs["from_"] = twilio_alpha_numeric_sender

def _sender():
log.info(
"Sending sms code to %s from product %s",
f"{phone_number=}",
twilio_alpha_numeric_sender,
)
#
# SEE https://www.twilio.com/docs/sms/quickstart/python
#
client = Client(twilo_auth.TWILIO_ACCOUNT_SID, twilo_auth.TWILIO_AUTH_TOKEN)
message = client.messages.create(**create_kwargs)

log.debug(
"Got twilio client %s",
f"{message=}",
)

await asyncio.get_event_loop().run_in_executor(executor=None, func=_sender)

except TwilioException as exc:
error_code = create_error_code(exc)
more_extra: LogExtra = get_log_record_extra(user_id=user_id) or {}
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
log.exception(
"Failed while setting up 2FA code and sending SMS to %s [%s]",
mask_phone_number(phone_number),
f"{error_code}",
extra={"error_code": error_code, **more_extra},
)
#
# SEE https://www.twilio.com/docs/sms/quickstart/python
#
client = Client(twilo_auth.TWILIO_ACCOUNT_SID, twilo_auth.TWILIO_AUTH_TOKEN)
message = client.messages.create(**create_kwargs)

log.debug(
"Got twilio client %s",
f"{message=}",
)

await asyncio.get_event_loop().run_in_executor(executor=None, func=_sender)
raise SendingVerificationSmsError(reason=exc) from exc


#
Expand All @@ -141,21 +158,33 @@ async def send_email_code(
code: str,
first_name: str,
product: Product,
user_id: UserID | None = None,
):
email_template_path = await get_template_path(request, "new_2fa_code.jinja2")
await send_email_from_template(
request,
from_=support_email,
to=user_email,
template=email_template_path,
context={
"host": request.host,
"code": code,
"name": first_name,
"support_email": support_email,
"product": product,
},
)
try:
email_template_path = await get_template_path(request, "new_2fa_code.jinja2")
await send_email_from_template(
request,
from_=support_email,
to=user_email,
template=email_template_path,
context={
"host": request.host,
"code": code,
"name": first_name,
"support_email": support_email,
"product": product,
},
)
except TwilioException as exc:
error_code = create_error_code(exc)
more_extra: LogExtra = get_log_record_extra(user_id=user_id) or {}
log.exception(
"Failed while setting up 2FA code and sending Email to %s [%s]",
user_email,
f"{error_code}",
extra={"error_code": error_code, **more_extra},
)
raise SendingVerificationEmailError(reason=exc) from exc


#
Expand Down
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import logging
from typing import Literal

from aiohttp import web
from aiohttp.web import RouteTableDef
from models_library.emails import LowerCaseEmailStr
from pydantic import Field
from servicelib.aiohttp import status
from servicelib.aiohttp.requests_validation import parse_request_body_as
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON

from ..products.api import Product, get_current_product
from ..session.access_policies import session_access_required
from ._2fa_api import (
create_2fa_code,
delete_2fa_code,
get_2fa_code,
mask_phone_number,
send_email_code,
send_sms_code,
)
from ._constants import (
CODE_2FA_EMAIL_CODE_REQUIRED,
CODE_2FA_SMS_CODE_REQUIRED,
MSG_2FA_CODE_SENT,
MSG_EMAIL_SENT,
MSG_UNKNOWN_EMAIL,
)
from ._models import InputSchema
from .errors import handle_login_exceptions
from .settings import LoginSettingsForProduct, get_plugin_settings
from .storage import AsyncpgStorage, get_plugin_storage
from .utils import envelope_response

_logger = logging.getLogger(__name__)


routes = RouteTableDef()


class Resend2faBody(InputSchema):
email: LowerCaseEmailStr = Field(..., description="User email (identifier)")
via: Literal["SMS", "Email"] = "SMS"


@routes.post("/v0/auth/two_factor:resend", name="auth_resend_2fa_code")
@session_access_required(
name="auth_resend_2fa_code",
one_time_access=False,
)
@handle_login_exceptions
async def resend_2fa_code(request: web.Request):
"""Resends 2FA code via SMS/Email"""
product: Product = get_current_product(request)
settings: LoginSettingsForProduct = get_plugin_settings(
request.app, product_name=product.name
)
db: AsyncpgStorage = get_plugin_storage(request.app)
resend_2fa_ = await parse_request_body_as(Resend2faBody, request)

user = await db.get_user({"email": resend_2fa_.email})
if not user:
raise web.HTTPUnauthorized(
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
reason=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON
)

if not settings.LOGIN_2FA_REQUIRED:
raise web.HTTPServiceUnavailable(
reason="2FA login is not available",
content_type=MIMETYPE_APPLICATION_JSON,
)

# Already a code?
previous_code = await get_2fa_code(request.app, user_email=resend_2fa_.email)
if previous_code is not None:
await delete_2fa_code(request.app, user_email=resend_2fa_.email)

# guaranteed by LoginSettingsForProduct
assert settings.LOGIN_2FA_REQUIRED # nosec
assert settings.LOGIN_TWILIO # nosec
assert product.twilio_messaging_sid # nosec

# creates and stores code
code = await create_2fa_code(
request.app,
user_email=user["email"],
expiration_in_seconds=settings.LOGIN_2FA_CODE_EXPIRATION_SEC,
)

# sends via SMS
if resend_2fa_.via == "SMS":
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
await send_sms_code(
phone_number=user["phone"],
code=code,
twilo_auth=settings.LOGIN_TWILIO,
twilio_messaging_sid=product.twilio_messaging_sid,
twilio_alpha_numeric_sender=product.twilio_alpha_numeric_sender_id,
first_name=user["first_name"] or user["name"],
user_id=user["id"],
)

response = envelope_response(
{
"name": CODE_2FA_SMS_CODE_REQUIRED,
"parameters": {
"message": MSG_2FA_CODE_SENT.format(
phone_number=mask_phone_number(user["phone"])
),
"retry_2fa_after": settings.LOGIN_2FA_CODE_EXPIRATION_SEC,
},
# NOTE: REMOVE when frontend is refactored
"reason": MSG_2FA_CODE_SENT.format(
phone_number=mask_phone_number(user["phone"])
),
},
status=status.HTTP_200_OK,
)

# sends via Email
else:
assert resend_2fa_.via == "Email" # nosec
await send_email_code(
request,
user_email=user["email"],
support_email=product.support_email,
code=code,
first_name=user["first_name"] or user["name"],
product=product,
user_id=user["id"],
)

response = envelope_response(
{
"name": CODE_2FA_EMAIL_CODE_REQUIRED,
"parameters": {
"message": MSG_EMAIL_SENT.format(email=user["email"]),
"retry_2fa_after": settings.LOGIN_2FA_CODE_EXPIRATION_SEC,
},
# NOTE: REMOVE when frontend is refactored
"reason": MSG_EMAIL_SENT.format(email=user["email"]),
},
status=status.HTTP_200_OK,
)

return response
Loading
Loading