Skip to content

Commit c2b6c91

Browse files
authored
OTP voice support (#376)
1 parent 9941ed9 commit c2b6c91

File tree

8 files changed

+213
-20
lines changed

8 files changed

+213
-20
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ For rate limiting information, please confer to the [API Rate Limits](#api-rate-
8181

8282
### OTP Authentication
8383

84-
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.
84+
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.
8585

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

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

109109
### Magic Link
110110

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

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

13311331
To ease your e2e tests, we exposed dedicated management methods,
1332-
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.
1332+
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.
13331333

13341334
```Python
13351335
# User for test can be created, this user will be able to generate code/link without

descope/auth.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ def adjust_and_verify_delivery_method(
215215
user["phone"] = login_id
216216
if not re.match(PHONE_REGEX, user["phone"]):
217217
return False
218+
elif method == DeliveryMethod.VOICE:
219+
if not user.get("phone", None):
220+
user["phone"] = login_id
221+
if not re.match(PHONE_REGEX, user["phone"]):
222+
return False
218223
elif method == DeliveryMethod.WHATSAPP:
219224
if not user.get("phone", None):
220225
user["phone"] = login_id
@@ -230,6 +235,7 @@ def compose_url(base: str, method: DeliveryMethod) -> str:
230235
suffix = {
231236
DeliveryMethod.EMAIL: "email",
232237
DeliveryMethod.SMS: "sms",
238+
DeliveryMethod.VOICE: "voice",
233239
DeliveryMethod.WHATSAPP: "whatsapp",
234240
}.get(method)
235241

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

@@ -260,6 +267,7 @@ def get_method_string(method: DeliveryMethod) -> str:
260267
name = {
261268
DeliveryMethod.EMAIL: "email",
262269
DeliveryMethod.SMS: "sms",
270+
DeliveryMethod.VOICE: "voice",
263271
DeliveryMethod.WHATSAPP: "whatsapp",
264272
DeliveryMethod.EMBEDDED: "Embedded",
265273
}.get(method)
@@ -301,7 +309,11 @@ def validate_phone(method: DeliveryMethod, phone: str):
301309
400, ERROR_TYPE_INVALID_ARGUMENT, "Invalid phone number"
302310
)
303311

304-
if method != DeliveryMethod.SMS and method != DeliveryMethod.WHATSAPP:
312+
if (
313+
method != DeliveryMethod.SMS
314+
and method != DeliveryMethod.VOICE
315+
and method != DeliveryMethod.WHATSAPP
316+
):
305317
raise AuthException(
306318
400, ERROR_TYPE_INVALID_ARGUMENT, "Invalid delivery method"
307319
)
@@ -669,7 +681,11 @@ def select_tenant(self, tenant_id: str, refresh_token: str) -> dict:
669681

670682
@staticmethod
671683
def extract_masked_address(response: dict, method: DeliveryMethod) -> str:
672-
if method == DeliveryMethod.SMS or method == DeliveryMethod.WHATSAPP:
684+
if (
685+
method == DeliveryMethod.SMS
686+
or method == DeliveryMethod.VOICE
687+
or method == DeliveryMethod.WHATSAPP
688+
):
673689
return response["maskedPhone"]
674690
elif method == DeliveryMethod.EMAIL:
675691
return response["maskedEmail"]

descope/authmethod/otp.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def sign_in(
2929
3030
Args:
3131
method (DeliveryMethod): The method to use for delivering the OTP verification code to the user, for example
32-
email, SMS, or WhatsApp
32+
Email, SMS, Voice call, or WhatsApp
3333
login_id (str): The login ID of the user being validated for example phone or email
3434
login_options (LoginOptions): Optional advanced controls over login parameters
3535
refresh_token: Optional refresh token is needed for specific login options
@@ -58,7 +58,7 @@ def sign_up(
5858
) -> str:
5959
"""
6060
Sign up (create) a new user using their email or phone number. Choose a delivery method for OTP
61-
verification, for example email, SMS, or WhatsApp.
61+
verification, for example Email, SMS, Voice call, or WhatsApp.
6262
(optional) Include additional user metadata that you wish to preserve.
6363
6464
Args:
@@ -99,7 +99,7 @@ def sign_up_or_in(
9999
using the OTP DeliveryMethod specified.
100100
101101
Args:
102-
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example phone or email
102+
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp
103103
login_id (str): The Login ID of the user being validated
104104
105105
Raise:
@@ -130,7 +130,7 @@ def verify_code(self, method: DeliveryMethod, login_id: str, code: str) -> dict:
130130
(This function is not needed if you are using the sign_up_or_in function.
131131
132132
Args:
133-
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example phone or email
133+
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp
134134
login_id (str): The Login ID of the user being validated
135135
code (str): The authorization code enter by the end user during signup/signin
136136
@@ -206,7 +206,7 @@ def update_user_phone(
206206
Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP.
207207
208208
Args:
209-
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example phone or email
209+
method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp
210210
login_id (str): The login ID of the user whose information is being updated
211211
phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten
212212
refresh_token (str): The session's refresh token (used for OTP verification)
@@ -230,7 +230,7 @@ def update_user_phone(
230230
login_id, phone, add_to_login_ids, on_merge_use_existing, template_options
231231
)
232232
response = self._auth.do_post(uri, body, None, refresh_token)
233-
return Auth.extract_masked_address(response.json(), DeliveryMethod.SMS)
233+
return Auth.extract_masked_address(response.json(), method)
234234

235235
@staticmethod
236236
def _compose_signup_url(method: DeliveryMethod) -> str:

descope/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class DeliveryMethod(Enum):
102102
SMS = 2
103103
EMAIL = 3
104104
EMBEDDED = 4
105+
VOICE = 5
105106

106107

107108
class LoginOptions:

descope/flask/__init__.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,9 @@ def decorated(*args, **kwargs):
202202
return decorator
203203

204204

205-
def descope_verify_code_by_phone(descope_client: DescopeClient):
205+
def descope_verify_code_by_phone_sms(descope_client: DescopeClient):
206206
"""
207-
Verify code by email decorator
207+
Verify code by phone sms decorator
208208
"""
209209

210210
def decorator(f):
@@ -245,9 +245,52 @@ def decorated(*args, **kwargs):
245245
return decorator
246246

247247

248-
def descope_verify_code_by_whatsapp(descope_client: DescopeClient):
248+
def descope_verify_code_by_phone_voice_call(descope_client: DescopeClient):
249+
"""
250+
Verify code by phone voice call decorator
251+
"""
252+
253+
def decorator(f):
254+
@wraps(f)
255+
def decorated(*args, **kwargs):
256+
data = request.get_json(force=True)
257+
phone = data.get("phone", None)
258+
code = data.get("code", None)
259+
if not code or not phone:
260+
return Response("Unauthorized", 401)
261+
262+
try:
263+
jwt_response = descope_client.otp.verify_code(
264+
DeliveryMethod.VOICE, phone, code
265+
)
266+
except AuthException:
267+
return Response("Unauthorized", 401)
268+
269+
# Save the claims on the context execute the original API
270+
_request_ctx_stack.top.claims = jwt_response
271+
response = f(*args, **kwargs)
272+
273+
set_cookie_on_response(
274+
response,
275+
jwt_response[SESSION_TOKEN_NAME],
276+
jwt_response[COOKIE_DATA_NAME],
277+
)
278+
set_cookie_on_response(
279+
response,
280+
jwt_response[REFRESH_SESSION_TOKEN_NAME],
281+
jwt_response[COOKIE_DATA_NAME],
282+
)
283+
284+
return response
285+
286+
return decorated
287+
288+
return decorator
289+
290+
291+
def descope_verify_code_by_phone_whatsapp(descope_client: DescopeClient):
249292
"""
250-
Verify code by whatsapp decorator
293+
Verify code by phone whatsapp decorator
251294
"""
252295

253296
def decorator(f):

descope/management/user.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,14 +1310,14 @@ def generate_otp_for_test_user(
13101310
13111311
Args:
13121312
method (DeliveryMethod): The method to use for "delivering" the OTP verification code to the user, for example
1313-
EMAIL, SMS, WHATSAPP or EMBEDDED
1313+
EMAIL, SMS, VOICE, WHATSAPP or EMBEDDED
13141314
login_id (str): The login ID of the test user being validated.
13151315
login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt.
13161316
13171317
Return value (dict):
13181318
Return dict in the format
13191319
{"code": "", "loginId": ""}
1320-
Containing the code for the login (exactly as it sent via Email or SMS).
1320+
Containing the code for the login (exactly as it sent via Email or Phone messaging).
13211321
13221322
Raise:
13231323
AuthException: raised if the operation fails
@@ -1346,15 +1346,15 @@ def generate_magic_link_for_test_user(
13461346
13471347
Args:
13481348
method (DeliveryMethod): The method to use for "delivering" the verification magic link to the user, for example
1349-
EMAIL, SMS, WHATSAPP or EMBEDDED
1349+
EMAIL, SMS, VOICE, WHATSAPP or EMBEDDED
13501350
login_id (str): The login ID of the test user being validated.
13511351
uri (str): Optional redirect uri which will be used instead of any global configuration.
13521352
login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt.
13531353
13541354
Return value (dict):
13551355
Return dict in the format
13561356
{"link": "", "loginId": ""}
1357-
Containing the magic link for the login (exactly as it sent via Email or SMS).
1357+
Containing the magic link for the login (exactly as it sent via Email or Phone messaging).
13581358
13591359
Raise:
13601360
AuthException: raised if the operation fails
@@ -1389,7 +1389,7 @@ def generate_enchanted_link_for_test_user(
13891389
Return value (dict):
13901390
Return dict in the format
13911391
{"link": "", "loginId": "", "pendingRef": ""}
1392-
Containing the enchanted link for the login (exactly as it sent via Email or SMS) and pendingRef.
1392+
Containing the enchanted link for the login (exactly as it sent via Email or Phone messaging) and pendingRef.
13931393
13941394
Raise:
13951395
AuthException: raised if the operation fails

tests/test_auth.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,43 @@ def test_verify_delivery_method(self):
231231
False,
232232
)
233233

234+
self.assertEqual(
235+
Auth.adjust_and_verify_delivery_method(
236+
DeliveryMethod.VOICE, "111111111111", {"email": ""}
237+
),
238+
True,
239+
)
240+
self.assertEqual(
241+
Auth.adjust_and_verify_delivery_method(
242+
DeliveryMethod.VOICE, "+111111111111", {"email": ""}
243+
),
244+
True,
245+
)
246+
self.assertEqual(
247+
Auth.adjust_and_verify_delivery_method(
248+
DeliveryMethod.VOICE, "++111111111111", {"email": ""}
249+
),
250+
False,
251+
)
252+
self.assertEqual(
253+
Auth.adjust_and_verify_delivery_method(
254+
DeliveryMethod.VOICE, "asdsad", {"email": ""}
255+
),
256+
False,
257+
)
258+
self.assertEqual(
259+
Auth.adjust_and_verify_delivery_method(
260+
DeliveryMethod.VOICE, "", {"email": ""}
261+
),
262+
False,
263+
)
264+
self.assertEqual(
265+
Auth.adjust_and_verify_delivery_method(
266+
DeliveryMethod.VOICE, "[email protected]", {"email": ""}
267+
),
268+
False,
269+
)
270+
234271
self.assertEqual(
235272
Auth.adjust_and_verify_delivery_method(
236273
DeliveryMethod.WHATSAPP, "111111111111", {"email": ""}
@@ -270,6 +307,10 @@ def test_get_login_id_name_by_method(self):
270307
Auth.get_login_id_by_method(DeliveryMethod.SMS, user),
271308
("phone", "11111111"),
272309
)
310+
self.assertEqual(
311+
Auth.get_login_id_by_method(DeliveryMethod.VOICE, user),
312+
("voice", "11111111"),
313+
)
273314
self.assertEqual(
274315
Auth.get_login_id_by_method(DeliveryMethod.WHATSAPP, user),
275316
("whatsapp", "11111111"),
@@ -289,6 +330,10 @@ def test_get_method_string(self):
289330
Auth.get_method_string(DeliveryMethod.SMS),
290331
"sms",
291332
)
333+
self.assertEqual(
334+
Auth.get_method_string(DeliveryMethod.VOICE),
335+
"voice",
336+
)
292337
self.assertEqual(
293338
Auth.get_method_string(DeliveryMethod.WHATSAPP),
294339
"whatsapp",

0 commit comments

Comments
 (0)