Skip to content

Commit 4eab8c8

Browse files
Merge pull request #452 from supertokens/feat/twitter-provider
feat: Add Twitter provider
2 parents 2bdd8b1 + 75615ef commit 4eab8c8

File tree

6 files changed

+132
-12
lines changed

6 files changed

+132
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [unreleased]
1010

11+
- Add Twitter provider for thirdparty login
1112
- Add `Cache-Control` header for jwks endpoint `/jwt/jwks.json`
1213
- Add `validity_in_secs` to the return value of overridable `get_jwks` recipe function.
1314
- This can be used to control the `Cache-Control` header mentioned above.

supertokens_python/recipe/thirdparty/api/implementation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ async def sign_in_up_post(
8585
user_context=user_context,
8686
)
8787

88-
if user_info.email is None and not provider.config.require_email:
88+
if user_info.email is None and provider.config.require_email is False:
89+
# We don't expect to get an email from this provider.
90+
# So we generate a fake one
8991
if provider.config.generate_fake_email is not None:
9092
user_info.email = UserInfoEmail(
9193
email=await provider.config.generate_fake_email(

supertokens_python/recipe/thirdparty/provider.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def __init__(
8989
client_secret: Optional[str] = None,
9090
client_type: Optional[str] = None,
9191
scope: Optional[List[str]] = None,
92-
force_pkce: bool = False,
92+
force_pkce: Optional[bool] = None,
9393
additional_config: Optional[Dict[str, Any]] = None,
9494
):
9595
self.client_id = client_id
@@ -166,7 +166,7 @@ def __init__(
166166
jwks_uri: Optional[str] = None,
167167
oidc_discovery_endpoint: Optional[str] = None,
168168
user_info_map: Optional[UserInfoMap] = None,
169-
require_email: bool = True,
169+
require_email: Optional[bool] = None,
170170
validate_id_token_payload: Optional[
171171
Callable[
172172
[Dict[str, Any], ProviderConfigForClient, Dict[str, Any]],
@@ -223,7 +223,7 @@ def __init__(
223223
client_secret: Optional[str] = None,
224224
client_type: Optional[str] = None,
225225
scope: Optional[List[str]] = None,
226-
force_pkce: bool = False,
226+
force_pkce: Optional[bool] = None,
227227
additional_config: Optional[Dict[str, Any]] = None,
228228
# CommonProviderConfig:
229229
third_party_id: str = "temp",
@@ -240,7 +240,7 @@ def __init__(
240240
jwks_uri: Optional[str] = None,
241241
oidc_discovery_endpoint: Optional[str] = None,
242242
user_info_map: Optional[UserInfoMap] = None,
243-
require_email: bool = True,
243+
require_email: Optional[bool] = None,
244244
validate_id_token_payload: Optional[
245245
Callable[
246246
[Dict[str, Any], ProviderConfigForClient, Dict[str, Any]],
@@ -303,7 +303,7 @@ def __init__(
303303
jwks_uri: Optional[str] = None,
304304
oidc_discovery_endpoint: Optional[str] = None,
305305
user_info_map: Optional[UserInfoMap] = None,
306-
require_email: bool = True,
306+
require_email: Optional[bool] = None,
307307
validate_id_token_payload: Optional[
308308
Callable[
309309
[Dict[str, Any], ProviderConfigForClient, Dict[str, Any]],

supertokens_python/recipe/thirdparty/providers/config_utils.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .google_workspaces import GoogleWorkspaces
1414
from .google import Google
1515
from .linkedin import Linkedin
16+
from .twitter import Twitter
1617
from .okta import Okta
1718
from .custom import NewProvider
1819
from .utils import do_get_request
@@ -82,11 +83,7 @@ def merge_config(
8283
if config_from_core.oidc_discovery_endpoint is None
8384
else config_from_core.oidc_discovery_endpoint
8485
),
85-
require_email=(
86-
config_from_static.require_email
87-
if config_from_core.require_email is None
88-
else config_from_core.require_email
89-
),
86+
require_email=config_from_static.require_email,
9087
user_info_map=config_from_static.user_info_map,
9188
generate_fake_email=config_from_static.generate_fake_email,
9289
validate_id_token_payload=config_from_static.validate_id_token_payload,
@@ -206,6 +203,8 @@ def create_provider(provider_input: ProviderInput) -> Provider:
206203
return Okta(provider_input)
207204
if provider_input.config.third_party_id.startswith("linkedin"):
208205
return Linkedin(provider_input)
206+
if provider_input.config.third_party_id.startswith("twitter"):
207+
return Twitter(provider_input)
209208
if provider_input.config.third_party_id.startswith("boxy-saml"):
210209
return BoxySAML(provider_input)
211210

supertokens_python/recipe/thirdparty/providers/custom.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
do_get_request,
1515
do_post_request,
1616
get_actual_client_id_from_development_client_id,
17-
is_using_oauth_development_client_id,
17+
is_using_oauth_development_client_id, DEV_KEY_IDENTIFIER, DEV_OAUTH_CLIENT_IDS,
1818
)
1919

2020
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]
180180
return res
181181

182182

183+
def is_using_development_client_id(client_id):
184+
return client_id.startswith(DEV_KEY_IDENTIFIER) or client_id in DEV_OAUTH_CLIENT_IDS
185+
186+
187+
183188
class GenericProvider(Provider):
184189
def __init__(self, provider_config: ProviderConfig):
185190
self.input_config = input_config = self._normalize_input(provider_config)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
2+
#
3+
# This software is licensed under the Apache License, Version 2.0 (the
4+
# "License") as published by the Apache Software Foundation.
5+
#
6+
# You may not use this file except in compliance with the License. You may
7+
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
from __future__ import annotations
15+
16+
from base64 import b64encode
17+
from typing import Any, Dict, Optional
18+
from supertokens_python.recipe.thirdparty.provider import RedirectUriInfo
19+
from supertokens_python.recipe.thirdparty.providers.utils import do_post_request, DEV_OAUTH_REDIRECT_URL, \
20+
get_actual_client_id_from_development_client_id
21+
from ..provider import (
22+
Provider,
23+
ProviderConfigForClient,
24+
ProviderInput,
25+
UserFields,
26+
UserInfoMap,
27+
)
28+
29+
from .custom import (
30+
GenericProvider,
31+
NewProvider, is_using_development_client_id,
32+
)
33+
34+
35+
class TwitterImpl(GenericProvider):
36+
async def get_config_for_client_type(
37+
self, client_type: Optional[str], user_context: Dict[str, Any]
38+
) -> ProviderConfigForClient:
39+
config = await super().get_config_for_client_type(client_type, user_context)
40+
41+
if config.scope is None:
42+
config.scope = ["users.read", "tweet.read"]
43+
44+
if config.force_pkce is None:
45+
config.force_pkce = True
46+
47+
return config
48+
49+
async def exchange_auth_code_for_oauth_tokens(
50+
self, redirect_uri_info: RedirectUriInfo, user_context: Dict[str, Any]
51+
) -> Dict[str, Any]:
52+
53+
client_id = self.config.client_id
54+
redirect_uri = redirect_uri_info.redirect_uri_on_provider_dashboard
55+
56+
# We need to do this because we don't call the original implementation
57+
# Transformation needed for dev keys BEGIN
58+
if is_using_development_client_id(self.config.client_id):
59+
client_id = get_actual_client_id_from_development_client_id(self.config.client_id)
60+
redirect_uri = DEV_OAUTH_REDIRECT_URL
61+
# Transformation needed for dev keys END
62+
63+
credentials = client_id + ":" + (self.config.client_secret or "")
64+
auth_token = b64encode(credentials.encode()).decode()
65+
66+
twitter_oauth_tokens_params: Dict[str, Any] = {
67+
"grant_type": "authorization_code",
68+
"client_id": client_id,
69+
"code_verifier": redirect_uri_info.pkce_code_verifier,
70+
"redirect_uri": redirect_uri,
71+
"code": redirect_uri_info.redirect_uri_query_params["code"],
72+
}
73+
74+
twitter_oauth_tokens_params = {
75+
**twitter_oauth_tokens_params,
76+
**(self.config.token_endpoint_body_params or {}),
77+
}
78+
79+
assert self.config.token_endpoint is not None
80+
81+
return await do_post_request(
82+
self.config.token_endpoint,
83+
body_params=twitter_oauth_tokens_params,
84+
headers={"Authorization": f"Basic {auth_token}"},
85+
)
86+
87+
88+
def Twitter(input: ProviderInput) -> Provider: # pylint: disable=redefined-builtin
89+
if input.config.name is None:
90+
input.config.name = "Twitter"
91+
92+
if input.config.authorization_endpoint is None:
93+
input.config.authorization_endpoint = "https://twitter.com/i/oauth2/authorize"
94+
95+
if input.config.token_endpoint is None:
96+
input.config.token_endpoint = "https://api.twitter.com/2/oauth2/token"
97+
98+
if input.config.user_info_endpoint is None:
99+
input.config.user_info_endpoint = "https://api.twitter.com/2/users/me"
100+
101+
if input.config.require_email is None:
102+
input.config.require_email = False
103+
104+
if input.config.user_info_map is None:
105+
input.config.user_info_map = UserInfoMap(UserFields(), UserFields())
106+
107+
if input.config.user_info_map.from_user_info_api is None:
108+
input.config.user_info_map.from_user_info_api = UserFields()
109+
110+
if input.config.user_info_map.from_user_info_api.user_id is None:
111+
input.config.user_info_map.from_user_info_api.user_id = "data.id"
112+
113+
return NewProvider(input, TwitterImpl)

0 commit comments

Comments
 (0)