Skip to content

Commit

Permalink
Merge pull request #14 from bariqhibat/bariqhibat/support-phone-otp
Browse files Browse the repository at this point in the history
Add support for phone otp
  • Loading branch information
J0 committed Oct 9, 2021
2 parents 0320f45 + 202346a commit f237f15
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 3 deletions.
68 changes: 68 additions & 0 deletions gotrue/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,74 @@ def sign_up_with_email(self, email: str, password: str) -> Dict[str, Any]:
)
return to_dict(request)

def sign_up_with_phone(self, phone: str, password: str) -> Dict[str, Any]:
"""Creates a new user using their phone number
Parameters
----------
phone : str
The user's phone number.
password : str
The user's password.
Returns
-------
request : dict of any
The user or error message returned by the supabase backend.
"""
credentials = {"phone": phone, "password": password}
request = requests.post(
f"{self.url}/signup", json.dumps(credentials), headers=self.headers
)
return to_dict(request)

def send_mobile_otp(self, phone: str) -> Dict[str, Any]:
"""Sends a mobile OTP via SMS. Will register the account if it doesn't already exist
Parameters
----------
phone : str
The user's phone number WITH international prefix.
Returns
-------
request : dict of any
The user or error message returned by the supabase backend.
"""
credentials = {"phone": phone}
request = requests.post(
f"{self.url}/otp", json.dumps(credentials), headers=self.headers
)
return to_dict(request)

def verify_mobile_otp(self, phone: str, token: str, options: Dict[str, Any]) -> Dict[str, Any]:
"""Send User supplied Mobile OTP to be verified
Parameters
----------
phone : str
The user's phone number WITH international prefix
token : str
Token that user was sent to their mobile phone
options : dict of any
A URL or mobile address to send the user to after they are confirmed.
Returns
-------
request : dict of any
The user or error message returned by the supabase backend.
"""
payload = {
"phone": phone,
"token": token,
"type": "sms",
"redirect_to": options.get("redirect_to")
}
request = requests.post(
f"{self.url}/verify", json.dumps(payload), headers=self.headers
)
return to_dict(request)

def sign_in_with_email(self, email: str, password: str) -> Dict[str, Any]:
"""Logs in an existing user using their email address.
Expand Down
38 changes: 35 additions & 3 deletions gotrue/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __init__(
self.api = GoTrueApi(url=url, headers=headers, cookie_options=cookie_options)
self._recover_session()

def sign_up(self, email: str, password: str):
def sign_up(self, password: str, phone: Optional[str], email: Optional[str]):
"""Creates a new user.
Parameters
Expand All @@ -71,7 +71,14 @@ def sign_up(self, email: str, password: str):
The user's password.
"""
self._remove_session()
data = self.api.sign_up_with_email(email, password)

if email and password:
data = self.api.sign_up_with_email(email, password)
elif phone and password:
data = self.api.sign_up_with_phone(phone, password)
else:
raise ValueError("Email or phone must be defined, both can't be None.")

if "expires_in" in data and "user" in data:
# The user has confirmed their email or the underlying DB doesn't
# require email confirmation.
Expand All @@ -84,17 +91,42 @@ def sign_in(
email: Optional[str] = None,
password: Optional[str] = None,
provider: Optional[str] = None,
phone: Optional[str] = None,
) -> Dict[str, Any]:
"""Log in an exisiting user, or login via a third-party provider."""
self._remove_session()
if email is not None and password is None:
data = self.api.send_magic_link_email(email)
elif email is not None and password is not None:
data = self._handle_email_sign_in(email, password)
elif phone is not None and password is None:
data = self.api.send_mobile_otp(phone)
elif provider is not None:
data = self._handle_provider_sign_in(provider)
else:
raise ValueError("Email or provider must be defined, both can't be None.")
raise ValueError("Email, provider, or phone must be defined, all can't be None.")
return data

def verify_otp(
self,
phone: str,
token: str,
options: Optional[Dict[str, Any]] = {},
) -> Dict[str, Any]:
"""Log in a user given a User supplied OTP received via mobile."""
self._remove_session()

if options is None:
options = {}

data = self.api.verify_mobile_otp(phone, token, options)

if "access_token" in data:
session = data

self._save_session(session)
self._notify_all_subscribers('SIGNED _IN')

return data

def user(self) -> Optional[Dict[str, Any]]:
Expand Down
48 changes: 48 additions & 0 deletions tests/test_gotrue.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ def _random_string(length: int = 10) -> str:
random.choices(string.ascii_uppercase + string.digits, k=length))


def _random_phone_number() -> str:
first = str(random.randint(100, 999))
second = str(random.randint(1, 888)).zfill(3)
last = (str(random.randint(1, 9998)).zfill(4))
while last in ['1111', '2222', '3333', '4444', '5555', '6666', '7777', '8888']:
last = (str(random.randint(1, 9998)).zfill(4))
return '{}-{}-{}'.format(first, second, last)


def _assert_authenticated_user(data: Dict[str, Any]):
"""Raise assertion error if user is not logged in correctly."""
assert "access_token" in data
Expand Down Expand Up @@ -114,3 +123,42 @@ def test_set_auth(client: Client):
client.set_auth(mock_access_token)
new_session = client.session()
assert new_session["access_token"] == mock_access_token


def test_sign_up_phone_password(client: Client):
"""Test client can sign up with phone and password"""
random_phone: str = _random_phone_number()
random_password: str = _random_string(20)
data = client.sign_up(phone=random_phone, password=random_password)
_assert_authenticated_user(data)
assert client.current_user is not None
assert client.current_session is not None

assert "id" in data and isinstance(data.get('id'), str)
assert "created_at" in data and isinstance(data.get('created_at'), str)
assert "email" in data and data.get('email') == ''
assert "confirmation_sent_at" in data and isinstance(
data.get('confirmation_sent_at'), str)
assert "phone" in data and data.get('phone') == random_phone
assert "aud" in data and isinstance(data.get('aud'), str)
assert "updated_at" in data and isinstance(data.get('updated_at'), str)
assert "app_metadata" in data and isinstance(
data.get('app_metadata'), dict)
assert "provider" in data.get(
'app_metadata') and data['app_metadata'].get('provider') == 'phone'
assert "user_metadata" in data and isinstance(data.get('id'), dict)
assert "status" in data.get(
'user_metadata') and data['user_metadata'].get('status') == 'alpha'


def test_verify_mobile_otp(client: Client):
"""Test client can verify their mobile using OTP"""
random_token: str = '123456'
random_phone: str = _random_phone_number()
data = client.verify_otp(phone=random_phone, token=random_token)

assert "session" in data and data['session'] is None
assert "user" in data and data['user'] is None
assert "error" in data
assert "message" in data['error']
assert "Otp has expired or is invalid" in data['error']['message']

0 comments on commit f237f15

Please sign in to comment.