Skip to content

Commit

Permalink
Add Sign in With Ethereum (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
jennifer-stytch authored Aug 1, 2024
1 parent 7aeefd1 commit b1c0bc5
Show file tree
Hide file tree
Showing 15 changed files with 194 additions and 55 deletions.
12 changes: 6 additions & 6 deletions stytch/b2b/api/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,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 @@ -633,9 +633,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
4 changes: 2 additions & 2 deletions stytch/b2b/api/organizations_members.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ def unlink_retired_email(
Member's primary email address and the old primary email address is retired.
A retired email address cannot be used by other Members in the same Organization. However, unlinking retired email
addresses allows then to be subsequently re-used by other Organization Members. Retired email addresses can be viewed
addresses allows them to be subsequently re-used by other Organization Members. Retired email addresses can be viewed
on the [Member object](https://stytch.com/docs/b2b/api/member-object).
%}
Expand Down Expand Up @@ -707,7 +707,7 @@ async def unlink_retired_email_async(
Member's primary email address and the old primary email address is retired.
A retired email address cannot be used by other Members in the same Organization. However, unlinking retired email
addresses allows then to be subsequently re-used by other Organization Members. Retired email addresses can be viewed
addresses allows them to be subsequently re-used by other Organization Members. Retired email addresses can be viewed
on the [Member object](https://stytch.com/docs/b2b/api/member-object).
%}
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 @@ -73,9 +73,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 @@ -136,9 +136,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
4 changes: 2 additions & 2 deletions stytch/b2b/api/scim_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def update(
- connection_id: The ID of the SCIM connection.
- display_name: A human-readable display name for the connection.
- identity_provider: (no documentation yet)
- scim_group_implicit_role_assignments: (no documentation yet)
- scim_group_implicit_role_assignments: An array of SCIM group implicit role assignments. Each object in the array must contain a `group` and a `role_id`.
""" # noqa
headers: Dict[str, str] = {}
if method_options is not None:
Expand Down Expand Up @@ -102,7 +102,7 @@ async def update_async(
- connection_id: The ID of the SCIM connection.
- display_name: A human-readable display name for the connection.
- identity_provider: (no documentation yet)
- scim_group_implicit_role_assignments: (no documentation yet)
- scim_group_implicit_role_assignments: An array of SCIM group implicit role assignments. Each object in the array must contain a `group` and a `role_id`.
""" # noqa
headers: Dict[str, str] = {}
if method_options is not None:
Expand Down
18 changes: 17 additions & 1 deletion stytch/b2b/models/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@ class ResultsMetadata(pydantic.BaseModel):


class RetiredEmail(pydantic.BaseModel):
"""
Fields:
- email_id: The globally unique UUID of a Member's email.
- email_address: The email address of the Member.
""" # noqa

email_id: str
email_address: str

Expand Down Expand Up @@ -328,7 +334,17 @@ class Member(pydantic.BaseModel):
who create an Organization through the [discovery flow](https://stytch.com/docs/b2b/api/create-organization-via-discovery). See the
[RBAC guide](https://stytch.com/docs/b2b/guides/rbac/stytch-default) for more details on this Role.
- totp_registration_id: (no documentation yet)
- retired_email_addresses: (no documentation yet)
- retired_email_addresses:
A list of retired email addresses for this member.
A previously active email address can be marked as retired in one of two ways:
- It's replaced with a new primary email address during an explicit Member update.
- A new email address is surfaced by an OAuth, SAML or OIDC provider. In this case the new email address becomes the
Member's primary email address and the old primary email address is retired.
A retired email address cannot be used by other Members in the same Organization. However, unlinking retired email
addresses allows them to be subsequently re-used by other Organization Members. Retired email addresses can be unlinked
using the [Unlink Retired Email endpoint](https://stytch.com/docs/b2b/api/unlink-retired-member-email).
- mfa_enrolled: Sets whether the Member is enrolled in MFA. If true, the Member must complete an MFA step whenever they wish to log in to their Organization. If false, the Member only needs to complete an MFA step if the Organization's MFA policy is set to `REQUIRED_FOR_ALL`.
- mfa_phone_number: The Member's phone number. A Member may only have one phone number.
- default_mfa_method: (no documentation yet)
Expand Down
7 changes: 7 additions & 0 deletions stytch/b2b/models/scim.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ class SCIMGroup(pydantic.BaseModel):


class SCIMGroupImplicitRoleAssignments(pydantic.BaseModel):
"""
Fields:
- role_id: The ID of the role.
- group_id: (no documentation yet)
- group_name: (no documentation yet)
""" # noqa

role_id: str
group_id: str
group_name: str
Expand Down
29 changes: 26 additions & 3 deletions stytch/consumer/api/crypto_wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

from __future__ import annotations

from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union

from stytch.consumer.models.crypto_wallets import (
AuthenticateResponse,
AuthenticateStartResponse,
SIWEParams,
)
from stytch.core.api_base import ApiBase
from stytch.core.http.client import AsyncClient, SyncClient
Expand All @@ -31,15 +32,22 @@ def authenticate_start(
user_id: Optional[str] = None,
session_token: Optional[str] = None,
session_jwt: Optional[str] = None,
siwe_params: Optional[Union[SIWEParams, Dict[str, Any]]] = None,
) -> AuthenticateStartResponse:
"""Initiate the authentication of a crypto wallet. After calling this endpoint, the user will need to sign a message containing only the returned `challenge` field.
"""Initiate the authentication of a crypto wallet. After calling this endpoint, the user will need to sign a message containing the returned `challenge` field.
For Ethereum crypto wallets, you can optionally use the Sign In With Ethereum (SIWE) protocol for the message by passing in the `siwe_params`. The only required fields are `domain` and `uri`.
If the crypto wallet detects that the domain in the message does not match the website's domain, it will display a warning to the user.
If not using the SIWE protocol, the message will simply consist of the project name and a random string.
Fields:
- crypto_wallet_type: The type of wallet to authenticate. Currently `ethereum` and `solana` are supported. Wallets for any EVM-compatible chains (such as Polygon or BSC) are also supported and are grouped under the `ethereum` type.
- crypto_wallet_address: The crypto wallet address to authenticate.
- user_id: The unique ID of a specific User.
- session_token: The `session_token` associated with a User's existing Session.
- session_jwt: The `session_jwt` associated with a User's existing Session.
- siwe_params: The parameters for a Sign In With Ethereum (SIWE) message. May only be passed if the `crypto_wallet_type` is `ethereum`.
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
Expand All @@ -52,6 +60,10 @@ def authenticate_start(
data["session_token"] = session_token
if session_jwt is not None:
data["session_jwt"] = session_jwt
if siwe_params is not None:
data["siwe_params"] = (
siwe_params if isinstance(siwe_params, dict) else siwe_params.dict()
)

url = self.api_base.url_for("/v1/crypto_wallets/authenticate/start", data)
res = self.sync_client.post(url, data, headers)
Expand All @@ -64,15 +76,22 @@ async def authenticate_start_async(
user_id: Optional[str] = None,
session_token: Optional[str] = None,
session_jwt: Optional[str] = None,
siwe_params: Optional[SIWEParams] = None,
) -> AuthenticateStartResponse:
"""Initiate the authentication of a crypto wallet. After calling this endpoint, the user will need to sign a message containing only the returned `challenge` field.
"""Initiate the authentication of a crypto wallet. After calling this endpoint, the user will need to sign a message containing the returned `challenge` field.
For Ethereum crypto wallets, you can optionally use the Sign In With Ethereum (SIWE) protocol for the message by passing in the `siwe_params`. The only required fields are `domain` and `uri`.
If the crypto wallet detects that the domain in the message does not match the website's domain, it will display a warning to the user.
If not using the SIWE protocol, the message will simply consist of the project name and a random string.
Fields:
- crypto_wallet_type: The type of wallet to authenticate. Currently `ethereum` and `solana` are supported. Wallets for any EVM-compatible chains (such as Polygon or BSC) are also supported and are grouped under the `ethereum` type.
- crypto_wallet_address: The crypto wallet address to authenticate.
- user_id: The unique ID of a specific User.
- session_token: The `session_token` associated with a User's existing Session.
- session_jwt: The `session_jwt` associated with a User's existing Session.
- siwe_params: The parameters for a Sign In With Ethereum (SIWE) message. May only be passed if the `crypto_wallet_type` is `ethereum`.
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
Expand All @@ -85,6 +104,10 @@ async def authenticate_start_async(
data["session_token"] = session_token
if session_jwt is not None:
data["session_jwt"] = session_jwt
if siwe_params is not None:
data["siwe_params"] = (
siwe_params if isinstance(siwe_params, dict) else siwe_params.dict()
)

url = self.api_base.url_for("/v1/crypto_wallets/authenticate/start", data)
res = await self.async_client.post(url, data, headers)
Expand Down
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
34 changes: 34 additions & 0 deletions stytch/consumer/api/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,23 @@ def migrate(
session_duration_minutes: Optional[int] = None,
session_custom_claims: Optional[Dict[str, Any]] = None,
) -> MigrateResponse:
"""Migrate a session from an external OIDC compliant endpoint. Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](/dashboard), and then perform a lookup using the `session_token`. If the response contains a valid email address, Stytch will attempt to match that email address with an existing User and create a Stytch Session. You will need to create the user before using this endpoint.
Fields:
- session_token: The `session_token` associated with a User's existing Session.
- session_duration_minutes: Set the session lifetime to be this many minutes from now. This will start a new session if one doesn't already exist,
returning both an opaque `session_token` and `session_jwt` for this session. Remember that the `session_jwt` will have a fixed lifetime of
five minutes regardless of the underlying session duration, and will need to be refreshed over time.
This value must be a minimum of 5 and a maximum of 527040 minutes (366 days).
If a `session_token` or `session_jwt` is provided then a successful authentication will continue to extend the session this many minutes.
If the `session_duration_minutes` parameter is not specified, a Stytch session will not be created.
- session_custom_claims: Add a custom claims map to the Session being authenticated. Claims are only created if a Session is initialized by providing a value in `session_duration_minutes`. Claims will be included on the Session object and in the JWT. To update a key in an existing Session, supply a new value. To delete a key, supply a null value.
Custom claims made with reserved claims ("iss", "sub", "aud", "exp", "nbf", "iat", "jti") will be ignored. Total custom claims size cannot exceed four kilobytes.
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
"session_token": session_token,
Expand All @@ -220,6 +237,23 @@ async def migrate_async(
session_duration_minutes: Optional[int] = None,
session_custom_claims: Optional[Dict[str, Any]] = None,
) -> MigrateResponse:
"""Migrate a session from an external OIDC compliant endpoint. Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](/dashboard), and then perform a lookup using the `session_token`. If the response contains a valid email address, Stytch will attempt to match that email address with an existing User and create a Stytch Session. You will need to create the user before using this endpoint.
Fields:
- session_token: The `session_token` associated with a User's existing Session.
- session_duration_minutes: Set the session lifetime to be this many minutes from now. This will start a new session if one doesn't already exist,
returning both an opaque `session_token` and `session_jwt` for this session. Remember that the `session_jwt` will have a fixed lifetime of
five minutes regardless of the underlying session duration, and will need to be refreshed over time.
This value must be a minimum of 5 and a maximum of 527040 minutes (366 days).
If a `session_token` or `session_jwt` is provided then a successful authentication will continue to extend the session this many minutes.
If the `session_duration_minutes` parameter is not specified, a Stytch session will not be created.
- session_custom_claims: Add a custom claims map to the Session being authenticated. Claims are only created if a Session is initialized by providing a value in `session_duration_minutes`. Claims will be included on the Session object and in the JWT. To update a key in an existing Session, supply a new value. To delete a key, supply a null value.
Custom claims made with reserved claims ("iss", "sub", "aud", "exp", "nbf", "iat", "jti") will be ignored. Total custom claims size cannot exceed four kilobytes.
""" # noqa
headers: Dict[str, str] = {}
data: Dict[str, Any] = {
"session_token": session_token,
Expand Down
24 changes: 12 additions & 12 deletions stytch/consumer/api/webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def register_start(
if authenticator_type is not None:
data["authenticator_type"] = authenticator_type
if return_passkey_credential_options is not None:
data[
"return_passkey_credential_options"
] = return_passkey_credential_options
data["return_passkey_credential_options"] = (
return_passkey_credential_options
)

url = self.api_base.url_for("/v1/webauthn/register/start", data)
res = self.sync_client.post(url, data, headers)
Expand Down Expand Up @@ -103,9 +103,9 @@ async def register_start_async(
if authenticator_type is not None:
data["authenticator_type"] = authenticator_type
if return_passkey_credential_options is not None:
data[
"return_passkey_credential_options"
] = return_passkey_credential_options
data["return_passkey_credential_options"] = (
return_passkey_credential_options
)

url = self.api_base.url_for("/v1/webauthn/register/start", data)
res = await self.async_client.post(url, data, headers)
Expand Down Expand Up @@ -236,9 +236,9 @@ def authenticate_start(
if user_id is not None:
data["user_id"] = user_id
if return_passkey_credential_options is not None:
data[
"return_passkey_credential_options"
] = return_passkey_credential_options
data["return_passkey_credential_options"] = (
return_passkey_credential_options
)

url = self.api_base.url_for("/v1/webauthn/authenticate/start", data)
res = self.sync_client.post(url, data, headers)
Expand Down Expand Up @@ -271,9 +271,9 @@ async def authenticate_start_async(
if user_id is not None:
data["user_id"] = user_id
if return_passkey_credential_options is not None:
data[
"return_passkey_credential_options"
] = return_passkey_credential_options
data["return_passkey_credential_options"] = (
return_passkey_credential_options
)

url = self.api_base.url_for("/v1/webauthn/authenticate/start", data)
res = await self.async_client.post(url, data, headers)
Expand Down
Loading

0 comments on commit b1c0bc5

Please sign in to comment.