oauth2-cli-auth 1.4.0__tar.gz → 1.6.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.4.0 → oauth2_cli_auth-1.6.0}/PKG-INFO +6 -5
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/README.md +2 -1
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/oauth2_cli_auth/__init__.py +1 -1
- oauth2_cli_auth-1.6.0/oauth2_cli_auth/__version__.py +1 -0
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/oauth2_cli_auth/code_grant.py +14 -1
- oauth2_cli_auth-1.6.0/oauth2_cli_auth/http_server.py +273 -0
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/pyproject.toml +5 -6
- oauth2_cli_auth-1.4.0/oauth2_cli_auth/http_server.py +0 -232
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/LICENSE +0 -0
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/oauth2_cli_auth/_timeout.py +0 -0
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/oauth2_cli_auth/_urllib_util.py +0 -0
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/oauth2_cli_auth/conftest.py +0 -0
- {oauth2_cli_auth-1.4.0 → oauth2_cli_auth-1.6.0}/oauth2_cli_auth/simplified_flow.py +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: oauth2-cli-auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Authenticate against OAuth2 Provider in Python CLIs
|
|
5
|
-
Home-page: https://github.com/timo-reymann/python-oauth2-cli-auth
|
|
6
5
|
License: MIT
|
|
7
6
|
Author: Timo Reymann
|
|
8
7
|
Author-email: mail@timo-reymann.de
|
|
9
|
-
Requires-Python: >=3.9
|
|
8
|
+
Requires-Python: >=3.9
|
|
10
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
10
|
Classifier: Environment :: Console
|
|
12
11
|
Classifier: Environment :: MacOS X
|
|
@@ -20,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Project-URL: Bug Tracker, https://github.com/timo-reymann/python-oauth2-cli-auth/issues
|
|
24
24
|
Project-URL: Repository, https://github.com/timo-reymann/python-oauth2-cli-auth
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -30,6 +30,7 @@ oauth2-cli-auth
|
|
|
30
30
|
[](https://app.circleci.com/pipelines/github/timo-reymann/python-oauth2-cli-auth)
|
|
31
31
|
[](https://github.com/timo-reymann/python-oauth2-cli-auth/releases)
|
|
32
32
|
[](https://pypi.org/project/oauth2-cli-auth)
|
|
33
|
+

|
|
33
34
|
[](https://codecov.io/gh/timo-reymann/python-oauth2-cli-auth)
|
|
34
35
|
[](https://sonarcloud.io/summary/new_code?id=timo-reymann_python-oauth2-cli-auth)
|
|
35
36
|
[](https://sonarcloud.io/summary/new_code?id=timo-reymann_python-oauth2-cli-auth)
|
|
@@ -111,7 +112,7 @@ So I created this small library without any dependencies besides the python stan
|
|
|
111
112
|
|
|
112
113
|
## Documentation
|
|
113
114
|
|
|
114
|
-
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth
|
|
115
|
+
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth)
|
|
115
116
|
|
|
116
117
|
## Contributing
|
|
117
118
|
|
|
@@ -4,6 +4,7 @@ oauth2-cli-auth
|
|
|
4
4
|
[](https://app.circleci.com/pipelines/github/timo-reymann/python-oauth2-cli-auth)
|
|
5
5
|
[](https://github.com/timo-reymann/python-oauth2-cli-auth/releases)
|
|
6
6
|
[](https://pypi.org/project/oauth2-cli-auth)
|
|
7
|
+

|
|
7
8
|
[](https://codecov.io/gh/timo-reymann/python-oauth2-cli-auth)
|
|
8
9
|
[](https://sonarcloud.io/summary/new_code?id=timo-reymann_python-oauth2-cli-auth)
|
|
9
10
|
[](https://sonarcloud.io/summary/new_code?id=timo-reymann_python-oauth2-cli-auth)
|
|
@@ -85,7 +86,7 @@ So I created this small library without any dependencies besides the python stan
|
|
|
85
86
|
|
|
86
87
|
## Documentation
|
|
87
88
|
|
|
88
|
-
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth
|
|
89
|
+
- [Library documentation](https://timo-reymann.github.io/python-oauth2-cli-auth)
|
|
89
90
|
|
|
90
91
|
## Contributing
|
|
91
92
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Authenticate against OAuth2 Provider in Python CLIs
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
from oauth2_cli_auth.__version__ import __version__
|
|
5
5
|
from oauth2_cli_auth.http_server import OAuthCallbackHttpServer
|
|
6
6
|
from oauth2_cli_auth.code_grant import OAuth2ClientInfo, exchange_code_for_access_token, get_auth_url, open_browser, load_oidc_config
|
|
7
7
|
from oauth2_cli_auth.simplified_flow import get_access_token_with_browser_open
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.6.0"
|
|
@@ -18,15 +18,27 @@ class OAuth2ClientInfo:
|
|
|
18
18
|
"""
|
|
19
19
|
authorization_url: str
|
|
20
20
|
"""Authorization URL to redirect the user to"""
|
|
21
|
+
|
|
21
22
|
token_url: str
|
|
22
23
|
"""Token URL for fetching the access token"""
|
|
24
|
+
|
|
23
25
|
client_id: str
|
|
24
26
|
"""Id of the client to request for"""
|
|
27
|
+
|
|
25
28
|
scopes: list[str]
|
|
26
29
|
"""List of scopes to request"""
|
|
27
30
|
|
|
28
31
|
@staticmethod
|
|
29
|
-
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
|
+
"""
|
|
30
42
|
config = load_oidc_config(oidc_config_endpoint)
|
|
31
43
|
return OAuth2ClientInfo(
|
|
32
44
|
authorization_url=config.get("authorization_endpoint"),
|
|
@@ -49,6 +61,7 @@ def load_oidc_config(odic_well_known_endpoint: str) -> dict:
|
|
|
49
61
|
def open_browser(url: str, print_open_browser_instruction: Callable[[str], None] | None = print) -> None:
|
|
50
62
|
"""
|
|
51
63
|
Open browser using webbrowser module and show message about URL open
|
|
64
|
+
|
|
52
65
|
:param print_open_browser_instruction: Callback to print the instructions to open the browser. Set to None in order to supress the output.
|
|
53
66
|
:param url: URL to open and display
|
|
54
67
|
:return: None
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "oauth2-cli-auth"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.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"
|
|
@@ -37,11 +37,10 @@ url = "https://test.pypi.org/legacy/"
|
|
|
37
37
|
priority="explicit"
|
|
38
38
|
|
|
39
39
|
[tool.poetry.group.dev.dependencies]
|
|
40
|
-
pytest = "^
|
|
40
|
+
pytest = "^8.0.0"
|
|
41
41
|
coverage = "^7.3.2"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
pydoctor = "^23.9.1"
|
|
42
|
+
setuptools = "^77.0.0"
|
|
43
|
+
pydoctor = "^24.0.0"
|
|
45
44
|
|
|
46
45
|
[tool.coverage.run]
|
|
47
46
|
omit = [".*", "*/site-packages/*", "*/*_test.py"]
|
|
@@ -1,232 +0,0 @@
|
|
|
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
|
-
from oauth2_cli_auth._timeout import _method_with_timeout, TimeoutException
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CallbackPageTemplate:
|
|
12
|
-
"""
|
|
13
|
-
Holder for callback page assets and template
|
|
14
|
-
"""
|
|
15
|
-
SUCCESS_SVG = """
|
|
16
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
17
|
-
<g fill="none" stroke="#22AE73" stroke-width="2">
|
|
18
|
-
<circle cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
19
|
-
<circle id="colored" fill="#22AE73" cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
20
|
-
<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;"/>
|
|
21
|
-
</g>
|
|
22
|
-
</svg>
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
ERROR_SVG = """
|
|
26
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
27
|
-
<g fill="none" stroke="#F44812" stroke-width="2">
|
|
28
|
-
<circle cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
29
|
-
<circle id="colored" fill="#F44812" cx="77" cy="77" r="72" style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
30
|
-
<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;"/>
|
|
31
|
-
</g>
|
|
32
|
-
</svg>
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
PAGE_TEMPLATE = Template("""
|
|
36
|
-
<html lang="$lang">
|
|
37
|
-
<head>
|
|
38
|
-
<title>$title</title>
|
|
39
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
40
|
-
<meta name="charset" content="utf-8">
|
|
41
|
-
<style>
|
|
42
|
-
* {
|
|
43
|
-
margin: 0;
|
|
44
|
-
padding: 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
body {
|
|
48
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@media (prefers-color-scheme: dark) {
|
|
52
|
-
body {
|
|
53
|
-
background: rgb(34, 39, 46);
|
|
54
|
-
color: rgb(173, 186, 199);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
html, body {
|
|
59
|
-
display: flex;
|
|
60
|
-
align-items: center;
|
|
61
|
-
justify-content: center;
|
|
62
|
-
height: 100%;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
h1 {
|
|
66
|
-
font-size: 4rem;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
p {
|
|
70
|
-
font-size: 1.4rem;
|
|
71
|
-
max-width: 70ch;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.message {
|
|
75
|
-
text-align: center;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.animation-ctn {
|
|
79
|
-
text-align: center;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
@keyframes checkmark {
|
|
83
|
-
0% {
|
|
84
|
-
stroke-dashoffset: 100px
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
100% {
|
|
88
|
-
stroke-dashoffset: 0px
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
@keyframes checkmark-circle {
|
|
93
|
-
0% {
|
|
94
|
-
stroke-dashoffset: 480px
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
100% {
|
|
98
|
-
stroke-dashoffset: 960px
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@keyframes colored-circle {
|
|
103
|
-
0% {
|
|
104
|
-
opacity: 0
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
100% {
|
|
108
|
-
opacity: 100
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.icon svg {
|
|
113
|
-
padding: 1rem;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.icon svg polyline {
|
|
117
|
-
-webkit-animation: checkmark 0.25s ease-in-out 0.7s backwards;
|
|
118
|
-
animation: checkmark 0.25s ease-in-out 0.7s backwards
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.icon svg circle {
|
|
122
|
-
-webkit-animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
123
|
-
animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.icon svg circle#colored {
|
|
127
|
-
-webkit-animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
128
|
-
animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
129
|
-
}
|
|
130
|
-
</style>
|
|
131
|
-
</head>
|
|
132
|
-
|
|
133
|
-
<body>
|
|
134
|
-
<div class="message">
|
|
135
|
-
<div class="animation-ctn">
|
|
136
|
-
<div class="icon">
|
|
137
|
-
$svg
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<h1>$title</h1>
|
|
142
|
-
<p>$message</p>
|
|
143
|
-
</div>
|
|
144
|
-
</body>
|
|
145
|
-
</html>
|
|
146
|
-
""")
|
|
147
|
-
|
|
148
|
-
def render(self, title: str, message: str, lang: str = "en", has_error: bool = False):
|
|
149
|
-
return self.PAGE_TEMPLATE.substitute(
|
|
150
|
-
lang=lang,
|
|
151
|
-
title=title,
|
|
152
|
-
message=message,
|
|
153
|
-
svg=self.ERROR_SVG if has_error else self.SUCCESS_SVG,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class OAuthRedirectHandler(BaseHTTPRequestHandler):
|
|
158
|
-
"""
|
|
159
|
-
HTTPRequest Handler that is intended to be used as oauth2 callback page
|
|
160
|
-
"""
|
|
161
|
-
callback_template = CallbackPageTemplate()
|
|
162
|
-
|
|
163
|
-
def log_message(self, format, *args):
|
|
164
|
-
# silence the log messages
|
|
165
|
-
pass
|
|
166
|
-
|
|
167
|
-
def do_GET(self):
|
|
168
|
-
params = parse_qs(urlparse(self.path).query)
|
|
169
|
-
|
|
170
|
-
has_error = "code" not in params or len(params['code']) != 1 or params['code'][0].strip() == ""
|
|
171
|
-
|
|
172
|
-
if has_error:
|
|
173
|
-
self.send_response(400)
|
|
174
|
-
title = "Oh snap!"
|
|
175
|
-
message = "Something went wrong trying to authenticate you. Please try going back in your browser, or restart the auth process."
|
|
176
|
-
else:
|
|
177
|
-
self.send_response(200)
|
|
178
|
-
self.server._code = params["code"][0]
|
|
179
|
-
title = "Success"
|
|
180
|
-
message = "You have been authenticated successfully. You may close this browser window now and go back to the terminal"
|
|
181
|
-
|
|
182
|
-
self.send_header("Content-type", "text/html")
|
|
183
|
-
self.end_headers()
|
|
184
|
-
self.wfile.write(
|
|
185
|
-
self.callback_template
|
|
186
|
-
.render(
|
|
187
|
-
lang="en",
|
|
188
|
-
title=title,
|
|
189
|
-
message=message,
|
|
190
|
-
has_error=has_error
|
|
191
|
-
)
|
|
192
|
-
.encode("utf-8")
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class OAuthCallbackHttpServer(HTTPServer):
|
|
197
|
-
"""
|
|
198
|
-
Simplistic HTTP Server to provide local callback URL for oauth2 provider
|
|
199
|
-
"""
|
|
200
|
-
|
|
201
|
-
def __init__(self, port):
|
|
202
|
-
super().__init__(("", port), OAuthRedirectHandler)
|
|
203
|
-
|
|
204
|
-
self._code = None
|
|
205
|
-
|
|
206
|
-
def get_code(self):
|
|
207
|
-
return self._code
|
|
208
|
-
|
|
209
|
-
@property
|
|
210
|
-
def callback_url(self):
|
|
211
|
-
return f"http://localhost:{self.server_port}"
|
|
212
|
-
|
|
213
|
-
def wait_for_code(self, attempts: int = 3, timeout_per_attempt=10) -> Optional[str]:
|
|
214
|
-
"""
|
|
215
|
-
Wait for the server to open the callback page containing the code query parameter.
|
|
216
|
-
|
|
217
|
-
It tries for #attempts with a timeout of #timeout_per_attempts for each attempt.
|
|
218
|
-
This prevents the CLI from getting stuck by unsolved callback URls
|
|
219
|
-
|
|
220
|
-
:param attempts: Amount of attempts
|
|
221
|
-
:param timeout_per_attempt: Timeout for each attempt to be successful
|
|
222
|
-
:return: Code from callback page or None if the callback page is not called successfully
|
|
223
|
-
"""
|
|
224
|
-
for i in range(0, attempts):
|
|
225
|
-
try:
|
|
226
|
-
_method_with_timeout(self.handle_request, timeout_seconds=timeout_per_attempt)
|
|
227
|
-
except TimeoutException:
|
|
228
|
-
continue
|
|
229
|
-
if self.get_code() is not None:
|
|
230
|
-
return self.get_code()
|
|
231
|
-
|
|
232
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|