oauth2-cli-auth 1.3.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/PKG-INFO +3 -3
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/README.md +1 -1
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/oauth2_cli_auth/code_grant.py +22 -3
- oauth2_cli_auth-1.5.0/oauth2_cli_auth/http_server.py +273 -0
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/oauth2_cli_auth/simplified_flow.py +4 -0
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/pyproject.toml +23 -3
- oauth2_cli_auth-1.3.0/oauth2_cli_auth/http_server.py +0 -223
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/LICENSE +0 -0
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/oauth2_cli_auth/__init__.py +0 -0
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/oauth2_cli_auth/_timeout.py +0 -0
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/oauth2_cli_auth/_urllib_util.py +0 -0
- {oauth2_cli_auth-1.3.0 → oauth2_cli_auth-1.5.0}/oauth2_cli_auth/conftest.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: oauth2-cli-auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Authenticate against OAuth2 Provider in Python CLIs
|
|
5
5
|
Home-page: https://github.com/timo-reymann/python-oauth2-cli-auth
|
|
6
6
|
License: MIT
|
|
7
7
|
Author: Timo Reymann
|
|
8
8
|
Author-email: mail@timo-reymann.de
|
|
9
|
-
Requires-Python: >=3.9
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Environment :: MacOS X
|
|
@@ -111,7 +111,7 @@ So I created this small library without any dependencies besides the python stan
|
|
|
111
111
|
|
|
112
112
|
## Documentation
|
|
113
113
|
|
|
114
|
-
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth
|
|
114
|
+
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth)
|
|
115
115
|
|
|
116
116
|
## Contributing
|
|
117
117
|
|
|
@@ -85,7 +85,7 @@ So I created this small library without any dependencies besides the python stan
|
|
|
85
85
|
|
|
86
86
|
## Documentation
|
|
87
87
|
|
|
88
|
-
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth
|
|
88
|
+
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth)
|
|
89
89
|
|
|
90
90
|
## Contributing
|
|
91
91
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functionality to work with OAuth2 code grant
|
|
3
|
+
"""
|
|
1
4
|
import base64
|
|
2
5
|
import urllib.parse
|
|
3
6
|
import urllib.request
|
|
@@ -15,15 +18,27 @@ class OAuth2ClientInfo:
|
|
|
15
18
|
"""
|
|
16
19
|
authorization_url: str
|
|
17
20
|
"""Authorization URL to redirect the user to"""
|
|
21
|
+
|
|
18
22
|
token_url: str
|
|
19
23
|
"""Token URL for fetching the access token"""
|
|
24
|
+
|
|
20
25
|
client_id: str
|
|
21
26
|
"""Id of the client to request for"""
|
|
27
|
+
|
|
22
28
|
scopes: list[str]
|
|
23
29
|
"""List of scopes to request"""
|
|
24
30
|
|
|
25
31
|
@staticmethod
|
|
26
|
-
def from_oidc_endpoint(oidc_config_endpoint: str, client_id: str, scopes: list[str]):
|
|
32
|
+
def from_oidc_endpoint(oidc_config_endpoint: str, client_id: str, scopes: list[str]) -> "OAuth2ClientInfo":
|
|
33
|
+
"""
|
|
34
|
+
Create client information object from well known endpoint in format as specified in
|
|
35
|
+
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
|
36
|
+
|
|
37
|
+
:param oidc_config_endpoint: Endpoint to load
|
|
38
|
+
:param client_id: Client ID to use
|
|
39
|
+
:param scopes: Scopes to add to OAuth2 authorization requests
|
|
40
|
+
:return: OAuth2ClientInfo containing parsed information from endpoint
|
|
41
|
+
"""
|
|
27
42
|
config = load_oidc_config(oidc_config_endpoint)
|
|
28
43
|
return OAuth2ClientInfo(
|
|
29
44
|
authorization_url=config.get("authorization_endpoint"),
|
|
@@ -34,6 +49,11 @@ class OAuth2ClientInfo:
|
|
|
34
49
|
|
|
35
50
|
|
|
36
51
|
def load_oidc_config(odic_well_known_endpoint: str) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
Load OIDC configuration from the well known configuration endpoint
|
|
54
|
+
|
|
55
|
+
:param odic_well_known_endpoint: Endpoint to load configuration from
|
|
56
|
+
"""
|
|
37
57
|
config = _load_json(odic_well_known_endpoint)
|
|
38
58
|
return config
|
|
39
59
|
|
|
@@ -41,6 +61,7 @@ def load_oidc_config(odic_well_known_endpoint: str) -> dict:
|
|
|
41
61
|
def open_browser(url: str, print_open_browser_instruction: Callable[[str], None] | None = print) -> None:
|
|
42
62
|
"""
|
|
43
63
|
Open browser using webbrowser module and show message about URL open
|
|
64
|
+
|
|
44
65
|
:param print_open_browser_instruction: Callback to print the instructions to open the browser. Set to None in order to supress the output.
|
|
45
66
|
:param url: URL to open and display
|
|
46
67
|
:return: None
|
|
@@ -76,8 +97,6 @@ def exchange_code_for_response(
|
|
|
76
97
|
:param client_info: Info about oauth2 client
|
|
77
98
|
:param redirect_uri: Callback URL
|
|
78
99
|
:param code: Code to redeem
|
|
79
|
-
:param access_token_field: Name of the field containing the access token to use. This might differ depending on
|
|
80
|
-
the provider you are using. For example for Auth0 you have to set this to id_token
|
|
81
100
|
:return: Response from OAuth2 endpoint
|
|
82
101
|
"""
|
|
83
102
|
headers = {
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local HTTP server logic
|
|
3
|
+
"""
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
from string import Template
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from urllib.parse import parse_qs, urlparse
|
|
8
|
+
|
|
9
|
+
from oauth2_cli_auth._timeout import TimeoutException, _method_with_timeout
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CallbackPageTemplate:
|
|
13
|
+
"""
|
|
14
|
+
Holder for callback page assets and template
|
|
15
|
+
"""
|
|
16
|
+
SUCCESS_SVG = """
|
|
17
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
18
|
+
<g fill="none" stroke="#22AE73" stroke-width="2">
|
|
19
|
+
<circle cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
20
|
+
<circle id="colored" fill="#22AE73" cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
21
|
+
<polyline class="st0" stroke="#fff" stroke-width="10" points="43.5,77.8 63.7,97.9 112.2,49.4 " style="stroke-dasharray:100px, 100px; stroke-dashoffset: 200px;"/>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
|
24
|
+
"""
|
|
25
|
+
"""SVG to display checkmark"""
|
|
26
|
+
|
|
27
|
+
ERROR_SVG = """
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
29
|
+
<g fill="none" stroke="#F44812" stroke-width="2">
|
|
30
|
+
<circle cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
31
|
+
<circle id="colored" fill="#F44812" cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
32
|
+
<polyline class="st0" stroke="#fff" stroke-width="10" points="43.5,77.8 112.2,77.8 " style="stroke-dasharray:100px, 100px; stroke-dashoffset: 200px;"/>
|
|
33
|
+
</g>
|
|
34
|
+
</svg>
|
|
35
|
+
"""
|
|
36
|
+
"""SVG to display error icon"""
|
|
37
|
+
|
|
38
|
+
PAGE_TEMPLATE = Template(
|
|
39
|
+
"""
|
|
40
|
+
<html lang="$lang">
|
|
41
|
+
<head>
|
|
42
|
+
<title>$title</title>
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
44
|
+
<meta name="charset" content="utf-8">
|
|
45
|
+
<style>
|
|
46
|
+
* {
|
|
47
|
+
margin: 0;
|
|
48
|
+
padding: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
body {
|
|
52
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
body {
|
|
57
|
+
background: rgb(34, 39, 46);
|
|
58
|
+
color: rgb(173, 186, 199);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
html, body {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h1 {
|
|
70
|
+
font-size: 4rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
p {
|
|
74
|
+
font-size: 1.4rem;
|
|
75
|
+
max-width: 70ch;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.message {
|
|
79
|
+
text-align: center;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.animation-ctn {
|
|
83
|
+
text-align: center;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@keyframes checkmark {
|
|
87
|
+
0% {
|
|
88
|
+
stroke-dashoffset: 100px
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
100% {
|
|
92
|
+
stroke-dashoffset: 0px
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@keyframes checkmark-circle {
|
|
97
|
+
0% {
|
|
98
|
+
stroke-dashoffset: 480px
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
100% {
|
|
102
|
+
stroke-dashoffset: 960px
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@keyframes colored-circle {
|
|
107
|
+
0% {
|
|
108
|
+
opacity: 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
100% {
|
|
112
|
+
opacity: 100
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.icon svg {
|
|
117
|
+
padding: 1rem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.icon svg polyline {
|
|
121
|
+
-webkit-animation: checkmark 0.25s ease-in-out 0.7s backwards;
|
|
122
|
+
animation: checkmark 0.25s ease-in-out 0.7s backwards
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.icon svg circle {
|
|
126
|
+
-webkit-animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
127
|
+
animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.icon svg circle#colored {
|
|
131
|
+
-webkit-animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
132
|
+
animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
135
|
+
</head>
|
|
136
|
+
|
|
137
|
+
<body>
|
|
138
|
+
<div class="message">
|
|
139
|
+
<div class="animation-ctn">
|
|
140
|
+
<div class="icon">
|
|
141
|
+
$svg
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<h1>$title</h1>
|
|
146
|
+
<p>$message</p>
|
|
147
|
+
</div>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
|
150
|
+
"""
|
|
151
|
+
)
|
|
152
|
+
"""Template for callback HTML page"""
|
|
153
|
+
|
|
154
|
+
def render(self, title: str, message: str, lang: str = "en", has_error: bool = False):
|
|
155
|
+
"""
|
|
156
|
+
Render callback page
|
|
157
|
+
|
|
158
|
+
:param title: Page title
|
|
159
|
+
:param message: Message to display under the icon
|
|
160
|
+
:param lang: Langauge for HTML tag
|
|
161
|
+
:param has_error: Indicates weather checkmark (false) or cross (true) should be displayed
|
|
162
|
+
:return: Rendered HTML
|
|
163
|
+
"""
|
|
164
|
+
return self.PAGE_TEMPLATE.substitute(
|
|
165
|
+
lang=lang,
|
|
166
|
+
title=title,
|
|
167
|
+
message=message,
|
|
168
|
+
svg=self.ERROR_SVG if has_error else self.SUCCESS_SVG,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class OAuthRedirectHandler(BaseHTTPRequestHandler):
|
|
173
|
+
"""
|
|
174
|
+
HTTPRequest Handler that is intended to be used as oauth2 callback page
|
|
175
|
+
"""
|
|
176
|
+
_callback_template = CallbackPageTemplate()
|
|
177
|
+
LANGUAGE = "en"
|
|
178
|
+
"""HTML language to render for callback page"""
|
|
179
|
+
|
|
180
|
+
def log_message(self, format, *args):
|
|
181
|
+
"""
|
|
182
|
+
Disables logging for BaseHTTPRequestHandler
|
|
183
|
+
"""
|
|
184
|
+
# silence the log messages
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
def _serve_html(self, html: str):
|
|
188
|
+
self.send_header("Content-type", "text/html")
|
|
189
|
+
self.end_headers()
|
|
190
|
+
self.wfile.write(html.encode("utf-8"))
|
|
191
|
+
|
|
192
|
+
def _serve_callback_page(self, title: str, message: str, has_error: bool):
|
|
193
|
+
rendered_callback_page = self._callback_template.render(
|
|
194
|
+
lang=self.LANGUAGE,
|
|
195
|
+
title=title,
|
|
196
|
+
message=message,
|
|
197
|
+
has_error=has_error
|
|
198
|
+
)
|
|
199
|
+
self._serve_html(rendered_callback_page)
|
|
200
|
+
|
|
201
|
+
# noinspection PyPep8Naming
|
|
202
|
+
def do_GET(self) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Provides callback page for the oauth2 redirect
|
|
205
|
+
"""
|
|
206
|
+
params = parse_qs(urlparse(self.path).query)
|
|
207
|
+
|
|
208
|
+
has_error = ("code" not in params
|
|
209
|
+
or len(params['code']) != 1
|
|
210
|
+
or params['code'][0].strip() == "")
|
|
211
|
+
|
|
212
|
+
if has_error:
|
|
213
|
+
self.send_response(400)
|
|
214
|
+
self._serve_callback_page(
|
|
215
|
+
title="Oh snap!",
|
|
216
|
+
message="Something went wrong trying to authenticate you. Please try going back in your browser, or restart the auth process.",
|
|
217
|
+
has_error=True,
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
self.send_response(200)
|
|
221
|
+
self.server._code = params["code"][0]
|
|
222
|
+
self._serve_callback_page(
|
|
223
|
+
title="Success",
|
|
224
|
+
message="You have been authenticated successfully. You may close this browser window now and go back to the terminal",
|
|
225
|
+
has_error=False,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class OAuthCallbackHttpServer(HTTPServer):
|
|
230
|
+
"""
|
|
231
|
+
Simplistic HTTP Server to provide local callback URL for oauth2 provider
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __init__(self, port):
|
|
235
|
+
super().__init__(("", port), OAuthRedirectHandler)
|
|
236
|
+
|
|
237
|
+
self._code: str | None = None
|
|
238
|
+
|
|
239
|
+
def get_code(self) -> str | None:
|
|
240
|
+
"""
|
|
241
|
+
This method should only be called after the request was done and might be None when no token is given.
|
|
242
|
+
|
|
243
|
+
:return: Authorization code or None if the request was not performed yet
|
|
244
|
+
"""
|
|
245
|
+
return self._code
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def callback_url(self) -> str:
|
|
249
|
+
"""
|
|
250
|
+
Callback URL for the HTTP-Server
|
|
251
|
+
"""
|
|
252
|
+
return f"http://localhost:{self.server_port}"
|
|
253
|
+
|
|
254
|
+
def wait_for_code(self, attempts: int = 3, timeout_per_attempt=10) -> Optional[str]:
|
|
255
|
+
"""
|
|
256
|
+
Wait for the server to open the callback page containing the code query parameter.
|
|
257
|
+
|
|
258
|
+
It tries for #attempts with a timeout of #timeout_per_attempts for each attempt.
|
|
259
|
+
This prevents the CLI from getting stuck by unsolved callback URls
|
|
260
|
+
|
|
261
|
+
:param attempts: Amount of attempts
|
|
262
|
+
:param timeout_per_attempt: Timeout for each attempt to be successful
|
|
263
|
+
:return: Code from callback page or None if the callback page is not called successfully
|
|
264
|
+
"""
|
|
265
|
+
for _ in range(0, attempts):
|
|
266
|
+
try:
|
|
267
|
+
_method_with_timeout(self.handle_request, timeout_seconds=timeout_per_attempt)
|
|
268
|
+
except TimeoutException:
|
|
269
|
+
continue
|
|
270
|
+
if self.get_code() is not None:
|
|
271
|
+
return self.get_code()
|
|
272
|
+
|
|
273
|
+
return None
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ready to use workflows
|
|
3
|
+
"""
|
|
1
4
|
from oauth2_cli_auth import OAuthCallbackHttpServer, get_auth_url, exchange_code_for_access_token, OAuth2ClientInfo, \
|
|
2
5
|
open_browser
|
|
3
6
|
|
|
@@ -10,6 +13,7 @@ def get_access_token_with_browser_open(client_info: OAuth2ClientInfo, server_por
|
|
|
10
13
|
- Open the browser with the authorization URL
|
|
11
14
|
- Wait for the code to arrive
|
|
12
15
|
- Get access token from code
|
|
16
|
+
|
|
13
17
|
:param client_info: Client Info for Oauth2 Interaction
|
|
14
18
|
:param server_port: Port of the local web server to spin up
|
|
15
19
|
:return: Access Token
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "oauth2-cli-auth"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
4
4
|
description = "Authenticate against OAuth2 Provider in Python CLIs"
|
|
5
5
|
authors = ["Timo Reymann <mail@timo-reymann.de>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -26,7 +26,7 @@ include = [
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
]
|
|
28
28
|
[tool.poetry.dependencies]
|
|
29
|
-
python = "
|
|
29
|
+
python = ">=3.9"
|
|
30
30
|
|
|
31
31
|
[tool.poetry.urls]
|
|
32
32
|
"Bug Tracker" = "https://github.com/timo-reymann/python-oauth2-cli-auth/issues"
|
|
@@ -39,7 +39,8 @@ priority="explicit"
|
|
|
39
39
|
[tool.poetry.group.dev.dependencies]
|
|
40
40
|
pytest = "^7.4.3"
|
|
41
41
|
coverage = "^7.3.2"
|
|
42
|
-
|
|
42
|
+
setuptools = "^69.5.1"
|
|
43
|
+
pydoctor = "^23.9.1"
|
|
43
44
|
|
|
44
45
|
[tool.coverage.run]
|
|
45
46
|
omit = [".*", "*/site-packages/*", "*/*_test.py"]
|
|
@@ -50,3 +51,22 @@ fail_under = 70
|
|
|
50
51
|
[build-system]
|
|
51
52
|
requires = ["poetry-core"]
|
|
52
53
|
build-backend = "poetry.core.masonry.api"
|
|
54
|
+
|
|
55
|
+
[tool.pydoctor]
|
|
56
|
+
verbose = 0
|
|
57
|
+
warnings-as-errors = true
|
|
58
|
+
|
|
59
|
+
intersphinx = ["https://docs.python.org/3/objects.inv"]
|
|
60
|
+
docformat = "restructuredtext"
|
|
61
|
+
html-output = "gh-pages"
|
|
62
|
+
|
|
63
|
+
add-package = ["oauth2_cli_auth"]
|
|
64
|
+
project-name = "oauth2_cli_auth"
|
|
65
|
+
project-url = "https://github.com/timo-reymann/python-oauth2-cli-auth"
|
|
66
|
+
privacy = [
|
|
67
|
+
"HIDDEN:oauth2_cli_auth.*_test",
|
|
68
|
+
"HIDDEN:oauth2_cli_auth.conftest"
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
theme = "base"
|
|
72
|
+
template-dir = "pydoctor-theme"
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
2
|
-
from string import Template
|
|
3
|
-
from typing import Optional
|
|
4
|
-
from urllib.parse import parse_qs, urlparse
|
|
5
|
-
from oauth2_cli_auth._timeout import _method_with_timeout, TimeoutException
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CallbackPageTemplate:
|
|
9
|
-
SUCCESS_SVG = """
|
|
10
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
11
|
-
<g fill="none" stroke="#22AE73" stroke-width="2">
|
|
12
|
-
<circle cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
13
|
-
<circle id="colored" fill="#22AE73" cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
14
|
-
<polyline class="st0" stroke="#fff" stroke-width="10" points="43.5,77.8 63.7,97.9 112.2,49.4 " style="stroke-dasharray:100px, 100px; stroke-dashoffset: 200px;"/>
|
|
15
|
-
</g>
|
|
16
|
-
</svg>
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
ERROR_SVG = """
|
|
20
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
21
|
-
<g fill="none" stroke="#F44812" stroke-width="2">
|
|
22
|
-
<circle cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
23
|
-
<circle id="colored" fill="#F44812" cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
24
|
-
<polyline class="st0" stroke="#fff" stroke-width="10" points="43.5,77.8 112.2,77.8 " style="stroke-dasharray:100px, 100px; stroke-dashoffset: 200px;"/>
|
|
25
|
-
</g>
|
|
26
|
-
</svg>
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
PAGE_TEMPLATE = Template("""
|
|
30
|
-
<html lang="$lang">
|
|
31
|
-
<head>
|
|
32
|
-
<title>$title</title>
|
|
33
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
34
|
-
<meta name="charset" content="utf-8">
|
|
35
|
-
<style>
|
|
36
|
-
* {
|
|
37
|
-
margin: 0;
|
|
38
|
-
padding: 0;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
body {
|
|
42
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
@media (prefers-color-scheme: dark) {
|
|
46
|
-
body {
|
|
47
|
-
background: rgb(34, 39, 46);
|
|
48
|
-
color: rgb(173, 186, 199);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
html, body {
|
|
53
|
-
display: flex;
|
|
54
|
-
align-items: center;
|
|
55
|
-
justify-content: center;
|
|
56
|
-
height: 100%;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
h1 {
|
|
60
|
-
font-size: 4rem;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
p {
|
|
64
|
-
font-size: 1.4rem;
|
|
65
|
-
max-width: 70ch;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.message {
|
|
69
|
-
text-align: center;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.animation-ctn {
|
|
73
|
-
text-align: center;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@keyframes checkmark {
|
|
77
|
-
0% {
|
|
78
|
-
stroke-dashoffset: 100px
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
100% {
|
|
82
|
-
stroke-dashoffset: 0px
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@keyframes checkmark-circle {
|
|
87
|
-
0% {
|
|
88
|
-
stroke-dashoffset: 480px
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
100% {
|
|
92
|
-
stroke-dashoffset: 960px
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@keyframes colored-circle {
|
|
97
|
-
0% {
|
|
98
|
-
opacity: 0
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
100% {
|
|
102
|
-
opacity: 100
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.icon svg {
|
|
107
|
-
padding: 1rem;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.icon svg polyline {
|
|
111
|
-
-webkit-animation: checkmark 0.25s ease-in-out 0.7s backwards;
|
|
112
|
-
animation: checkmark 0.25s ease-in-out 0.7s backwards
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.icon svg circle {
|
|
116
|
-
-webkit-animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
117
|
-
animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.icon svg circle#colored {
|
|
121
|
-
-webkit-animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
122
|
-
animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
123
|
-
}
|
|
124
|
-
</style>
|
|
125
|
-
</head>
|
|
126
|
-
|
|
127
|
-
<body>
|
|
128
|
-
<div class="message">
|
|
129
|
-
<div class="animation-ctn">
|
|
130
|
-
<div class="icon">
|
|
131
|
-
$svg
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<h1>$title</h1>
|
|
136
|
-
<p>$message</p>
|
|
137
|
-
</div>
|
|
138
|
-
</body>
|
|
139
|
-
</html>
|
|
140
|
-
""")
|
|
141
|
-
|
|
142
|
-
def render(self, title: str, message: str, lang: str = "en", has_error: bool = False):
|
|
143
|
-
return self.PAGE_TEMPLATE.substitute(
|
|
144
|
-
lang=lang,
|
|
145
|
-
title=title,
|
|
146
|
-
message=message,
|
|
147
|
-
svg=self.ERROR_SVG if has_error else self.SUCCESS_SVG,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class OAuthRedirectHandler(BaseHTTPRequestHandler):
|
|
152
|
-
callback_template = CallbackPageTemplate()
|
|
153
|
-
|
|
154
|
-
def log_message(self, format, *args):
|
|
155
|
-
# silence the log messages
|
|
156
|
-
pass
|
|
157
|
-
|
|
158
|
-
def do_GET(self):
|
|
159
|
-
params = parse_qs(urlparse(self.path).query)
|
|
160
|
-
|
|
161
|
-
has_error = "code" not in params or len(params['code']) != 1 or params['code'][0].strip() == ""
|
|
162
|
-
|
|
163
|
-
if has_error:
|
|
164
|
-
self.send_response(400)
|
|
165
|
-
title = "Oh snap!"
|
|
166
|
-
message = "Something went wrong trying to authenticate you. Please try going back in your browser, or restart the auth process."
|
|
167
|
-
else:
|
|
168
|
-
self.send_response(200)
|
|
169
|
-
self.server._code = params["code"][0]
|
|
170
|
-
title = "Success"
|
|
171
|
-
message = "You have been authenticated successfully. You may close this browser window now and go back to the terminal"
|
|
172
|
-
|
|
173
|
-
self.send_header("Content-type", "text/html")
|
|
174
|
-
self.end_headers()
|
|
175
|
-
self.wfile.write(
|
|
176
|
-
self.callback_template
|
|
177
|
-
.render(
|
|
178
|
-
lang="en",
|
|
179
|
-
title=title,
|
|
180
|
-
message=message,
|
|
181
|
-
has_error=has_error
|
|
182
|
-
)
|
|
183
|
-
.encode("utf-8")
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
class OAuthCallbackHttpServer(HTTPServer):
|
|
188
|
-
"""
|
|
189
|
-
Simplistic HTTP Server to provide local callback URL for oauth2 provider
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
def __init__(self, port):
|
|
193
|
-
super().__init__(("", port), OAuthRedirectHandler)
|
|
194
|
-
|
|
195
|
-
self._code = None
|
|
196
|
-
|
|
197
|
-
def get_code(self):
|
|
198
|
-
return self._code
|
|
199
|
-
|
|
200
|
-
@property
|
|
201
|
-
def callback_url(self):
|
|
202
|
-
return f"http://localhost:{self.server_port}"
|
|
203
|
-
|
|
204
|
-
def wait_for_code(self, attempts: int = 3, timeout_per_attempt=10) -> Optional[str]:
|
|
205
|
-
"""
|
|
206
|
-
Wait for the server to open the callback page containing the code query parameter.
|
|
207
|
-
|
|
208
|
-
It tries for #attempts with a timeout of #timeout_per_attempts for each attempt.
|
|
209
|
-
This prevents the CLI from getting stuck by unsolved callback URls
|
|
210
|
-
|
|
211
|
-
:param attempts: Amount of attempts
|
|
212
|
-
:param timeout_per_attempt: Timeout for each attempt to be successful
|
|
213
|
-
:return: Code from callback page or None if the callback page is not called successfully
|
|
214
|
-
"""
|
|
215
|
-
for i in range(0, attempts):
|
|
216
|
-
try:
|
|
217
|
-
_method_with_timeout(self.handle_request, timeout_seconds=timeout_per_attempt)
|
|
218
|
-
except TimeoutException:
|
|
219
|
-
continue
|
|
220
|
-
if self.get_code() is not None:
|
|
221
|
-
return self.get_code()
|
|
222
|
-
|
|
223
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|