Skip to content

Commit b2bb7a1

Browse files
feat: add MFA Phone (#578)
1 parent 0503105 commit b2bb7a1

File tree

3 files changed

+55
-10
lines changed

3 files changed

+55
-10
lines changed

supabase_auth/_async/gotrue_client.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -737,14 +737,25 @@ async def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse:
737737
session = await self.get_session()
738738
if not session:
739739
raise AuthSessionMissingError()
740+
741+
body = {
742+
"friendly_name": params["friendly_name"],
743+
"factor_type": params["factor_type"],
744+
}
745+
746+
if params["factor_type"] == "phone":
747+
body["phone"] = params["phone"]
748+
else:
749+
body["issuer"] = params["issuer"]
750+
740751
response = await self._request(
741752
"POST",
742753
"factors",
743-
body=params,
754+
body=body,
744755
jwt=session.access_token,
745756
xform=partial(model_validate, AuthMFAEnrollResponse),
746757
)
747-
if response.totp.qr_code:
758+
if params["factor_type"] == "totp" and response.totp.qr_code:
748759
response.totp.qr_code = f"data:image/svg+xml;utf-8,{response.totp.qr_code}"
749760
return response
750761

@@ -755,6 +766,7 @@ async def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeRespon
755766
return await self._request(
756767
"POST",
757768
f"factors/{params.get('factor_id')}/challenge",
769+
body={"channel": params["channel"]},
758770
jwt=session.access_token,
759771
xform=partial(model_validate, AuthMFAChallengeResponse),
760772
)
@@ -807,7 +819,8 @@ async def _list_factors(self) -> AuthMFAListFactorsResponse:
807819
response = await self.get_user()
808820
all = response.user.factors or []
809821
totp = [f for f in all if f.factor_type == "totp" and f.status == "verified"]
810-
return AuthMFAListFactorsResponse(all=all, totp=totp)
822+
phone = [f for f in all if f.factor_type == "phone" and f.status == "verified"]
823+
return AuthMFAListFactorsResponse(all=all, totp=totp, phone=phone)
811824

812825
async def _get_authenticator_assurance_level(
813826
self,

supabase_auth/_sync/gotrue_client.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -729,14 +729,25 @@ def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse:
729729
session = self.get_session()
730730
if not session:
731731
raise AuthSessionMissingError()
732+
733+
body = {
734+
"friendly_name": params["friendly_name"],
735+
"factor_type": params["factor_type"],
736+
}
737+
738+
if params["factor_type"] == "phone":
739+
body["phone"] = params["phone"]
740+
else:
741+
body["issuer"] = params["issuer"]
742+
732743
response = self._request(
733744
"POST",
734745
"factors",
735-
body=params,
746+
body=body,
736747
jwt=session.access_token,
737748
xform=partial(model_validate, AuthMFAEnrollResponse),
738749
)
739-
if response.totp.qr_code:
750+
if params["factor_type"] == "totp" and response.totp.qr_code:
740751
response.totp.qr_code = f"data:image/svg+xml;utf-8,{response.totp.qr_code}"
741752
return response
742753

@@ -747,6 +758,7 @@ def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse:
747758
return self._request(
748759
"POST",
749760
f"factors/{params.get('factor_id')}/challenge",
761+
body={"channel": params["channel"]},
750762
jwt=session.access_token,
751763
xform=partial(model_validate, AuthMFAChallengeResponse),
752764
)
@@ -799,7 +811,8 @@ def _list_factors(self) -> AuthMFAListFactorsResponse:
799811
response = self.get_user()
800812
all = response.user.factors or []
801813
totp = [f for f in all if f.factor_type == "totp" and f.status == "verified"]
802-
return AuthMFAListFactorsResponse(all=all, totp=totp)
814+
phone = [f for f in all if f.factor_type == "phone" and f.status == "verified"]
815+
return AuthMFAListFactorsResponse(all=all, totp=totp, phone=phone)
803816

804817
def _get_authenticator_assurance_level(
805818
self,

supabase_auth/types.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from time import time
55
from typing import Any, Callable, Dict, List, Union
66

7-
from pydantic import BaseModel
7+
from pydantic import BaseModel, ConfigDict
88

99
try:
1010
# > 2
@@ -180,7 +180,7 @@ class Factor(BaseModel):
180180
"""
181181
Friendly name of the factor, useful to disambiguate between multiple factors.
182182
"""
183-
factor_type: Union[Literal["totp"], str]
183+
factor_type: Union[Literal["totp", "phone"], str]
184184
"""
185185
Type of factor. Only `totp` supported with this version but may change in
186186
future versions.
@@ -492,9 +492,10 @@ class GenerateEmailChangeLinkParams(TypedDict):
492492

493493

494494
class MFAEnrollParams(TypedDict):
495-
factor_type: Literal["totp"]
495+
factor_type: Literal["totp", "phone"]
496496
issuer: NotRequired[str]
497497
friendly_name: NotRequired[str]
498+
phone: str
498499

499500

500501
class MFAUnenrollParams(TypedDict):
@@ -539,6 +540,7 @@ class MFAChallengeParams(TypedDict):
539540
"""
540541
ID of the factor to be challenged.
541542
"""
543+
channel: NotRequired[Literal["sms", "whatsapp"]]
542544

543545

544546
class MFAChallengeAndVerifyParams(TypedDict):
@@ -600,14 +602,23 @@ class AuthMFAEnrollResponse(BaseModel):
600602
"""
601603
ID of the factor that was just enrolled (in an unverified state).
602604
"""
603-
type: Literal["totp"]
605+
type: Literal["totp", "phone"]
604606
"""
605607
Type of MFA factor. Only `totp` supported for now.
606608
"""
607609
totp: AuthMFAEnrollResponseTotp
608610
"""
609611
TOTP enrollment information.
610612
"""
613+
model_config = ConfigDict(arbitrary_types_allowed=True)
614+
friendly_name: str
615+
"""
616+
Friendly name of the factor, useful for distinguishing between factors
617+
"""
618+
phone: str
619+
"""
620+
Phone number of the MFA factor in E.164 format. Used to send messages
621+
"""
611622

612623

613624
class AuthMFAUnenrollResponse(BaseModel):
@@ -626,6 +637,10 @@ class AuthMFAChallengeResponse(BaseModel):
626637
"""
627638
Timestamp in UNIX seconds when this challenge will no longer be usable.
628639
"""
640+
factor_type: Literal["totp", "phone"]
641+
"""
642+
Factor Type which generated the challenge
643+
"""
629644

630645

631646
class AuthMFAListFactorsResponse(BaseModel):
@@ -637,6 +652,10 @@ class AuthMFAListFactorsResponse(BaseModel):
637652
"""
638653
Only verified TOTP factors. (A subset of `all`.)
639654
"""
655+
phone: List[Factor]
656+
"""
657+
Only verified Phone factors. (A subset of `all`.)
658+
"""
640659

641660

642661
AuthenticatorAssuranceLevels = Literal["aal1", "aal2"]

0 commit comments

Comments
 (0)