From f9487c992880af6cb97282db3f7316e73f1a4904 Mon Sep 17 00:00:00 2001 From: vincent <107003653+vincent-stytch@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:34:53 -0700 Subject: [PATCH] OIDC improvements (#220) * OIDC improvements * bump version --- stytch/b2b/api/organizations_members.py | 43 ++++++++++++++++++++++ stytch/b2b/api/sso_oidc.py | 16 ++++++++ stytch/b2b/api/sso_saml.py | 4 +- stytch/b2b/models/organizations.py | 10 +++++ stytch/b2b/models/organizations_members.py | 11 +++++- stytch/b2b/models/sso.py | 2 + stytch/consumer/models/sessions.py | 7 ++++ stytch/version.py | 2 +- 8 files changed, 91 insertions(+), 4 deletions(-) diff --git a/stytch/b2b/api/organizations_members.py b/stytch/b2b/api/organizations_members.py index 9897810..7fb76a9 100644 --- a/stytch/b2b/api/organizations_members.py +++ b/stytch/b2b/api/organizations_members.py @@ -22,6 +22,7 @@ DeleteTOTPRequestOptions, DeleteTOTPResponse, GetResponse, + OIDCProvidersResponse, ReactivateRequestOptions, ReactivateResponse, SearchRequestOptions, @@ -640,6 +641,48 @@ async def dangerously_get_async( res = await self.async_client.get(url, data, headers) return GetResponse.from_json(res.response.status, res.json) + def oidc_providers( + self, + organization_id: str, + member_id: str, + include_refresh_token: Optional[bool] = None, + ) -> OIDCProvidersResponse: + headers: Dict[str, str] = {} + data: Dict[str, Any] = { + "organization_id": organization_id, + "member_id": member_id, + } + if include_refresh_token is not None: + data["include_refresh_token"] = include_refresh_token + + url = self.api_base.url_for( + "/v1/b2b/organizations/{organization_id}/members/{member_id}/oidc_providers", + data, + ) + res = self.sync_client.get(url, data, headers) + return OIDCProvidersResponse.from_json(res.response.status_code, res.json) + + async def oidc_providers_async( + self, + organization_id: str, + member_id: str, + include_refresh_token: Optional[bool] = None, + ) -> OIDCProvidersResponse: + headers: Dict[str, str] = {} + data: Dict[str, Any] = { + "organization_id": organization_id, + "member_id": member_id, + } + if include_refresh_token is not None: + data["include_refresh_token"] = include_refresh_token + + url = self.api_base.url_for( + "/v1/b2b/organizations/{organization_id}/members/{member_id}/oidc_providers", + data, + ) + res = await self.async_client.get(url, data, headers) + return OIDCProvidersResponse.from_json(res.response.status, res.json) + def unlink_retired_email( self, organization_id: str, diff --git a/stytch/b2b/api/sso_oidc.py b/stytch/b2b/api/sso_oidc.py index 3c1427c..8e47142 100644 --- a/stytch/b2b/api/sso_oidc.py +++ b/stytch/b2b/api/sso_oidc.py @@ -103,6 +103,8 @@ def update_connection( identity_provider: Optional[ Union[UpdateConnectionRequestIdentityProvider, str] ] = None, + custom_scopes: Optional[str] = None, + attribute_mapping: Optional[Dict[str, Any]] = None, method_options: Optional[UpdateConnectionRequestOptions] = None, ) -> UpdateConnectionResponse: """Updates an existing OIDC connection. @@ -136,6 +138,8 @@ def update_connection( - userinfo_url: The location of the IDP's [UserInfo Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). This will be provided by the IdP. - jwks_url: The location of the IdP's JSON Web Key Set, used to verify credentials issued by the IdP. This will be provided by the IdP. - identity_provider: The identity provider of this connection. For OIDC, the accepted values are `generic`, `okta`, and `microsoft-entra`. For SAML, the accepted values are `generic`, `okta`, `microsoft-entra`, and `google-workspace`. + - custom_scopes: Include a space-separated list of custom scopes that you'd like to include. Note that this list must be URL encoded, e.g. the spaces must be expressed as %20. + - attribute_mapping: An object that represents the attributes used to identify a Member. This object will map the IdP-defined User attributes to Stytch-specific values, which will appear on the member's Trusted Metadata. """ # noqa headers: Dict[str, str] = {} if method_options is not None: @@ -162,6 +166,10 @@ def update_connection( data["jwks_url"] = jwks_url if identity_provider is not None: data["identity_provider"] = identity_provider + if custom_scopes is not None: + data["custom_scopes"] = custom_scopes + if attribute_mapping is not None: + data["attribute_mapping"] = attribute_mapping url = self.api_base.url_for( "/v1/b2b/sso/oidc/{organization_id}/connections/{connection_id}", data @@ -182,6 +190,8 @@ async def update_connection_async( userinfo_url: Optional[str] = None, jwks_url: Optional[str] = None, identity_provider: Optional[UpdateConnectionRequestIdentityProvider] = None, + custom_scopes: Optional[str] = None, + attribute_mapping: Optional[Dict[str, Any]] = None, method_options: Optional[UpdateConnectionRequestOptions] = None, ) -> UpdateConnectionResponse: """Updates an existing OIDC connection. @@ -215,6 +225,8 @@ async def update_connection_async( - userinfo_url: The location of the IDP's [UserInfo Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). This will be provided by the IdP. - jwks_url: The location of the IdP's JSON Web Key Set, used to verify credentials issued by the IdP. This will be provided by the IdP. - identity_provider: The identity provider of this connection. For OIDC, the accepted values are `generic`, `okta`, and `microsoft-entra`. For SAML, the accepted values are `generic`, `okta`, `microsoft-entra`, and `google-workspace`. + - custom_scopes: Include a space-separated list of custom scopes that you'd like to include. Note that this list must be URL encoded, e.g. the spaces must be expressed as %20. + - attribute_mapping: An object that represents the attributes used to identify a Member. This object will map the IdP-defined User attributes to Stytch-specific values, which will appear on the member's Trusted Metadata. """ # noqa headers: Dict[str, str] = {} if method_options is not None: @@ -241,6 +253,10 @@ async def update_connection_async( data["jwks_url"] = jwks_url if identity_provider is not None: data["identity_provider"] = identity_provider + if custom_scopes is not None: + data["custom_scopes"] = custom_scopes + if attribute_mapping is not None: + data["attribute_mapping"] = attribute_mapping url = self.api_base.url_for( "/v1/b2b/sso/oidc/{organization_id}/connections/{connection_id}", data diff --git a/stytch/b2b/api/sso_saml.py b/stytch/b2b/api/sso_saml.py index 1cd1cb8..b4d5510 100644 --- a/stytch/b2b/api/sso_saml.py +++ b/stytch/b2b/api/sso_saml.py @@ -140,7 +140,7 @@ def update_connection( [RBAC guide](https://stytch.com/docs/b2b/guides/rbac/role-assignment) for more information about role assignment. Before adding any group implicit role assignments, you must add a "groups" key to your SAML connection's `attribute_mapping`. Make sure that your IdP is configured to correctly send the group information. - - alternative_audience_uri: An alternative URL to use for the Audience Restriction. This value can be used when you wish to migrate an existing SAML integration to Stytch with zero downtime. + - alternative_audience_uri: An alternative URL to use for the Audience Restriction. This value can be used when you wish to migrate an existing SAML integration to Stytch with zero downtime. Read our [SSO migration guide](https://stytch.com/docs/b2b/guides/migrations/additional-migration-considerations) for more info. - identity_provider: The identity provider of this connection. For OIDC, the accepted values are `generic`, `okta`, and `microsoft-entra`. For SAML, the accepted values are `generic`, `okta`, `microsoft-entra`, and `google-workspace`. """ # noqa headers: Dict[str, str] = {} @@ -223,7 +223,7 @@ async def update_connection_async( [RBAC guide](https://stytch.com/docs/b2b/guides/rbac/role-assignment) for more information about role assignment. Before adding any group implicit role assignments, you must add a "groups" key to your SAML connection's `attribute_mapping`. Make sure that your IdP is configured to correctly send the group information. - - alternative_audience_uri: An alternative URL to use for the Audience Restriction. This value can be used when you wish to migrate an existing SAML integration to Stytch with zero downtime. + - alternative_audience_uri: An alternative URL to use for the Audience Restriction. This value can be used when you wish to migrate an existing SAML integration to Stytch with zero downtime. Read our [SSO migration guide](https://stytch.com/docs/b2b/guides/migrations/additional-migration-considerations) for more info. - identity_provider: The identity provider of this connection. For OIDC, the accepted values are `generic`, `okta`, and `microsoft-entra`. For SAML, the accepted values are `generic`, `okta`, `microsoft-entra`, and `google-workspace`. """ # noqa headers: Dict[str, str] = {} diff --git a/stytch/b2b/models/organizations.py b/stytch/b2b/models/organizations.py index 85e3013..a5dd35a 100644 --- a/stytch/b2b/models/organizations.py +++ b/stytch/b2b/models/organizations.py @@ -184,6 +184,16 @@ class OAuthRegistration(pydantic.BaseModel): locale: Optional[str] = None +class OIDCProviderInfo(pydantic.BaseModel): + provider_subject: str + id_token: str + access_token: str + access_token_expires_in: int + scopes: List[str] + connection_id: str + refresh_token: Optional[str] = None + + class Organization(pydantic.BaseModel): """ Fields: diff --git a/stytch/b2b/models/organizations_members.py b/stytch/b2b/models/organizations_members.py index 23619bd..a8ed24a 100644 --- a/stytch/b2b/models/organizations_members.py +++ b/stytch/b2b/models/organizations_members.py @@ -10,7 +10,12 @@ import pydantic -from stytch.b2b.models.organizations import Member, Organization, ResultsMetadata +from stytch.b2b.models.organizations import ( + Member, + OIDCProviderInfo, + Organization, + ResultsMetadata, +) from stytch.core.response_base import ResponseBase from stytch.shared.method_options import Authorization @@ -233,6 +238,10 @@ class GetResponse(ResponseBase): organization: Organization +class OIDCProvidersResponse(ResponseBase): + registrations: List[OIDCProviderInfo] + + class ReactivateResponse(ResponseBase): """Response type for `Members.reactivate`. Fields: diff --git a/stytch/b2b/models/sso.py b/stytch/b2b/models/sso.py index 3bd68e8..31807d9 100644 --- a/stytch/b2b/models/sso.py +++ b/stytch/b2b/models/sso.py @@ -93,6 +93,8 @@ class OIDCConnection(pydantic.BaseModel): userinfo_url: str jwks_url: str identity_provider: str + custom_scopes: str + attribute_mapping: Optional[Dict[str, Any]] = None class SAMLConnectionImplicitRoleAssignment(pydantic.BaseModel): diff --git a/stytch/consumer/models/sessions.py b/stytch/consumer/models/sessions.py index 2af8d8e..330941c 100644 --- a/stytch/consumer/models/sessions.py +++ b/stytch/consumer/models/sessions.py @@ -57,6 +57,7 @@ class AuthenticationFactorDeliveryMethod(str, enum.Enum): IMPORTED_AUTH0 = "imported_auth0" OAUTH_EXCHANGE_SLACK = "oauth_exchange_slack" OAUTH_EXCHANGE_HUBSPOT = "oauth_exchange_hubspot" + OAUTH_EXCHANGE_GITHUB = "oauth_exchange_github" class AuthenticationFactorType(str, enum.Enum): @@ -155,6 +156,10 @@ class GitLabOAuthFactor(pydantic.BaseModel): email_id: Optional[str] = None +class GithubOAuthExchangeFactor(pydantic.BaseModel): + email_id: str + + class GithubOAuthFactor(pydantic.BaseModel): id: str provider_subject: str @@ -388,6 +393,7 @@ class AuthenticationFactor(pydantic.BaseModel): - hubspot_oauth_factor: (no documentation yet) - slack_oauth_exchange_factor: (no documentation yet) - hubspot_oauth_exchange_factor: (no documentation yet) + - github_oauth_exchange_factor: (no documentation yet) """ # noqa type: AuthenticationFactorType @@ -431,6 +437,7 @@ class AuthenticationFactor(pydantic.BaseModel): hubspot_oauth_factor: Optional[HubspotOAuthFactor] = None slack_oauth_exchange_factor: Optional[SlackOAuthExchangeFactor] = None hubspot_oauth_exchange_factor: Optional[HubspotOAuthExchangeFactor] = None + github_oauth_exchange_factor: Optional[GithubOAuthExchangeFactor] = None class Session(pydantic.BaseModel): diff --git a/stytch/version.py b/stytch/version.py index 427ec25..b11143b 100644 --- a/stytch/version.py +++ b/stytch/version.py @@ -1 +1 @@ -__version__ = "11.5.0" +__version__ = "11.6.0"