Skip to content

Commit 6d4b1f4

Browse files
authored
Enchanted link support (#86)
* Enchanted link support + tests Remove support from cross device related to descope/etc#1014 * Added missing tests
1 parent 09f7b61 commit 6d4b1f4

File tree

8 files changed

+519
-370
lines changed

8 files changed

+519
-370
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import string
2+
3+
import requests
4+
5+
from descope.auth import Auth
6+
from descope.common import (
7+
REFRESH_SESSION_COOKIE_NAME,
8+
DeliveryMethod,
9+
EndpointsV1,
10+
LoginOptions,
11+
validateRefreshTokenProvided,
12+
)
13+
from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException
14+
15+
16+
class EnchantedLink:
17+
_auth: Auth
18+
19+
def __init__(self, auth: Auth):
20+
self._auth = auth
21+
22+
def sign_in(
23+
self,
24+
identifier: str,
25+
uri: str,
26+
loginOptions: LoginOptions = None,
27+
refreshToken: str = None,
28+
) -> dict:
29+
if not identifier:
30+
raise AuthException(
31+
400,
32+
ERROR_TYPE_INVALID_ARGUMENT,
33+
"Identifier is empty",
34+
)
35+
36+
validateRefreshTokenProvided(loginOptions, refreshToken)
37+
38+
body = EnchantedLink._compose_signin_body(identifier, uri, loginOptions)
39+
uri = EnchantedLink._compose_signin_url()
40+
41+
response = self._auth.do_post(uri, body, None, refreshToken)
42+
return EnchantedLink._get_pending_ref_from_response(response)
43+
44+
def sign_up(self, identifier: str, uri: str, user: dict = None) -> None:
45+
if not self._auth.verify_delivery_method(
46+
DeliveryMethod.EMAIL, identifier, user
47+
):
48+
raise AuthException(
49+
400,
50+
ERROR_TYPE_INVALID_ARGUMENT,
51+
f"Identifier {identifier} is not valid for email",
52+
)
53+
54+
body = EnchantedLink._compose_signup_body(identifier, uri, user)
55+
uri = EnchantedLink._compose_signup_url()
56+
response = self._auth.do_post(uri, body, None)
57+
return EnchantedLink._get_pending_ref_from_response(response)
58+
59+
def sign_up_or_in(self, identifier: str, uri: str) -> dict:
60+
body = EnchantedLink._compose_signin_body(identifier, uri)
61+
uri = EnchantedLink._compose_sign_up_or_in_url()
62+
response = self._auth.do_post(uri, body, None)
63+
return EnchantedLink._get_pending_ref_from_response(response)
64+
65+
def get_session(self, pending_ref: str) -> dict:
66+
uri = EndpointsV1.getSessionEnchantedLinkAuthPath
67+
body = EnchantedLink._compose_get_session_body(pending_ref)
68+
response = self._auth.do_post(uri, body, None)
69+
70+
resp = response.json()
71+
jwt_response = self._auth.generate_jwt_response(
72+
resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None)
73+
)
74+
return jwt_response
75+
76+
def verify(self, token: str):
77+
uri = EndpointsV1.verifyEnchantedLinkAuthPath
78+
body = EnchantedLink._compose_verify_body(token)
79+
self._auth.do_post(uri, body, None)
80+
81+
def update_user_email(
82+
self, identifier: str, email: str, refresh_token: str
83+
) -> dict:
84+
if not identifier:
85+
raise AuthException(
86+
400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty"
87+
)
88+
89+
Auth.validate_email(email)
90+
91+
body = EnchantedLink._compose_update_user_email_body(identifier, email)
92+
uri = EndpointsV1.updateUserEmailOTPPath
93+
response = self._auth.do_post(uri, body, None, refresh_token)
94+
return EnchantedLink._get_pending_ref_from_response(response)
95+
96+
@staticmethod
97+
def _compose_signin_url() -> str:
98+
return Auth.compose_url(
99+
EndpointsV1.signInAuthEnchantedLinkPath, DeliveryMethod.EMAIL
100+
)
101+
102+
@staticmethod
103+
def _compose_signup_url() -> str:
104+
return Auth.compose_url(
105+
EndpointsV1.signUpAuthEnchantedLinkPath, DeliveryMethod.EMAIL
106+
)
107+
108+
@staticmethod
109+
def _compose_sign_up_or_in_url() -> str:
110+
return Auth.compose_url(
111+
EndpointsV1.signUpOrInAuthEnchantedLinkPath, DeliveryMethod.EMAIL
112+
)
113+
114+
@staticmethod
115+
def _compose_signin_body(
116+
identifier: string,
117+
uri: string,
118+
loginOptions: LoginOptions = None,
119+
) -> dict:
120+
return {
121+
"externalId": identifier,
122+
"URI": uri,
123+
"loginOptions": loginOptions.__dict__ if loginOptions else {},
124+
}
125+
126+
@staticmethod
127+
def _compose_signup_body(
128+
identifier: string,
129+
uri: string,
130+
user: dict = None,
131+
) -> dict:
132+
body = {"externalId": identifier, "URI": uri}
133+
134+
if user is not None:
135+
body["user"] = user
136+
method_str, val = Auth.get_identifier_by_method(DeliveryMethod.EMAIL, user)
137+
body[method_str] = val
138+
return body
139+
140+
@staticmethod
141+
def _compose_verify_body(token: string) -> dict:
142+
return {"token": token}
143+
144+
@staticmethod
145+
def _compose_update_user_email_body(identifier: str, email: str) -> dict:
146+
return {"externalId": identifier, "email": email}
147+
148+
@staticmethod
149+
def _compose_get_session_body(pending_ref: str) -> dict:
150+
return {"pendingRef": pending_ref}
151+
152+
@staticmethod
153+
def _get_pending_ref_from_response(response: requests.Response) -> dict:
154+
return response.json()

0 commit comments

Comments
 (0)