Skip to content

Commit

Permalink
OTP voice support (#376)
Browse files Browse the repository at this point in the history
  • Loading branch information
dorsha authored Apr 3, 2024
1 parent 9941ed9 commit c2b6c91
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 20 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ For rate limiting information, please confer to the [API Rate Limits](#api-rate-

### OTP Authentication

Send a user a one-time password (OTP) using your preferred delivery method (_email / SMS_). An email address or phone number must be provided accordingly.
Send a user a one-time password (OTP) using your preferred delivery method (_email / SMS / Voice call / WhatsApp_). An email address or phone number must be provided accordingly.

The user can either `sign up`, `sign in` or `sign up or in`

Expand All @@ -108,7 +108,7 @@ The session and refresh JWTs should be returned to the caller, and passed with e

### Magic Link

Send a user a Magic Link using your preferred delivery method (_email / SMS_).
Send a user a Magic Link using your preferred delivery method (_email / SMS / Voice call / WhatsApp_).
The Magic Link will redirect the user to page where the its token needs to be verified.
This redirection can be configured in code, or generally in the [Descope Console](https://app.descope.com/settings/authentication/magiclink)

Expand Down Expand Up @@ -1329,7 +1329,7 @@ apps = apps_resp["apps"]
### Utils for your end to end (e2e) tests and integration tests

To ease your e2e tests, we exposed dedicated management methods,
that way, you don't need to use 3rd party messaging services in order to receive sign-in/up Emails or SMS, and avoid the need of parsing the code and token from them.
that way, you don't need to use 3rd party messaging services in order to receive sign-in/up Email, SMS, Voice call, WhatsApp, and avoid the need of parsing the code and token from them.

```Python
# User for test can be created, this user will be able to generate code/link without
Expand Down
20 changes: 18 additions & 2 deletions descope/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ def adjust_and_verify_delivery_method(
user["phone"] = login_id
if not re.match(PHONE_REGEX, user["phone"]):
return False
elif method == DeliveryMethod.VOICE:
if not user.get("phone", None):
user["phone"] = login_id
if not re.match(PHONE_REGEX, user["phone"]):
return False
elif method == DeliveryMethod.WHATSAPP:
if not user.get("phone", None):
user["phone"] = login_id
Expand All @@ -230,6 +235,7 @@ def compose_url(base: str, method: DeliveryMethod) -> str:
suffix = {
DeliveryMethod.EMAIL: "email",
DeliveryMethod.SMS: "sms",
DeliveryMethod.VOICE: "voice",
DeliveryMethod.WHATSAPP: "whatsapp",
}.get(method)

Expand All @@ -245,6 +251,7 @@ def get_login_id_by_method(method: DeliveryMethod, user: dict) -> tuple[str, str
login_id = {
DeliveryMethod.EMAIL: ("email", user.get("email", "")),
DeliveryMethod.SMS: ("phone", user.get("phone", "")),
DeliveryMethod.VOICE: ("voice", user.get("phone", "")),
DeliveryMethod.WHATSAPP: ("whatsapp", user.get("phone", "")),
}.get(method)

Expand All @@ -260,6 +267,7 @@ def get_method_string(method: DeliveryMethod) -> str:
name = {
DeliveryMethod.EMAIL: "email",
DeliveryMethod.SMS: "sms",
DeliveryMethod.VOICE: "voice",
DeliveryMethod.WHATSAPP: "whatsapp",
DeliveryMethod.EMBEDDED: "Embedded",
}.get(method)
Expand Down Expand Up @@ -301,7 +309,11 @@ def validate_phone(method: DeliveryMethod, phone: str):
400, ERROR_TYPE_INVALID_ARGUMENT, "Invalid phone number"
)

if method != DeliveryMethod.SMS and method != DeliveryMethod.WHATSAPP:
if (
method != DeliveryMethod.SMS
and method != DeliveryMethod.VOICE
and method != DeliveryMethod.WHATSAPP
):
raise AuthException(
400, ERROR_TYPE_INVALID_ARGUMENT, "Invalid delivery method"
)
Expand Down Expand Up @@ -669,7 +681,11 @@ def select_tenant(self, tenant_id: str, refresh_token: str) -> dict:

@staticmethod
def extract_masked_address(response: dict, method: DeliveryMethod) -> str:
if method == DeliveryMethod.SMS or method == DeliveryMethod.WHATSAPP:
if (
method == DeliveryMethod.SMS
or method == DeliveryMethod.VOICE
or method == DeliveryMethod.WHATSAPP
):
return response["maskedPhone"]
elif method == DeliveryMethod.EMAIL:
return response["maskedEmail"]
Expand Down
12 changes: 6 additions & 6 deletions descope/authmethod/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def sign_in(
Args:
method (DeliveryMethod): The method to use for delivering the OTP verification code to the user, for example
email, SMS, or WhatsApp
Email, SMS, Voice call, or WhatsApp
login_id (str): The login ID of the user being validated for example phone or email
login_options (LoginOptions): Optional advanced controls over login parameters
refresh_token: Optional refresh token is needed for specific login options
Expand Down Expand Up @@ -58,7 +58,7 @@ def sign_up(
) -> str:
"""
Sign up (create) a new user using their email or phone number. Choose a delivery method for OTP
verification, for example email, SMS, or WhatsApp.
verification, for example Email, SMS, Voice call, or WhatsApp.
(optional) Include additional user metadata that you wish to preserve.
Args:
Expand Down Expand Up @@ -99,7 +99,7 @@ def sign_up_or_in(
using the OTP DeliveryMethod specified.
Args:
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example phone or email
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp
login_id (str): The Login ID of the user being validated
Raise:
Expand Down Expand Up @@ -130,7 +130,7 @@ def verify_code(self, method: DeliveryMethod, login_id: str, code: str) -> dict:
(This function is not needed if you are using the sign_up_or_in function.
Args:
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example phone or email
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp
login_id (str): The Login ID of the user being validated
code (str): The authorization code enter by the end user during signup/signin
Expand Down Expand Up @@ -206,7 +206,7 @@ def update_user_phone(
Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP.
Args:
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example phone or email
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp
login_id (str): The login ID of the user whose information is being updated
phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten
refresh_token (str): The session's refresh token (used for OTP verification)
Expand All @@ -230,7 +230,7 @@ def update_user_phone(
login_id, phone, add_to_login_ids, on_merge_use_existing, template_options
)
response = self._auth.do_post(uri, body, None, refresh_token)
return Auth.extract_masked_address(response.json(), DeliveryMethod.SMS)
return Auth.extract_masked_address(response.json(), method)

@staticmethod
def _compose_signup_url(method: DeliveryMethod) -> str:
Expand Down
1 change: 1 addition & 0 deletions descope/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class DeliveryMethod(Enum):
SMS = 2
EMAIL = 3
EMBEDDED = 4
VOICE = 5


class LoginOptions:
Expand Down
51 changes: 47 additions & 4 deletions descope/flask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ def decorated(*args, **kwargs):
return decorator


def descope_verify_code_by_phone(descope_client: DescopeClient):
def descope_verify_code_by_phone_sms(descope_client: DescopeClient):
"""
Verify code by email decorator
Verify code by phone sms decorator
"""

def decorator(f):
Expand Down Expand Up @@ -245,9 +245,52 @@ def decorated(*args, **kwargs):
return decorator


def descope_verify_code_by_whatsapp(descope_client: DescopeClient):
def descope_verify_code_by_phone_voice_call(descope_client: DescopeClient):
"""
Verify code by phone voice call decorator
"""

def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
data = request.get_json(force=True)
phone = data.get("phone", None)
code = data.get("code", None)
if not code or not phone:
return Response("Unauthorized", 401)

try:
jwt_response = descope_client.otp.verify_code(
DeliveryMethod.VOICE, phone, code
)
except AuthException:
return Response("Unauthorized", 401)

# Save the claims on the context execute the original API
_request_ctx_stack.top.claims = jwt_response
response = f(*args, **kwargs)

set_cookie_on_response(
response,
jwt_response[SESSION_TOKEN_NAME],
jwt_response[COOKIE_DATA_NAME],
)
set_cookie_on_response(
response,
jwt_response[REFRESH_SESSION_TOKEN_NAME],
jwt_response[COOKIE_DATA_NAME],
)

return response

return decorated

return decorator


def descope_verify_code_by_phone_whatsapp(descope_client: DescopeClient):
"""
Verify code by whatsapp decorator
Verify code by phone whatsapp decorator
"""

def decorator(f):
Expand Down
10 changes: 5 additions & 5 deletions descope/management/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,14 +1310,14 @@ def generate_otp_for_test_user(
Args:
method (DeliveryMethod): The method to use for "delivering" the OTP verification code to the user, for example
EMAIL, SMS, WHATSAPP or EMBEDDED
EMAIL, SMS, VOICE, WHATSAPP or EMBEDDED
login_id (str): The login ID of the test user being validated.
login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt.
Return value (dict):
Return dict in the format
{"code": "", "loginId": ""}
Containing the code for the login (exactly as it sent via Email or SMS).
Containing the code for the login (exactly as it sent via Email or Phone messaging).
Raise:
AuthException: raised if the operation fails
Expand Down Expand Up @@ -1346,15 +1346,15 @@ def generate_magic_link_for_test_user(
Args:
method (DeliveryMethod): The method to use for "delivering" the verification magic link to the user, for example
EMAIL, SMS, WHATSAPP or EMBEDDED
EMAIL, SMS, VOICE, WHATSAPP or EMBEDDED
login_id (str): The login ID of the test user being validated.
uri (str): Optional redirect uri which will be used instead of any global configuration.
login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt.
Return value (dict):
Return dict in the format
{"link": "", "loginId": ""}
Containing the magic link for the login (exactly as it sent via Email or SMS).
Containing the magic link for the login (exactly as it sent via Email or Phone messaging).
Raise:
AuthException: raised if the operation fails
Expand Down Expand Up @@ -1389,7 +1389,7 @@ def generate_enchanted_link_for_test_user(
Return value (dict):
Return dict in the format
{"link": "", "loginId": "", "pendingRef": ""}
Containing the enchanted link for the login (exactly as it sent via Email or SMS) and pendingRef.
Containing the enchanted link for the login (exactly as it sent via Email or Phone messaging) and pendingRef.
Raise:
AuthException: raised if the operation fails
Expand Down
45 changes: 45 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,43 @@ def test_verify_delivery_method(self):
False,
)

self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.VOICE, "111111111111", {"email": ""}
),
True,
)
self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.VOICE, "+111111111111", {"email": ""}
),
True,
)
self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.VOICE, "++111111111111", {"email": ""}
),
False,
)
self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.VOICE, "asdsad", {"email": ""}
),
False,
)
self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.VOICE, "", {"email": ""}
),
False,
)
self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.VOICE, "[email protected]", {"email": ""}
),
False,
)

self.assertEqual(
Auth.adjust_and_verify_delivery_method(
DeliveryMethod.WHATSAPP, "111111111111", {"email": ""}
Expand Down Expand Up @@ -270,6 +307,10 @@ def test_get_login_id_name_by_method(self):
Auth.get_login_id_by_method(DeliveryMethod.SMS, user),
("phone", "11111111"),
)
self.assertEqual(
Auth.get_login_id_by_method(DeliveryMethod.VOICE, user),
("voice", "11111111"),
)
self.assertEqual(
Auth.get_login_id_by_method(DeliveryMethod.WHATSAPP, user),
("whatsapp", "11111111"),
Expand All @@ -289,6 +330,10 @@ def test_get_method_string(self):
Auth.get_method_string(DeliveryMethod.SMS),
"sms",
)
self.assertEqual(
Auth.get_method_string(DeliveryMethod.VOICE),
"voice",
)
self.assertEqual(
Auth.get_method_string(DeliveryMethod.WHATSAPP),
"whatsapp",
Expand Down
Loading

0 comments on commit c2b6c91

Please sign in to comment.