Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,29 @@ pending_ref = resp["pendingRef"] # Used to poll for a valid session
masked_email = resp["maskedEmail"] # The email that the message was sent to in a masked format
```

To deliver the link by SMS instead, use the phone variants — `sign_up_with_phone`,
`sign_in_with_phone` and `sign_up_or_in_with_phone`. They return `maskedPhone` in place
of `maskedEmail`. The SMS carries only the correct link, so there is nothing for the
user to choose:

```python
resp = descope_client.enchantedlink.sign_up_or_in_with_phone(
phone=phone,
uri="http://myapp.com/verify-enchanted-link", # Set redirect URI here or via console
)
link_identifier = resp["linkId"] # Show the user which link they should press in their SMS
pending_ref = resp["pendingRef"] # Used to poll for a valid session
masked_phone = resp["maskedPhone"] # The phone number that the message was sent to in a masked format
```

An existing user's email or phone can be updated with an enchanted link sent to the new
address, which the user must click to confirm the change:

```python
descope_client.enchantedlink.update_user_email(login_id, new_email, refresh_token)
descope_client.enchantedlink.update_user_phone(login_id, new_phone, refresh_token)
```

After sending the link, you must poll to receive a valid session using the `pending_ref` from
the previous step. A valid session will be returned only after the user clicks the right link.

Expand Down
43 changes: 36 additions & 7 deletions descope/authmethod/_enchantedlink_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty")

@staticmethod
def _compose_signin_url() -> str:
return Auth.compose_url(EndpointsV1.sign_in_auth_enchantedlink_path, DeliveryMethod.EMAIL)
def _compose_signin_url(method: DeliveryMethod) -> str:
return Auth.compose_url(EndpointsV1.sign_in_auth_enchantedlink_path, method)

@staticmethod
def _compose_signup_url() -> str:
return Auth.compose_url(EndpointsV1.sign_up_auth_enchantedlink_path, DeliveryMethod.EMAIL)
def _compose_signup_url(method: DeliveryMethod) -> str:
return Auth.compose_url(EndpointsV1.sign_up_auth_enchantedlink_path, method)

@staticmethod
def _compose_sign_up_or_in_url() -> str:
return Auth.compose_url(EndpointsV1.sign_up_or_in_auth_enchantedlink_path, DeliveryMethod.EMAIL)
def _compose_sign_up_or_in_url(method: DeliveryMethod) -> str:
return Auth.compose_url(EndpointsV1.sign_up_or_in_auth_enchantedlink_path, method)

@staticmethod
def _compose_update_phone_url(method: DeliveryMethod) -> str:
return Auth.compose_url(EndpointsV1.update_user_phone_enchantedlink_path, method)

@staticmethod
def _compose_signin_body(
Expand All @@ -58,6 +62,7 @@

@staticmethod
def _compose_signup_body(
method: DeliveryMethod,
login_id: str,
uri: str,
user: dict | None = None,
Expand All @@ -70,7 +75,7 @@

if user is not None:
body["user"] = user
method_str, val = Auth.get_login_id_by_method(DeliveryMethod.EMAIL, user)
method_str, val = Auth.get_login_id_by_method(method, user)
body[method_str] = val
return body

Expand Down Expand Up @@ -102,6 +107,30 @@
body["providerId"] = provider_id
return body

@staticmethod
def _compose_update_user_phone_body(
login_id: str,
phone: str,
add_to_login_ids: bool,
on_merge_use_existing: bool,
template_options: dict | None = None,
template_id: str | None = None,
provider_id: str | None = None,
) -> dict:
body: dict[str, str | bool | dict] = {
"loginId": login_id,
"phone": phone,
"addToLoginIDs": add_to_login_ids,
"onMergeUseExisting": on_merge_use_existing,
}
if template_options is not None:
body["templateOptions"] = template_options

Check warning on line 127 in descope/authmethod/_enchantedlink_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if template_id is not None:
body["templateId"] = template_id

Check warning on line 129 in descope/authmethod/_enchantedlink_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if provider_id is not None:
body["providerId"] = provider_id

Check warning on line 131 in descope/authmethod/_enchantedlink_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
return body

@staticmethod
def _compose_get_session_body(pending_ref: str) -> dict:
return {"pendingRef": pending_ref}
90 changes: 86 additions & 4 deletions descope/authmethod/enchantedlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,23 @@
validate_refresh_token_provided(login_options, refresh_token)

body = self._compose_signin_body(login_id, uri, login_options)
url = self._compose_signin_url()
url = self._compose_signin_url(DeliveryMethod.EMAIL)
response = self._http.post(url, body=body, pswd=refresh_token)
return response.json()

def sign_in_with_phone(
self,
phone: str,
uri: str,
login_options: LoginOptions | None = None,
refresh_token: str | None = None,
) -> dict:
self._validate_sign_in_login_id(phone)

validate_refresh_token_provided(login_options, refresh_token)

body = self._compose_signin_body(phone, uri, login_options)
url = self._compose_signin_url(DeliveryMethod.SMS)
response = self._http.post(url, body=body, pswd=refresh_token)
return response.json()

Expand All @@ -48,22 +64,60 @@
f"Login ID {login_id} is not valid for email",
)

body = self._compose_signup_body(login_id, uri, user, signup_options)
url = self._compose_signup_url()
body = self._compose_signup_body(DeliveryMethod.EMAIL, login_id, uri, user, signup_options)
url = self._compose_signup_url(DeliveryMethod.EMAIL)
response = self._http.post(url, body=body)
return response.json()

def sign_up_with_phone(
self,
phone: str,
uri: str,
user: dict | None = None,
signup_options: SignUpOptions | None = None,
) -> dict:
if not user:
user = {}

Check warning on line 80 in descope/authmethod/enchantedlink.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage

if not self._auth.adjust_and_verify_delivery_method(DeliveryMethod.SMS, phone, user):
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Login ID {phone} is not valid for phone",
)

body = self._compose_signup_body(DeliveryMethod.SMS, phone, uri, user, signup_options)
url = self._compose_signup_url(DeliveryMethod.SMS)
response = self._http.post(url, body=body)
return response.json()

def sign_up_or_in(self, login_id: str, uri: str, signup_options: SignUpOptions | None = None) -> dict:
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
)

body = self._compose_signin_body(login_id, uri, login_options)
url = self._compose_sign_up_or_in_url()
url = self._compose_sign_up_or_in_url(DeliveryMethod.EMAIL)
response = self._http.post(url, body=body)
return response.json()

def sign_up_or_in_with_phone(self, phone: str, uri: str, signup_options: SignUpOptions | None = None) -> dict:
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
Comment thread
eliran-descope marked this conversation as resolved.
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
)

body = self._compose_signin_body(phone, uri, login_options)
url = self._compose_sign_up_or_in_url(DeliveryMethod.SMS)
response = self._http.post(url, body=body)
return response.json()

Expand Down Expand Up @@ -106,3 +160,31 @@
uri = EndpointsV1.update_user_email_enchantedlink_path
response = self._http.post(uri, body=body, pswd=refresh_token)
return response.json()

def update_user_phone(
self,
login_id: str,
phone: str,
refresh_token: str,
add_to_login_ids: bool = False,
on_merge_use_existing: bool = False,
template_options: dict | None = None,
template_id: str | None = None,
provider_id: str | None = None,
) -> dict:
self._validate_login_id(login_id)

Auth.validate_phone(DeliveryMethod.SMS, phone)

body = self._compose_update_user_phone_body(
login_id,
phone,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
url = self._compose_update_phone_url(DeliveryMethod.SMS)
response = self._http.post(url, body=body, pswd=refresh_token)
return response.json()
94 changes: 90 additions & 4 deletions descope/authmethod/enchantedlink_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,24 @@
validate_refresh_token_provided(login_options, refresh_token)

body = self._compose_signin_body(login_id, uri, login_options)
url = self._compose_signin_url()
url = self._compose_signin_url(DeliveryMethod.EMAIL)
response = await self._http.post(url, body=body, pswd=refresh_token)
return response.json()

async def sign_in_with_phone(
self,
phone: str,
uri: str,
login_options: LoginOptions | None = None,
refresh_token: str | None = None,
) -> dict:
"""Send an enchanted-link SMS for sign-in; returns the pending-ref and link-id."""
self._validate_sign_in_login_id(phone)

validate_refresh_token_provided(login_options, refresh_token)

body = self._compose_signin_body(phone, uri, login_options)
url = self._compose_signin_url(DeliveryMethod.SMS)
response = await self._http.post(url, body=body, pswd=refresh_token)
return response.json()

Expand All @@ -52,8 +69,31 @@
f"Login ID {login_id} is not valid for email",
)

body = self._compose_signup_body(login_id, uri, user, signup_options)
url = self._compose_signup_url()
body = self._compose_signup_body(DeliveryMethod.EMAIL, login_id, uri, user, signup_options)
url = self._compose_signup_url(DeliveryMethod.EMAIL)
response = await self._http.post(url, body=body)
return response.json()

async def sign_up_with_phone(
self,
phone: str,
uri: str,
user: dict | None = None,
signup_options: SignUpOptions | None = None,
) -> dict:
"""Send an enchanted-link SMS for sign-up; returns the pending-ref and link-id."""
if not user:
user = {}

Check warning on line 86 in descope/authmethod/enchantedlink_async.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage

if not self._auth.adjust_and_verify_delivery_method(DeliveryMethod.SMS, phone, user):
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Login ID {phone} is not valid for phone",
)

body = self._compose_signup_body(DeliveryMethod.SMS, phone, uri, user, signup_options)
url = self._compose_signup_url(DeliveryMethod.SMS)
response = await self._http.post(url, body=body)
return response.json()

Expand All @@ -62,13 +102,30 @@
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
)

body = self._compose_signin_body(login_id, uri, login_options)
url = self._compose_sign_up_or_in_url()
url = self._compose_sign_up_or_in_url(DeliveryMethod.EMAIL)
response = await self._http.post(url, body=body)
return response.json()

async def sign_up_or_in_with_phone(self, phone: str, uri: str, signup_options: SignUpOptions | None = None) -> dict:
"""Send an enchanted-link SMS for sign-up or sign-in depending on whether the user exists."""
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
)

body = self._compose_signin_body(phone, uri, login_options)
url = self._compose_sign_up_or_in_url(DeliveryMethod.SMS)
response = await self._http.post(url, body=body)
return response.json()

Expand Down Expand Up @@ -115,3 +172,32 @@
uri = EndpointsV1.update_user_email_enchantedlink_path
response = await self._http.post(uri, body=body, pswd=refresh_token)
return response.json()

async def update_user_phone(
self,
login_id: str,
phone: str,
refresh_token: str,
add_to_login_ids: bool = False,
on_merge_use_existing: bool = False,
template_options: dict | None = None,
template_id: str | None = None,
provider_id: str | None = None,
) -> dict:
"""Send an enchanted-link SMS to a new phone number to verify the update."""
self._validate_login_id(login_id)

Auth.validate_phone(DeliveryMethod.SMS, phone)

body = self._compose_update_user_phone_body(
login_id,
phone,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
url = self._compose_update_phone_url(DeliveryMethod.SMS)
response = await self._http.post(url, body=body, pswd=refresh_token)
return response.json()
1 change: 1 addition & 0 deletions descope/authmethod/magiclink.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def sign_up_or_in(
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
Expand Down
1 change: 1 addition & 0 deletions descope/authmethod/magiclink_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async def sign_up_or_in(
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
Expand Down
1 change: 1 addition & 0 deletions descope/authmethod/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def sign_up_or_in(
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
Expand Down
1 change: 1 addition & 0 deletions descope/authmethod/otp_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async def sign_up_or_in(
login_options: LoginOptions | None = None
if signup_options is not None:
login_options = LoginOptions(
revoke_other_sessions=signup_options.revokeOtherSessions,
custom_claims=signup_options.customClaims,
template_options=signup_options.templateOptions,
template_id=signup_options.templateId,
Expand Down
1 change: 1 addition & 0 deletions descope/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class EndpointsV1:
verify_enchantedlink_auth_path = "/v1/auth/enchantedlink/verify"
get_session_enchantedlink_auth_path = "/v1/auth/enchantedlink/pending-session"
update_user_email_enchantedlink_path = "/v1/auth/enchantedlink/update/email"
update_user_phone_enchantedlink_path = "/v1/auth/enchantedlink/update/phone"

# oauth
oauth_start_path = "/v1/auth/oauth/authorize"
Expand Down
Loading
Loading