From 1736e5115efd78d361f7e53caf5609a1e24902ca Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 21 Sep 2023 15:57:56 +0530 Subject: [PATCH 1/5] feat: Add Twitter provider --- .../recipe/thirdparty/api/implementation.py | 2 +- .../recipe/thirdparty/provider.py | 10 +- .../thirdparty/providers/config_utils.py | 6 +- .../recipe/thirdparty/providers/twitter.py | 101 ++++++++++++++++++ 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 supertokens_python/recipe/thirdparty/providers/twitter.py diff --git a/supertokens_python/recipe/thirdparty/api/implementation.py b/supertokens_python/recipe/thirdparty/api/implementation.py index 803b67e4a..2a79dce1f 100644 --- a/supertokens_python/recipe/thirdparty/api/implementation.py +++ b/supertokens_python/recipe/thirdparty/api/implementation.py @@ -85,7 +85,7 @@ async def sign_in_up_post( user_context=user_context, ) - if user_info.email is None and not provider.config.require_email: + if user_info.email is None and provider.config.require_email is False: if provider.config.generate_fake_email is not None: user_info.email = UserInfoEmail( email=await provider.config.generate_fake_email( diff --git a/supertokens_python/recipe/thirdparty/provider.py b/supertokens_python/recipe/thirdparty/provider.py index fe37d2fb6..aa13b32d6 100644 --- a/supertokens_python/recipe/thirdparty/provider.py +++ b/supertokens_python/recipe/thirdparty/provider.py @@ -89,7 +89,7 @@ def __init__( client_secret: Optional[str] = None, client_type: Optional[str] = None, scope: Optional[List[str]] = None, - force_pkce: bool = False, + force_pkce: Optional[bool] = None, additional_config: Optional[Dict[str, Any]] = None, ): self.client_id = client_id @@ -166,7 +166,7 @@ def __init__( jwks_uri: Optional[str] = None, oidc_discovery_endpoint: Optional[str] = None, user_info_map: Optional[UserInfoMap] = None, - require_email: bool = True, + require_email: Optional[bool] = None, validate_id_token_payload: Optional[ Callable[ [Dict[str, Any], ProviderConfigForClient, Dict[str, Any]], @@ -223,7 +223,7 @@ def __init__( client_secret: Optional[str] = None, client_type: Optional[str] = None, scope: Optional[List[str]] = None, - force_pkce: bool = False, + force_pkce: Optional[bool] = None, additional_config: Optional[Dict[str, Any]] = None, # CommonProviderConfig: third_party_id: str = "temp", @@ -240,7 +240,7 @@ def __init__( jwks_uri: Optional[str] = None, oidc_discovery_endpoint: Optional[str] = None, user_info_map: Optional[UserInfoMap] = None, - require_email: bool = True, + require_email: Optional[bool] = None, validate_id_token_payload: Optional[ Callable[ [Dict[str, Any], ProviderConfigForClient, Dict[str, Any]], @@ -303,7 +303,7 @@ def __init__( jwks_uri: Optional[str] = None, oidc_discovery_endpoint: Optional[str] = None, user_info_map: Optional[UserInfoMap] = None, - require_email: bool = True, + require_email: Optional[bool] = None, validate_id_token_payload: Optional[ Callable[ [Dict[str, Any], ProviderConfigForClient, Dict[str, Any]], diff --git a/supertokens_python/recipe/thirdparty/providers/config_utils.py b/supertokens_python/recipe/thirdparty/providers/config_utils.py index fe6993bb6..c6da41972 100644 --- a/supertokens_python/recipe/thirdparty/providers/config_utils.py +++ b/supertokens_python/recipe/thirdparty/providers/config_utils.py @@ -82,11 +82,7 @@ def merge_config( if config_from_core.oidc_discovery_endpoint is None else config_from_core.oidc_discovery_endpoint ), - require_email=( - config_from_static.require_email - if config_from_core.require_email is None - else config_from_core.require_email - ), + require_email=config_from_static.require_email, user_info_map=config_from_static.user_info_map, generate_fake_email=config_from_static.generate_fake_email, validate_id_token_payload=config_from_static.validate_id_token_payload, diff --git a/supertokens_python/recipe/thirdparty/providers/twitter.py b/supertokens_python/recipe/thirdparty/providers/twitter.py new file mode 100644 index 000000000..d49ed47bc --- /dev/null +++ b/supertokens_python/recipe/thirdparty/providers/twitter.py @@ -0,0 +1,101 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from base64 import b64encode +from typing import Any, Dict, Optional +from supertokens_python.recipe.thirdparty.provider import RedirectUriInfo +from supertokens_python.recipe.thirdparty.providers.utils import do_post_request +from ..provider import ( + Provider, + ProviderConfigForClient, + ProviderInput, + UserFields, + UserInfoMap, +) + +from .custom import ( + GenericProvider, + NewProvider, +) + + +class TwitterImpl(GenericProvider): + async def get_config_for_client_type( + self, client_type: Optional[str], user_context: Dict[str, Any] + ) -> ProviderConfigForClient: + config = await super().get_config_for_client_type(client_type, user_context) + + if config.scope is None: + config.scope = ["users.read", "tweet.read"] + + if config.force_pkce is None: + config.force_pkce = True + + return config + + async def exchange_auth_code_for_oauth_tokens( + self, redirect_uri_info: RedirectUriInfo, user_context: Dict[str, Any] + ) -> Dict[str, Any]: + credentials = self.config.client_id + ":" + (self.config.client_secret or "") + auth_token = b64encode(credentials.encode()).decode() + + twitter_oauth_tokens_params: Dict[str, Any] = { + "grant_type": "authorization_code", + "client_id": self.config.client_id, + "code_verifier": redirect_uri_info.pkce_code_verifier, + "redirect_uri": redirect_uri_info.redirect_uri_on_provider_dashboard, + "code": redirect_uri_info.redirect_uri_query_params["code"], + } + + twitter_oauth_tokens_params = { + **twitter_oauth_tokens_params, + **(self.config.token_endpoint_body_params or {}), + } + + assert self.config.token_endpoint is not None + + return await do_post_request( + self.config.token_endpoint, + body_params=twitter_oauth_tokens_params, + headers={"Authorization": f"Basic {auth_token}"}, + ) + + +def Twitter(input: ProviderInput) -> Provider: # pylint: disable=redefined-builtin + if input.config.name is None: + input.config.name = "Twitter" + + if input.config.authorization_endpoint is None: + input.config.authorization_endpoint = "https://twitter.com/i/oauth2/authorize" + + if input.config.token_endpoint is None: + input.config.token_endpoint = "https://api.twitter.com/2/oauth2/token" + + if input.config.user_info_endpoint is None: + input.config.user_info_endpoint = "https://api.twitter.com/2/users/me" + + if input.config.require_email is None: + input.config.require_email = True + + if input.config.user_info_map is None: + input.config.user_info_map = UserInfoMap(UserFields(), UserFields()) + + if input.config.user_info_map.from_user_info_api is None: + input.config.user_info_map.from_user_info_api = UserFields() + + if input.config.user_info_map.from_user_info_api.user_id is None: + input.config.user_info_map.from_user_info_api.user_id = "data.id" + + return NewProvider(input, TwitterImpl) From b20d8abc8e6d693858965c90607a3eca51cfdf8d Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 21 Sep 2023 16:13:49 +0530 Subject: [PATCH 2/5] refactor: Use Twitter provider when thirdparty id starts with twitter --- supertokens_python/recipe/thirdparty/providers/config_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supertokens_python/recipe/thirdparty/providers/config_utils.py b/supertokens_python/recipe/thirdparty/providers/config_utils.py index c6da41972..d520106aa 100644 --- a/supertokens_python/recipe/thirdparty/providers/config_utils.py +++ b/supertokens_python/recipe/thirdparty/providers/config_utils.py @@ -13,6 +13,7 @@ from .google_workspaces import GoogleWorkspaces from .google import Google from .linkedin import Linkedin +from .twitter import Twitter from .okta import Okta from .custom import NewProvider from .utils import do_get_request @@ -202,6 +203,8 @@ def create_provider(provider_input: ProviderInput) -> Provider: return Okta(provider_input) if provider_input.config.third_party_id.startswith("linkedin"): return Linkedin(provider_input) + if provider_input.config.third_party_id.startswith("twitter"): + return Twitter(provider_input) if provider_input.config.third_party_id.startswith("boxy-saml"): return BoxySAML(provider_input) From ead8406d805aba06fd2fec7481c6eb3dc1818431 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Fri, 22 Sep 2023 12:40:42 +0530 Subject: [PATCH 3/5] fix: Twitter provider signup issue --- supertokens_python/recipe/thirdparty/api/implementation.py | 2 ++ supertokens_python/recipe/thirdparty/providers/twitter.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/supertokens_python/recipe/thirdparty/api/implementation.py b/supertokens_python/recipe/thirdparty/api/implementation.py index 2a79dce1f..6bf0a7c8c 100644 --- a/supertokens_python/recipe/thirdparty/api/implementation.py +++ b/supertokens_python/recipe/thirdparty/api/implementation.py @@ -86,6 +86,8 @@ async def sign_in_up_post( ) if user_info.email is None and provider.config.require_email is False: + # We don't expect to get an email from this provider. + # So we generate a fake one if provider.config.generate_fake_email is not None: user_info.email = UserInfoEmail( email=await provider.config.generate_fake_email( diff --git a/supertokens_python/recipe/thirdparty/providers/twitter.py b/supertokens_python/recipe/thirdparty/providers/twitter.py index d49ed47bc..bace8f67e 100644 --- a/supertokens_python/recipe/thirdparty/providers/twitter.py +++ b/supertokens_python/recipe/thirdparty/providers/twitter.py @@ -87,7 +87,7 @@ def Twitter(input: ProviderInput) -> Provider: # pylint: disable=redefined-buil input.config.user_info_endpoint = "https://api.twitter.com/2/users/me" if input.config.require_email is None: - input.config.require_email = True + input.config.require_email = False if input.config.user_info_map is None: input.config.user_info_map = UserInfoMap(UserFields(), UserFields()) From 23d96f3d7ee4362b74718d2eaabdfe5824947c63 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Fri, 22 Sep 2023 12:45:42 +0530 Subject: [PATCH 4/5] chores: Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 461fea709..a2b23fa55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Add Twitter provider for thirdparty login + ## [0.16.1] - 2023-09-19 - Handle AWS Public URLs (ending with `.amazonaws.com`) separately while extracting TLDs for SameSite attribute. From 236266cda61f09351eb6924e07f9157b88194a1f Mon Sep 17 00:00:00 2001 From: Mayank Thakur Date: Thu, 28 Sep 2023 18:57:44 +0530 Subject: [PATCH 5/5] fix: twitter provider, handle dev keys --- .../recipe/thirdparty/providers/custom.py | 7 +++++- .../recipe/thirdparty/providers/twitter.py | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/supertokens_python/recipe/thirdparty/providers/custom.py b/supertokens_python/recipe/thirdparty/providers/custom.py index e7c7fd9a4..f21846ba6 100644 --- a/supertokens_python/recipe/thirdparty/providers/custom.py +++ b/supertokens_python/recipe/thirdparty/providers/custom.py @@ -14,7 +14,7 @@ do_get_request, do_post_request, get_actual_client_id_from_development_client_id, - is_using_oauth_development_client_id, + is_using_oauth_development_client_id, DEV_KEY_IDENTIFIER, DEV_OAUTH_CLIENT_IDS, ) from ..types import RawUserInfoFromProvider, UserInfo, UserInfoEmail @@ -180,6 +180,11 @@ def merge_into_dict(src: Dict[str, Any], dest: Dict[str, Any]) -> Dict[str, Any] return res +def is_using_development_client_id(client_id): + return client_id.startswith(DEV_KEY_IDENTIFIER) or client_id in DEV_OAUTH_CLIENT_IDS + + + class GenericProvider(Provider): def __init__(self, provider_config: ProviderConfig): self.input_config = input_config = self._normalize_input(provider_config) diff --git a/supertokens_python/recipe/thirdparty/providers/twitter.py b/supertokens_python/recipe/thirdparty/providers/twitter.py index bace8f67e..6d7c987c5 100644 --- a/supertokens_python/recipe/thirdparty/providers/twitter.py +++ b/supertokens_python/recipe/thirdparty/providers/twitter.py @@ -16,7 +16,8 @@ from base64 import b64encode from typing import Any, Dict, Optional from supertokens_python.recipe.thirdparty.provider import RedirectUriInfo -from supertokens_python.recipe.thirdparty.providers.utils import do_post_request +from supertokens_python.recipe.thirdparty.providers.utils import do_post_request, DEV_OAUTH_REDIRECT_URL, \ + get_actual_client_id_from_development_client_id from ..provider import ( Provider, ProviderConfigForClient, @@ -27,7 +28,7 @@ from .custom import ( GenericProvider, - NewProvider, + NewProvider, is_using_development_client_id, ) @@ -48,14 +49,25 @@ async def get_config_for_client_type( async def exchange_auth_code_for_oauth_tokens( self, redirect_uri_info: RedirectUriInfo, user_context: Dict[str, Any] ) -> Dict[str, Any]: - credentials = self.config.client_id + ":" + (self.config.client_secret or "") + + client_id = self.config.client_id + redirect_uri = redirect_uri_info.redirect_uri_on_provider_dashboard + + # We need to do this because we don't call the original implementation + # Transformation needed for dev keys BEGIN + if is_using_development_client_id(self.config.client_id): + client_id = get_actual_client_id_from_development_client_id(self.config.client_id) + redirect_uri = DEV_OAUTH_REDIRECT_URL + # Transformation needed for dev keys END + + credentials = client_id + ":" + (self.config.client_secret or "") auth_token = b64encode(credentials.encode()).decode() twitter_oauth_tokens_params: Dict[str, Any] = { "grant_type": "authorization_code", - "client_id": self.config.client_id, + "client_id": client_id, "code_verifier": redirect_uri_info.pkce_code_verifier, - "redirect_uri": redirect_uri_info.redirect_uri_on_provider_dashboard, + "redirect_uri": redirect_uri, "code": redirect_uri_info.redirect_uri_query_params["code"], }