Skip to content

Commit

Permalink
feat: mfa challenge and verify and refresh session
Browse files Browse the repository at this point in the history
  • Loading branch information
leynier committed Oct 29, 2022
1 parent 4c2b443 commit b714206
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 2 deletions.
43 changes: 42 additions & 1 deletion gotrue/_async/gotrue_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
AuthMFAVerifyResponse,
AuthResponse,
DecodedJWTDict,
MFAChallengeAndVerifyParams,
MFAChallengeParams,
MFAEnrollParams,
MFAUnenrollParams,
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(
)
self.mfa = AsyncGoTrueMFAAPI()
self.mfa.challenge = self._challenge
self.mfa.challenge_and_verify = self._challenge_and_verify
self.mfa.enroll = self._enroll
self.mfa.get_authenticator_assurance_level = (
self._get_authenticator_assurance_level
Expand Down Expand Up @@ -137,7 +139,7 @@ async def sign_up(
password = credentials.get("password")
options = credentials.get("options", {})
redirect_to = options.get("redirect_to")
data = options.get("data")
data = options.get("data") or {}
captcha_token = options.get("captcha_token")
if email:
response = await self._request(
Expand Down Expand Up @@ -189,6 +191,7 @@ async def sign_in_with_password(
phone = credentials.get("phone")
password = credentials.get("password")
options = credentials.get("options", {})
data = options.get("data") or {}
captcha_token = options.get("captcha_token")
if email:
response = await self._request(
Expand All @@ -197,6 +200,7 @@ async def sign_in_with_password(
body={
"email": email,
"password": password,
"data": data,
"gotrue_meta_security": {
"captcha_token": captcha_token,
},
Expand All @@ -213,6 +217,7 @@ async def sign_in_with_password(
body={
"phone": phone,
"password": password,
"data": data,
"gotrue_meta_security": {
"captcha_token": captcha_token,
},
Expand Down Expand Up @@ -436,6 +441,25 @@ async def set_session(self, access_token: str, refresh_token: str) -> AuthRespon
self._notify_all_subscribers("TOKEN_REFRESHED", session)
return AuthResponse(session=session, user=response.user)

async def refresh_session(
self, refresh_token: Union[str, None] = None
) -> AuthResponse:
"""
Returns a new session, regardless of expiry status.
Takes in an optional current session. If not passed in, then refreshSession()
will attempt to retrieve it from getSession(). If the current session's
refresh token is invalid, an error will be thrown.
"""
if not refresh_token:
session = await self.get_session()
if session:
refresh_token = session.refresh_token
if not refresh_token:
raise AuthSessionMissingError()
session = await self._call_refresh_token(refresh_token)
return AuthResponse(session=session, user=session.user)

async def sign_out(self) -> None:
"""
Inside a browser context, `sign_out` will remove the logged in user from the
Expand Down Expand Up @@ -523,6 +547,23 @@ async def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeRespon
xform=AuthMFAChallengeResponse.parse_obj,
)

async def _challenge_and_verify(
self,
params: MFAChallengeAndVerifyParams,
) -> AuthMFAVerifyResponse:
response = await self._challenge(
{
"factor_id": params.get("factor_id"),
}
)
return await self._verify(
{
"factor_id": params.get("factor_id"),
"challenge_id": response.id,
"code": params.get("code"),
}
)

async def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse:
session = await self.get_session()
if not session:
Expand Down
12 changes: 12 additions & 0 deletions gotrue/_async/gotrue_mfa_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
AuthMFAListFactorsResponse,
AuthMFAUnenrollResponse,
AuthMFAVerifyResponse,
MFAChallengeAndVerifyParams,
MFAChallengeParams,
MFAEnrollParams,
MFAUnenrollParams,
Expand Down Expand Up @@ -38,6 +39,17 @@ async def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeRespons
"""
raise NotImplementedError()

async def challenge_and_verify(
self,
params: MFAChallengeAndVerifyParams,
) -> AuthMFAVerifyResponse:
"""
Helper method which creates a challenge and immediately uses the given code
to verify against it thereafter. The verification code is provided by the
user by entering a code seen in their authenticator app.
"""
raise NotImplementedError()

async def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse:
"""
Verifies a verification code against a challenge. The verification code is
Expand Down
43 changes: 42 additions & 1 deletion gotrue/_sync/gotrue_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
AuthMFAVerifyResponse,
AuthResponse,
DecodedJWTDict,
MFAChallengeAndVerifyParams,
MFAChallengeParams,
MFAEnrollParams,
MFAUnenrollParams,
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(
)
self.mfa = SyncGoTrueMFAAPI()
self.mfa.challenge = self._challenge
self.mfa.challenge_and_verify = self._challenge_and_verify
self.mfa.enroll = self._enroll
self.mfa.get_authenticator_assurance_level = (
self._get_authenticator_assurance_level
Expand Down Expand Up @@ -137,7 +139,7 @@ def sign_up(
password = credentials.get("password")
options = credentials.get("options", {})
redirect_to = options.get("redirect_to")
data = options.get("data")
data = options.get("data") or {}
captcha_token = options.get("captcha_token")
if email:
response = self._request(
Expand Down Expand Up @@ -189,6 +191,7 @@ def sign_in_with_password(
phone = credentials.get("phone")
password = credentials.get("password")
options = credentials.get("options", {})
data = options.get("data") or {}
captcha_token = options.get("captcha_token")
if email:
response = self._request(
Expand All @@ -197,6 +200,7 @@ def sign_in_with_password(
body={
"email": email,
"password": password,
"data": data,
"gotrue_meta_security": {
"captcha_token": captcha_token,
},
Expand All @@ -213,6 +217,7 @@ def sign_in_with_password(
body={
"phone": phone,
"password": password,
"data": data,
"gotrue_meta_security": {
"captcha_token": captcha_token,
},
Expand Down Expand Up @@ -436,6 +441,25 @@ def set_session(self, access_token: str, refresh_token: str) -> AuthResponse:
self._notify_all_subscribers("TOKEN_REFRESHED", session)
return AuthResponse(session=session, user=response.user)

def refresh_session(
self, refresh_token: Union[str, None] = None
) -> AuthResponse:
"""
Returns a new session, regardless of expiry status.
Takes in an optional current session. If not passed in, then refreshSession()
will attempt to retrieve it from getSession(). If the current session's
refresh token is invalid, an error will be thrown.
"""
if not refresh_token:
session = self.get_session()
if session:
refresh_token = session.refresh_token
if not refresh_token:
raise AuthSessionMissingError()
session = self._call_refresh_token(refresh_token)
return AuthResponse(session=session, user=session.user)

def sign_out(self) -> None:
"""
Inside a browser context, `sign_out` will remove the logged in user from the
Expand Down Expand Up @@ -523,6 +547,23 @@ def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse:
xform=AuthMFAChallengeResponse.parse_obj,
)

def _challenge_and_verify(
self,
params: MFAChallengeAndVerifyParams,
) -> AuthMFAVerifyResponse:
response = self._challenge(
{
"factor_id": params.get("factor_id"),
}
)
return self._verify(
{
"factor_id": params.get("factor_id"),
"challenge_id": response.id,
"code": params.get("code"),
}
)

def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse:
session = self.get_session()
if not session:
Expand Down
12 changes: 12 additions & 0 deletions gotrue/_sync/gotrue_mfa_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
AuthMFAListFactorsResponse,
AuthMFAUnenrollResponse,
AuthMFAVerifyResponse,
MFAChallengeAndVerifyParams,
MFAChallengeParams,
MFAEnrollParams,
MFAUnenrollParams,
Expand Down Expand Up @@ -38,6 +39,17 @@ def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse:
"""
raise NotImplementedError()

def challenge_and_verify(
self,
params: MFAChallengeAndVerifyParams,
) -> AuthMFAVerifyResponse:
"""
Helper method which creates a challenge and immediately uses the given code
to verify against it thereafter. The verification code is provided by the
user by entering a code seen in their authenticator app.
"""
raise NotImplementedError()

def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse:
"""
Verifies a verification code against a challenge. The verification code is
Expand Down
12 changes: 12 additions & 0 deletions gotrue/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ class SignUpWithPhoneAndPasswordCredentials(TypedDict):


class SignInWithPasswordCredentialsOptions(TypedDict):
data: NotRequired[Any]
captcha_token: NotRequired[str]


Expand Down Expand Up @@ -421,6 +422,17 @@ class MFAChallengeParams(TypedDict):
"""


class MFAChallengeAndVerifyParams(TypedDict):
factor_id: str
"""
ID of the factor being verified.
"""
code: str
"""
Verification code provided by the user.
"""


class AuthMFAVerifyResponse(BaseModel):
access_token: str
"""
Expand Down

0 comments on commit b714206

Please sign in to comment.