Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d34bfa5

Browse files
authoredJun 18, 2024
Add WakaTimeAuthorizationCode and Browser display settings (#84)
1 parent 6581916 commit d34bfa5

12 files changed

+1392
-95
lines changed
 

‎CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Publicly expose `requests_auth.SupportMultiAuth`, allowing multiple authentication support for every `requests` authentication class that exists.
1111
- Publicly expose `requests_auth.TokenMemoryCache`, allowing to create custom Oauth2 token cache based on this default implementation.
1212
- Thanks to the new `redirect_uri_domain` parameter on Authorization code (with and without PKCE) and Implicit flows, you can now provide the [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) to use in the `redirect_uri` when `localhost` (the default) is not allowed.
13+
- `requests_auth.WakaTimeAuthorizationCode` handling access to the [WakaTime API](https://wakatime.com/developers).
1314

1415
### Changed
1516
- Except for `requests_auth.testing`, only direct access via `requests_auth.` was considered publicly exposed. This is now explicit, as inner packages are now using private prefix (`_`).
1617
If you were relying on some classes or functions that are now internal, feel free to open an issue.
1718
- `requests_auth.JsonTokenFileCache` and `requests_auth.TokenMemoryCache` `get_token` method does not handle kwargs anymore, the `on_missing_token` callable does not expect any arguments anymore.
1819
- `requests_auth.JsonTokenFileCache` does not expose `tokens_path` or `last_save_time` attributes anymore and is also allowing `pathlib.Path` instances as cache location.
1920
- `requests_auth.TokenMemoryCache` does not expose `forbid_concurrent_cache_access` or `forbid_concurrent_missing_token_function_call` attributes anymore.
21+
- Browser display settings have been moved to a shared setting, see documentation for more information on `requests_auth.OAuth2.display`.
22+
As a result the following classes no longer expose `success_display_time` and `failure_display_time` parameters.
23+
- `requests_auth.OAuth2AuthorizationCode`.
24+
- `requests_auth.OktaAuthorizationCode`.
25+
- `requests_auth.WakaTimeAuthorizationCode`.
26+
- `requests_auth.OAuth2AuthorizationCodePKCE`.
27+
- `requests_auth.OktaAuthorizationCodePKCE`.
28+
- `requests_auth.OAuth2Implicit`.
29+
- `requests_auth.AzureActiveDirectoryImplicit`.
30+
- `requests_auth.AzureActiveDirectoryImplicitIdToken`.
31+
- `requests_auth.OktaImplicit`.
32+
- `requests_auth.OktaImplicitIdToken`.
2033

2134
### Fixed
2235
- Type information is now provided following [PEP 561](https://www.python.org/dev/peps/pep-0561/).
2336
- Remove deprecation warnings due to usage of `utcnow` and `utcfromtimestamp`.
2437
- `requests_auth.OktaClientCredentials` `scope` parameter is now mandatory and does not default to `openid` anymore.
2538
- `requests_auth.OktaClientCredentials` will now display a more user-friendly error message in case Okta instance is not provided.
2639
- Tokens cache `DEBUG` logs will not display tokens anymore.
40+
- Handle `text/html; charset=utf-8` content-type in token responses.
2741

2842
### Removed
2943
- Removing support for Python `3.7`.

‎README.md

+111-24
Large diffs are not rendered by default.

‎pyproject.toml

+2-4
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ keywords = [
1818
"authentication",
1919
"ntlm",
2020
"oauth2",
21-
"azure-active-directory",
22-
"azure-ad",
2321
"okta",
24-
"apikey",
25-
"multiple",
22+
"aad",
23+
"entra"
2624
]
2725
classifiers=[
2826
"Development Status :: 5 - Production/Stable",

‎requests_auth/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
NTLM,
66
SupportMultiAuth,
77
)
8+
from requests_auth._oauth2.browser import DisplaySettings
89
from requests_auth._oauth2.common import OAuth2
910
from requests_auth._oauth2.authorization_code import (
1011
OAuth2AuthorizationCode,
1112
OktaAuthorizationCode,
13+
WakaTimeAuthorizationCode,
1214
)
1315
from requests_auth._oauth2.authorization_code_pkce import (
1416
OAuth2AuthorizationCodePKCE,
@@ -46,6 +48,7 @@
4648
"HeaderApiKey",
4749
"QueryApiKey",
4850
"OAuth2",
51+
"DisplaySettings",
4952
"OAuth2AuthorizationCodePKCE",
5053
"OktaAuthorizationCodePKCE",
5154
"OAuth2Implicit",
@@ -55,6 +58,7 @@
5558
"AzureActiveDirectoryImplicitIdToken",
5659
"OAuth2AuthorizationCode",
5760
"OktaAuthorizationCode",
61+
"WakaTimeAuthorizationCode",
5862
"OAuth2ClientCredentials",
5963
"OktaClientCredentials",
6064
"OAuth2ResourceOwnerPasswordCredentials",

‎requests_auth/_oauth2/authentication_responses_server.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from urllib.parse import parse_qs, urlparse
66
from socket import socket
77

8+
from requests_auth._oauth2.common import OAuth2
9+
810
from requests_auth._errors import *
911

1012
logger = logging.getLogger(__name__)
@@ -86,7 +88,7 @@ def send_html(self, html_content: str):
8688
logger.debug("HTML content sent to client.")
8789

8890
def success_page(self, text: str):
89-
return f"""<body onload="window.open('', '_self', ''); window.setTimeout(close, {self.server.grant_details.reception_success_display_time})" style="
91+
return f"""<body onload="window.open('', '_self', ''); window.setTimeout(close, {OAuth2.display.success_display_time})" style="
9092
color: #4F8A10;
9193
background-color: #DFF2BF;
9294
font-size: xx-large;
@@ -97,7 +99,7 @@ def success_page(self, text: str):
9799
</body>"""
98100

99101
def error_page(self, text: str):
100-
return f"""<body onload="window.open('', '_self', ''); window.setTimeout(close, {self.server.grant_details.reception_failure_display_time})" style="
102+
return f"""<body onload="window.open('', '_self', ''); window.setTimeout(close, {OAuth2.display.failure_display_time})" style="
101103
color: #D8000C;
102104
background-color: #FFBABA;
103105
font-size: xx-large;
@@ -137,15 +139,11 @@ def __init__(
137139
url: str,
138140
name: str,
139141
reception_timeout: float,
140-
reception_success_display_time: int,
141-
reception_failure_display_time: int,
142142
redirect_uri_port: int,
143143
):
144144
self.url = url
145145
self.name = name
146146
self.reception_timeout = reception_timeout
147-
self.reception_success_display_time = reception_success_display_time
148-
self.reception_failure_display_time = reception_failure_display_time
149147
self.redirect_uri_port = redirect_uri_port
150148

151149

‎requests_auth/_oauth2/authorization_code.py

+57-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from hashlib import sha512
2+
from typing import Union, Iterable
3+
24
import requests
35
import requests.auth
46

@@ -36,12 +38,6 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
3638
Listen on port 5000 by default.
3739
:param timeout: Maximum amount of seconds to wait for a code or a token to be received once requested.
3840
Wait for 1 minute by default.
39-
:param success_display_time: In case a code is successfully received,
40-
this is the maximum amount of milliseconds the success page will be displayed in your browser.
41-
Display the page for 1 millisecond by default.
42-
:param failure_display_time: In case received code is not valid,
43-
this is the maximum amount of milliseconds the failure page will be displayed in your browser.
44-
Display the page for 5 seconds by default.
4541
:param header_name: Name of the header field used to send token.
4642
Token will be sent in Authorization header field by default.
4743
:param header_value: Format used to send the token value.
@@ -120,8 +116,6 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
120116
code_grant_url,
121117
code_field_name,
122118
self.timeout,
123-
self.success_display_time,
124-
self.failure_display_time,
125119
self.redirect_uri_port,
126120
)
127121

@@ -211,12 +205,6 @@ def __init__(self, instance: str, client_id: str, **kwargs):
211205
Listen on port 5000 by default.
212206
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
213207
Wait for 1 minute by default.
214-
:param success_display_time: In case a token is successfully received,
215-
this is the maximum amount of milliseconds the success page will be displayed in your browser.
216-
Display the page for 1 millisecond by default.
217-
:param failure_display_time: In case received token is not valid,
218-
this is the maximum amount of milliseconds the failure page will be displayed in your browser.
219-
Display the page for 5 seconds by default.
220208
:param header_name: Name of the header field used to send token.
221209
Token will be sent in Authorization header field by default.
222210
:param header_value: Format used to send the token value.
@@ -239,3 +227,58 @@ def __init__(self, instance: str, client_id: str, **kwargs):
239227
client_id=client_id,
240228
**kwargs,
241229
)
230+
231+
232+
class WakaTimeAuthorizationCode(OAuth2AuthorizationCode):
233+
"""
234+
Describes a WakaTime (OAuth 2) "Access Token" authorization code flow requests authentication.
235+
"""
236+
237+
def __init__(
238+
self,
239+
client_id: str,
240+
client_secret: str,
241+
scope: Union[str, Iterable[str]],
242+
**kwargs,
243+
):
244+
"""
245+
:param client_id: WakaTime Application Identifier (formatted as a Universal Unique Identifier)
246+
:param client_secret: WakaTime Application Secret (formatted as waka_sec_ followed by a Universal Unique Identifier)
247+
:param scope: Scope parameter sent in query. Can also be a list of scopes.
248+
:param response_type: Value of the response_type query parameter.
249+
token by default.
250+
:param token_field_name: Name of the expected field containing the token.
251+
access_token by default.
252+
:param early_expiry: Number of seconds before actual token expiry where token will be considered as expired.
253+
Default to 30 seconds to ensure token will not expire between the time of retrieval and the time the request
254+
reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry.
255+
:param nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details
256+
(formatted as a Universal Unique Identifier - UUID). Use a newly generated UUID by default.
257+
:param redirect_uri_domain: FQDN to use in the redirect_uri when localhost (default) is not allowed.
258+
:param redirect_uri_endpoint: Custom endpoint that will be used as redirect_uri the following way:
259+
http://localhost:<redirect_uri_port>/<redirect_uri_endpoint>. Default value is to redirect on / (root).
260+
:param redirect_uri_port: The port on which the server listening for the OAuth 2 token will be started.
261+
Listen on port 5000 by default.
262+
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
263+
Wait for 1 minute by default.
264+
:param header_name: Name of the header field used to send token.
265+
Token will be sent in Authorization header field by default.
266+
:param header_value: Format used to send the token value.
267+
"{token}" must be present as it will be replaced by the actual token.
268+
Token will be sent as "Bearer {token}" by default.
269+
:param session: requests.Session instance that will be used to request the token.
270+
Use it to provide a custom proxying rule for instance.
271+
:param kwargs: all additional authorization parameters that should be put as query parameter
272+
in the authorization URL.
273+
"""
274+
if not scope:
275+
raise Exception("Scope is mandatory.")
276+
OAuth2AuthorizationCode.__init__(
277+
self,
278+
"https://wakatime.com/oauth/authorize",
279+
"https://wakatime.com/oauth/token",
280+
client_id=client_id,
281+
client_secret=client_secret,
282+
scope=",".join(scope) if isinstance(scope, list) else scope,
283+
**kwargs,
284+
)

‎requests_auth/_oauth2/authorization_code_pkce.py

-14
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
4040
Listen on port 5000 by default.
4141
:param timeout: Maximum amount of seconds to wait for a code or a token to be received once requested.
4242
Wait for 1 minute by default.
43-
:param success_display_time: In case a code is successfully received,
44-
this is the maximum amount of milliseconds the success page will be displayed in your browser.
45-
Display the page for 1 millisecond by default.
46-
:param failure_display_time: In case received code is not valid,
47-
this is the maximum amount of milliseconds the failure page will be displayed in your browser.
48-
Display the page for 5 seconds by default.
4943
:param header_name: Name of the header field used to send token.
5044
Token will be sent in Authorization header field by default.
5145
:param header_value: Format used to send the token value.
@@ -131,8 +125,6 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
131125
code_grant_url,
132126
code_field_name,
133127
self.timeout,
134-
self.success_display_time,
135-
self.failure_display_time,
136128
self.redirect_uri_port,
137129
)
138130

@@ -257,12 +249,6 @@ def __init__(self, instance: str, client_id: str, **kwargs):
257249
Listen on port 5000 by default.
258250
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
259251
Wait for 1 minute by default.
260-
:param success_display_time: In case a token is successfully received,
261-
this is the maximum amount of milliseconds the success page will be displayed in your browser.
262-
Display the page for 1 millisecond by default.
263-
:param failure_display_time: In case received token is not valid,
264-
this is the maximum amount of milliseconds the failure page will be displayed in your browser.
265-
Display the page for 5 seconds by default.
266252
:param header_name: Name of the header field used to send token.
267253
Token will be sent in Authorization header field by default.
268254
:param header_value: Format used to send the token value.

‎requests_auth/_oauth2/browser.py

+22
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,25 @@ def __init__(self, kwargs):
2828
self.failure_display_time = int(
2929
kwargs.pop("failure_display_time", None) or 5000
3030
)
31+
32+
33+
class DisplaySettings:
34+
def __init__(
35+
self,
36+
*,
37+
success_display_time: int = 1,
38+
failure_display_time: int = 5_000,
39+
):
40+
"""
41+
:param success_display_time: In case a code/token is successfully received,
42+
this is the maximum amount of milliseconds the success page will be displayed in your browser.
43+
Display the page for 1 millisecond by default.
44+
:param failure_display_time: In case received code/token is not valid,
45+
this is the maximum amount of milliseconds the failure page will be displayed in your browser.
46+
Display the page for 5 seconds by default.
47+
"""
48+
# Time is expressed in milliseconds
49+
self.success_display_time = success_display_time
50+
51+
# Time is expressed in milliseconds
52+
self.failure_display_time = failure_display_time

‎requests_auth/_oauth2/common.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import requests.auth
55

66
from requests_auth._errors import InvalidGrantRequest, GrantNotProvided
7+
from requests_auth._oauth2.browser import DisplaySettings
78
from requests_auth._oauth2.tokens import TokenMemoryCache
89

910

@@ -33,6 +34,17 @@ def _get_query_parameter(url: str, param_name: str) -> Optional[str]:
3334
return all_values[0] if all_values else None
3435

3536

37+
def _content_from_response(response: requests.Response) -> dict:
38+
content_type = response.headers.get("content-type")
39+
if content_type == "text/html; charset=utf-8":
40+
return {
41+
key_values[0]: key_values[1]
42+
for key_value in response.text.split("&")
43+
if (key_values := key_value.split("=")) and len(key_values) == 2
44+
}
45+
return response.json()
46+
47+
3648
def request_new_grant_with_post(
3749
url: str, data, grant_name: str, timeout: float, session: requests.Session
3850
) -> (str, int, str):
@@ -42,7 +54,7 @@ def request_new_grant_with_post(
4254
# As described in https://tools.ietf.org/html/rfc6749#section-5.2
4355
raise InvalidGrantRequest(response)
4456

45-
content = response.json()
57+
content = _content_from_response(response)
4658
token = content.get(grant_name)
4759
if not token:
4860
raise GrantNotProvided(grant_name, content)
@@ -51,3 +63,4 @@ def request_new_grant_with_post(
5163

5264
class OAuth2:
5365
token_cache = TokenMemoryCache()
66+
display = DisplaySettings()

0 commit comments

Comments
 (0)