Skip to content

Commit

Permalink
Add email otp to b2b (#225)
Browse files Browse the repository at this point in the history
* add email otp to b2b

* update comments
  • Loading branch information
etaylormcgregor-stytch authored Nov 14, 2024
1 parent c8935e1 commit 583c55f
Show file tree
Hide file tree
Showing 13 changed files with 558 additions and 39 deletions.
12 changes: 6 additions & 6 deletions stytch/b2b/api/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,9 @@ def update(
if sso_jit_provisioning is not None:
data["sso_jit_provisioning"] = sso_jit_provisioning
if sso_jit_provisioning_allowed_connections is not None:
data[
"sso_jit_provisioning_allowed_connections"
] = sso_jit_provisioning_allowed_connections
data["sso_jit_provisioning_allowed_connections"] = (
sso_jit_provisioning_allowed_connections
)
if email_allowed_domains is not None:
data["email_allowed_domains"] = email_allowed_domains
if email_jit_provisioning is not None:
Expand Down Expand Up @@ -689,9 +689,9 @@ async def update_async(
if sso_jit_provisioning is not None:
data["sso_jit_provisioning"] = sso_jit_provisioning
if sso_jit_provisioning_allowed_connections is not None:
data[
"sso_jit_provisioning_allowed_connections"
] = sso_jit_provisioning_allowed_connections
data["sso_jit_provisioning_allowed_connections"] = (
sso_jit_provisioning_allowed_connections
)
if email_allowed_domains is not None:
data["email_allowed_domains"] = email_allowed_domains
if email_jit_provisioning is not None:
Expand Down
6 changes: 6 additions & 0 deletions stytch/b2b/api/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

from stytch.b2b.api.otp_email import Email
from stytch.b2b.api.otp_sms import Sms
from stytch.core.api_base import ApiBase
from stytch.core.http.client import AsyncClient, SyncClient
Expand All @@ -23,3 +24,8 @@ def __init__(
sync_client=self.sync_client,
async_client=self.async_client,
)
self.email = Email(
api_base=self.api_base,
sync_client=self.sync_client,
async_client=self.async_client,
)
263 changes: 263 additions & 0 deletions stytch/b2b/api/otp_email.py

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions stytch/b2b/api/otp_email_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# !!!
# WARNING: This file is autogenerated
# Only modify code within MANUAL() sections
# or your changes may be overwritten later!
# !!!

from __future__ import annotations

from typing import Any, Dict, Optional, Union

from stytch.b2b.models.otp_email_discovery import (
AuthenticateResponse,
SendRequestLocale,
SendResponse,
)
from stytch.core.api_base import ApiBase
from stytch.core.http.client import AsyncClient, SyncClient


class Discovery:
def __init__(
self, api_base: ApiBase, sync_client: SyncClient, async_client: AsyncClient
) -> None:
self.api_base = api_base
self.sync_client = sync_client
self.async_client = async_client

def send(
self,
email_address: str,
login_template_id: Optional[str] = None,
locale: Optional[Union[SendRequestLocale, str]] = None,
) -> SendResponse:
"""Send a discovery OTP to an email address. The OTP is valid for 10 minutes. Only the most recently sent OTP is valid: when an OTP is sent, all OTPs previously sent to the same email address are invalidated, even if unused or unexpired.
Fields:
- email_address: The email address to start the discovery flow for.
- login_template_id: Use a custom template for login emails. By default, it will use your default email template. The template must be a template using our built-in customizations or a custom HTML email for OTP - Login.
- locale: Used to determine which language to use when sending the user this delivery method. Parameter is a [IETF BCP 47 language tag](https://www.w3.org/International/articles/language-tags/), e.g. `"en"`.
Currently supported languages are English (`"en"`), Spanish (`"es"`), and Brazilian Portuguese (`"pt-br"`); if no value is provided, the copy defaults to English.
Request support for additional languages [here](https://docs.google.com/forms/d/e/1FAIpQLScZSpAu_m2AmLXRT3F3kap-s_mcV6UTBitYn6CdyWP0-o7YjQ/viewform?usp=sf_link")!
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
"email_address": email_address,
}
if login_template_id is not None:
data["login_template_id"] = login_template_id
if locale is not None:
data["locale"] = locale

url = self.api_base.url_for("/v1/b2b/otps/email/discovery/send", data)
res = self.sync_client.post(url, data, headers)
return SendResponse.from_json(res.response.status_code, res.json)

async def send_async(
self,
email_address: str,
login_template_id: Optional[str] = None,
locale: Optional[SendRequestLocale] = None,
) -> SendResponse:
"""Send a discovery OTP to an email address. The OTP is valid for 10 minutes. Only the most recently sent OTP is valid: when an OTP is sent, all OTPs previously sent to the same email address are invalidated, even if unused or unexpired.
Fields:
- email_address: The email address to start the discovery flow for.
- login_template_id: Use a custom template for login emails. By default, it will use your default email template. The template must be a template using our built-in customizations or a custom HTML email for OTP - Login.
- locale: Used to determine which language to use when sending the user this delivery method. Parameter is a [IETF BCP 47 language tag](https://www.w3.org/International/articles/language-tags/), e.g. `"en"`.
Currently supported languages are English (`"en"`), Spanish (`"es"`), and Brazilian Portuguese (`"pt-br"`); if no value is provided, the copy defaults to English.
Request support for additional languages [here](https://docs.google.com/forms/d/e/1FAIpQLScZSpAu_m2AmLXRT3F3kap-s_mcV6UTBitYn6CdyWP0-o7YjQ/viewform?usp=sf_link")!
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
"email_address": email_address,
}
if login_template_id is not None:
data["login_template_id"] = login_template_id
if locale is not None:
data["locale"] = locale

url = self.api_base.url_for("/v1/b2b/otps/email/discovery/send", data)
res = await self.async_client.post(url, data, headers)
return SendResponse.from_json(res.response.status, res.json)

def authenticate(
self,
email_address: str,
code: str,
) -> AuthenticateResponse:
"""Authenticates the OTP and returns an intermediate session token. Intermediate session tokens can be used for various Discovery login flows and are valid for 10 minutes.
Fields:
- email_address: The email address of the Member.
- code: The code to authenticate.
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
"email_address": email_address,
"code": code,
}

url = self.api_base.url_for("/v1/b2b/otps/email/discovery/authenticate", data)
res = self.sync_client.post(url, data, headers)
return AuthenticateResponse.from_json(res.response.status_code, res.json)

async def authenticate_async(
self,
email_address: str,
code: str,
) -> AuthenticateResponse:
"""Authenticates the OTP and returns an intermediate session token. Intermediate session tokens can be used for various Discovery login flows and are valid for 10 minutes.
Fields:
- email_address: The email address of the Member.
- code: The code to authenticate.
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
"email_address": email_address,
"code": code,
}

url = self.api_base.url_for("/v1/b2b/otps/email/discovery/authenticate", data)
res = await self.async_client.post(url, data, headers)
return AuthenticateResponse.from_json(res.response.status, res.json)
12 changes: 6 additions & 6 deletions stytch/b2b/api/passwords_discovery_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ def reset_start(
if reset_password_template_id is not None:
data["reset_password_template_id"] = reset_password_template_id
if reset_password_expiration_minutes is not None:
data[
"reset_password_expiration_minutes"
] = reset_password_expiration_minutes
data["reset_password_expiration_minutes"] = (
reset_password_expiration_minutes
)
if pkce_code_challenge is not None:
data["pkce_code_challenge"] = pkce_code_challenge
if locale is not None:
Expand Down Expand Up @@ -132,9 +132,9 @@ async def reset_start_async(
if reset_password_template_id is not None:
data["reset_password_template_id"] = reset_password_template_id
if reset_password_expiration_minutes is not None:
data[
"reset_password_expiration_minutes"
] = reset_password_expiration_minutes
data["reset_password_expiration_minutes"] = (
reset_password_expiration_minutes
)
if pkce_code_challenge is not None:
data["pkce_code_challenge"] = pkce_code_challenge
if locale is not None:
Expand Down
12 changes: 6 additions & 6 deletions stytch/b2b/api/passwords_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ def reset_start(
if reset_password_redirect_url is not None:
data["reset_password_redirect_url"] = reset_password_redirect_url
if reset_password_expiration_minutes is not None:
data[
"reset_password_expiration_minutes"
] = reset_password_expiration_minutes
data["reset_password_expiration_minutes"] = (
reset_password_expiration_minutes
)
if code_challenge is not None:
data["code_challenge"] = code_challenge
if login_redirect_url is not None:
Expand Down Expand Up @@ -138,9 +138,9 @@ async def reset_start_async(
if reset_password_redirect_url is not None:
data["reset_password_redirect_url"] = reset_password_redirect_url
if reset_password_expiration_minutes is not None:
data[
"reset_password_expiration_minutes"
] = reset_password_expiration_minutes
data["reset_password_expiration_minutes"] = (
reset_password_expiration_minutes
)
if code_challenge is not None:
data["code_challenge"] = code_challenge
if login_redirect_url is not None:
Expand Down
71 changes: 71 additions & 0 deletions stytch/b2b/models/otp_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# !!!
# WARNING: This file is autogenerated
# Only modify code within MANUAL() sections
# or your changes may be overwritten later!
# !!!

from __future__ import annotations

import enum
from typing import Optional

from stytch.b2b.models.mfa import MfaRequired
from stytch.b2b.models.organizations import Member, Organization
from stytch.b2b.models.sessions import MemberSession
from stytch.core.response_base import ResponseBase


class AuthenticateRequestLocale(str, enum.Enum):
EN = "en"
ES = "es"
PTBR = "pt-br"


class LoginOrSignupRequestLocale(str, enum.Enum):
EN = "en"
ES = "es"
PTBR = "pt-br"


class AuthenticateResponse(ResponseBase):
"""Response type for `Email.authenticate`.
Fields:
- member_id: Globally unique UUID that identifies a specific Member.
- method_id: The email or device involved in the authentication.
- organization_id: Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations on an Organization, so be sure to preserve this value.
- member: The [Member object](https://stytch.com/docs/b2b/api/member-object)
- session_token: A secret token for a given Stytch Session.
- session_jwt: The JSON Web Token (JWT) for a given Stytch Session.
- member_session: The [Session object](https://stytch.com/docs/b2b/api/session-object).
- organization: The [Organization object](https://stytch.com/docs/b2b/api/organization-object).
- intermediate_session_token: The Intermediate Session Token. This token does not necessarily belong to a specific instance of a Member, but represents a bag of factors that may be converted to a member session. The token can be used with the [OTP SMS Authenticate endpoint](https://stytch.com/docs/b2b/api/authenticate-otp-sms), [TOTP Authenticate endpoint](https://stytch.com/docs/b2b/api/authenticate-totp), or [Recovery Codes Recover endpoint](https://stytch.com/docs/b2b/api/recovery-codes-recover) to complete an MFA flow and log in to the Organization. It can also be used with the [Exchange Intermediate Session endpoint](https://stytch.com/docs/b2b/api/exchange-intermediate-session) to join a specific Organization that allows the factors represented by the intermediate session token; or the [Create Organization via Discovery endpoint](https://stytch.com/docs/b2b/api/create-organization-via-discovery) to create a new Organization and Member.
- member_authenticated: Indicates whether the Member is fully authenticated. If false, the Member needs to complete an MFA step to log in to the Organization.
- mfa_required: Information about the MFA requirements of the Organization and the Member's options for fulfilling MFA.
""" # noqa

member_id: str
method_id: str
organization_id: str
member: Member
session_token: str
session_jwt: str
member_session: MemberSession
organization: Organization
intermediate_session_token: str
member_authenticated: bool
mfa_required: Optional[MfaRequired] = None


class LoginOrSignupResponse(ResponseBase):
"""Response type for `Email.login_or_signup`.
Fields:
- member_id: Globally unique UUID that identifies a specific Member.
- member_created: A flag indicating `true` if a new Member object was created and `false` if the Member object already existed.
- member: The [Member object](https://stytch.com/docs/b2b/api/member-object)
- organization: The [Organization object](https://stytch.com/docs/b2b/api/organization-object).
""" # noqa

member_id: str
member_created: bool
member: Member
organization: Organization
49 changes: 49 additions & 0 deletions stytch/b2b/models/otp_email_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# !!!
# WARNING: This file is autogenerated
# Only modify code within MANUAL() sections
# or your changes may be overwritten later!
# !!!

from __future__ import annotations

import enum
from typing import List

from stytch.b2b.models.discovery import DiscoveredOrganization
from stytch.core.response_base import ResponseBase


class SendRequestLocale(str, enum.Enum):
EN = "en"
ES = "es"
PTBR = "pt-br"


class AuthenticateResponse(ResponseBase):
"""Response type for `Discovery.authenticate`.
Fields:
- intermediate_session_token: The Intermediate Session Token. This token does not necessarily belong to a specific instance of a Member, but represents a bag of factors that may be converted to a member session. The token can be used with the [OTP SMS Authenticate endpoint](https://stytch.com/docs/b2b/api/authenticate-otp-sms), [TOTP Authenticate endpoint](https://stytch.com/docs/b2b/api/authenticate-totp), or [Recovery Codes Recover endpoint](https://stytch.com/docs/b2b/api/recovery-codes-recover) to complete an MFA flow and log in to the Organization. It can also be used with the [Exchange Intermediate Session endpoint](https://stytch.com/docs/b2b/api/exchange-intermediate-session) to join a specific Organization that allows the factors represented by the intermediate session token; or the [Create Organization via Discovery endpoint](https://stytch.com/docs/b2b/api/create-organization-via-discovery) to create a new Organization and Member.
- email_address: The email address.
- discovered_organizations: An array of `discovered_organization` objects tied to the `intermediate_session_token`, `session_token`, or `session_jwt`. See the [Discovered Organization Object](https://stytch.com/docs/b2b/api/discovered-organization-object) for complete details.
Note that Organizations will only appear here under any of the following conditions:
1. The end user is already a Member of the Organization.
2. The end user is invited to the Organization.
3. The end user can join the Organization because:
a) The Organization allows JIT provisioning.
b) The Organizations' allowed domains list contains the Member's email domain.
c) The Organization has at least one other Member with a verified email address with the same domain as the end user (to prevent phishing attacks).
""" # noqa

intermediate_session_token: str
email_address: str
discovered_organizations: List[DiscoveredOrganization]


class SendResponse(ResponseBase):
"""Response type for `Discovery.send`.
Fields:
""" # noqa
12 changes: 6 additions & 6 deletions stytch/consumer/api/passwords_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def reset_start(
if reset_password_redirect_url is not None:
data["reset_password_redirect_url"] = reset_password_redirect_url
if reset_password_expiration_minutes is not None:
data[
"reset_password_expiration_minutes"
] = reset_password_expiration_minutes
data["reset_password_expiration_minutes"] = (
reset_password_expiration_minutes
)
if code_challenge is not None:
data["code_challenge"] = code_challenge
if attributes is not None:
Expand Down Expand Up @@ -133,9 +133,9 @@ async def reset_start_async(
if reset_password_redirect_url is not None:
data["reset_password_redirect_url"] = reset_password_redirect_url
if reset_password_expiration_minutes is not None:
data[
"reset_password_expiration_minutes"
] = reset_password_expiration_minutes
data["reset_password_expiration_minutes"] = (
reset_password_expiration_minutes
)
if code_challenge is not None:
data["code_challenge"] = code_challenge
if attributes is not None:
Expand Down
Loading

0 comments on commit 583c55f

Please sign in to comment.