oauth2-cli-auth 1.0.2__tar.gz → 1.2.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.0.2 → oauth2_cli_auth-1.2.0}/PKG-INFO +23 -3
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/README.md +22 -2
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/oauth2_cli_auth/__init__.py +1 -1
- oauth2_cli_auth-1.2.0/oauth2_cli_auth/_timeout.py +23 -0
- oauth2_cli_auth-1.2.0/oauth2_cli_auth/_urllib_util.py +25 -0
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/oauth2_cli_auth/code_grant.py +18 -7
- oauth2_cli_auth-1.2.0/oauth2_cli_auth/conftest.py +12 -0
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/oauth2_cli_auth/http_server.py +20 -3
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/pyproject.toml +2 -2
- oauth2_cli_auth-1.0.2/oauth2_cli_auth/success.html +0 -118
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/LICENSE +0 -0
- {oauth2_cli_auth-1.0.2 → oauth2_cli_auth-1.2.0}/oauth2_cli_auth/simplified_flow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: oauth2-cli-auth
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.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
|
|
@@ -60,9 +60,29 @@ pip install oauth2-cli-auth
|
|
|
60
60
|
|
|
61
61
|
## Usage
|
|
62
62
|
|
|
63
|
-
### Simple
|
|
63
|
+
### Simple with OIDC well known configuration endpoint
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
This should work for every provider supporting OIDC e.g. `gitlab.com`:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from oauth2_cli_auth import get_access_token_with_browser_open, OAuth2ClientInfo
|
|
69
|
+
|
|
70
|
+
client_info = OAuth2ClientInfo.from_oidc_endpoint(
|
|
71
|
+
"https://gitlab.com/.well-known/openid-configuration",
|
|
72
|
+
client_id="my-client-id",
|
|
73
|
+
scopes=["openid"]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
token = get_access_token_with_browser_open(client_info)
|
|
78
|
+
print(f"Obtained token '{token}'")
|
|
79
|
+
except ValueError:
|
|
80
|
+
print("Failed to obtain token")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Simple with manual endpoint specification
|
|
84
|
+
|
|
85
|
+
The following should work for almost all use cases, for rest please check the lib docs.
|
|
66
86
|
|
|
67
87
|
```python
|
|
68
88
|
from oauth2_cli_auth import get_access_token_with_browser_open, OAuth2ClientInfo
|
|
@@ -35,9 +35,29 @@ pip install oauth2-cli-auth
|
|
|
35
35
|
|
|
36
36
|
## Usage
|
|
37
37
|
|
|
38
|
-
### Simple
|
|
38
|
+
### Simple with OIDC well known configuration endpoint
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
This should work for every provider supporting OIDC e.g. `gitlab.com`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from oauth2_cli_auth import get_access_token_with_browser_open, OAuth2ClientInfo
|
|
44
|
+
|
|
45
|
+
client_info = OAuth2ClientInfo.from_oidc_endpoint(
|
|
46
|
+
"https://gitlab.com/.well-known/openid-configuration",
|
|
47
|
+
client_id="my-client-id",
|
|
48
|
+
scopes=["openid"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
token = get_access_token_with_browser_open(client_info)
|
|
53
|
+
print(f"Obtained token '{token}'")
|
|
54
|
+
except ValueError:
|
|
55
|
+
print("Failed to obtain token")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Simple with manual endpoint specification
|
|
59
|
+
|
|
60
|
+
The following should work for almost all use cases, for rest please check the lib docs.
|
|
41
61
|
|
|
42
62
|
```python
|
|
43
63
|
from oauth2_cli_auth import get_access_token_with_browser_open, OAuth2ClientInfo
|
|
@@ -3,5 +3,5 @@ Authenticate against OAuth2 Provider in Python CLIs
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from oauth2_cli_auth.http_server import OAuthCallbackHttpServer
|
|
6
|
-
from oauth2_cli_auth.code_grant import OAuth2ClientInfo, exchange_code_for_access_token, get_auth_url, open_browser
|
|
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,23 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TimeoutException(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _timeout_handler(signum, frame):
|
|
9
|
+
raise TimeoutException("Operation timed out")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _method_with_timeout(your_method, timeout_seconds=5, *args, **kwargs):
|
|
13
|
+
signal.signal(signal.SIGALRM, _timeout_handler)
|
|
14
|
+
signal.alarm(timeout_seconds)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
result = your_method(*args, **kwargs)
|
|
18
|
+
except TimeoutException as te:
|
|
19
|
+
raise te
|
|
20
|
+
finally:
|
|
21
|
+
signal.alarm(0) # Reset the alarm
|
|
22
|
+
|
|
23
|
+
return result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import urllib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _urlopen_with_backoff(url, max_retries=3, base_delay=1, timeout=15):
|
|
7
|
+
retries = 0
|
|
8
|
+
|
|
9
|
+
while retries < max_retries:
|
|
10
|
+
try:
|
|
11
|
+
response = urllib.request.urlopen(url, timeout=timeout)
|
|
12
|
+
return response
|
|
13
|
+
except Exception:
|
|
14
|
+
retries += 1
|
|
15
|
+
delay = base_delay * (2 ** retries)
|
|
16
|
+
time.sleep(delay)
|
|
17
|
+
|
|
18
|
+
raise Exception(f"Failed to open URL after {max_retries} retries")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_json(url_or_request: str | urllib.request.Request) -> dict:
|
|
22
|
+
with _urlopen_with_backoff(url_or_request) as response:
|
|
23
|
+
response_data = response.read().decode('utf-8')
|
|
24
|
+
json_response = json.loads(response_data)
|
|
25
|
+
return json_response
|
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
import urllib.request
|
|
4
4
|
import urllib.parse
|
|
5
5
|
import base64
|
|
6
|
-
import
|
|
6
|
+
from oauth2_cli_auth._urllib_util import _load_json
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@dataclass
|
|
@@ -20,6 +20,21 @@ class OAuth2ClientInfo:
|
|
|
20
20
|
scopes: list[str]
|
|
21
21
|
"""List of scopes to request"""
|
|
22
22
|
|
|
23
|
+
@staticmethod
|
|
24
|
+
def from_oidc_endpoint(oidc_config_endpoint: str, client_id: str, scopes: list[str]):
|
|
25
|
+
config = load_oidc_config(oidc_config_endpoint)
|
|
26
|
+
return OAuth2ClientInfo(
|
|
27
|
+
authorization_url=config.get("authorization_endpoint"),
|
|
28
|
+
token_url=config.get("token_endpoint"),
|
|
29
|
+
client_id=client_id,
|
|
30
|
+
scopes=scopes,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_oidc_config(odic_well_known_endpoint: str) -> dict:
|
|
35
|
+
config = _load_json(odic_well_known_endpoint)
|
|
36
|
+
return config
|
|
37
|
+
|
|
23
38
|
|
|
24
39
|
def open_browser(url: str) -> None:
|
|
25
40
|
"""
|
|
@@ -71,10 +86,6 @@ def exchange_code_for_access_token(client_info: OAuth2ClientInfo, redirect_uri:
|
|
|
71
86
|
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
|
|
72
87
|
|
|
73
88
|
request = urllib.request.Request(client_info.token_url, data=encoded_data, headers=headers)
|
|
74
|
-
|
|
75
|
-
response_data = response.read().decode('utf-8')
|
|
76
|
-
json_response = json.loads(response_data)
|
|
77
|
-
|
|
78
|
-
access_token = json_response.get(access_token_field)
|
|
89
|
+
json_response = _load_json(request)
|
|
79
90
|
|
|
80
|
-
return
|
|
91
|
+
return json_response.get(access_token_field)
|
|
@@ -2,6 +2,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
2
2
|
from string import Template
|
|
3
3
|
from typing import Optional
|
|
4
4
|
from urllib.parse import parse_qs, urlparse
|
|
5
|
+
from oauth2_cli_auth._timeout import _method_with_timeout, TimeoutException
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class CallbackPageTemplate:
|
|
@@ -150,10 +151,13 @@ class CallbackPageTemplate:
|
|
|
150
151
|
class OAuthRedirectHandler(BaseHTTPRequestHandler):
|
|
151
152
|
callback_template = CallbackPageTemplate()
|
|
152
153
|
|
|
154
|
+
def log_message(self, format, *args):
|
|
155
|
+
# silence the log messages
|
|
156
|
+
pass
|
|
157
|
+
|
|
153
158
|
def do_GET(self):
|
|
154
159
|
params = parse_qs(urlparse(self.path).query)
|
|
155
160
|
|
|
156
|
-
|
|
157
161
|
has_error = "code" not in params or len(params['code']) != 1 or params['code'][0].strip() == ""
|
|
158
162
|
|
|
159
163
|
if has_error:
|
|
@@ -197,9 +201,22 @@ class OAuthCallbackHttpServer(HTTPServer):
|
|
|
197
201
|
def callback_url(self):
|
|
198
202
|
return f"http://localhost:{self.server_port}"
|
|
199
203
|
|
|
200
|
-
def wait_for_code(self, attempts: int = 3) -> Optional[int]:
|
|
204
|
+
def wait_for_code(self, attempts: int = 3, timeout_per_attempt=10) -> Optional[int]:
|
|
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
|
+
"""
|
|
201
215
|
for i in range(0, attempts):
|
|
202
|
-
|
|
216
|
+
try:
|
|
217
|
+
_method_with_timeout(self.handle_request, timeout_seconds=timeout_per_attempt)
|
|
218
|
+
except TimeoutException:
|
|
219
|
+
continue
|
|
203
220
|
if self.get_code() is not None:
|
|
204
221
|
return self.get_code()
|
|
205
222
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "oauth2-cli-auth"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Authenticate against OAuth2 Provider in Python CLIs"
|
|
5
5
|
authors = ["Timo Reymann <mail@timo-reymann.de>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -43,7 +43,7 @@ coverage = "^7.3.2"
|
|
|
43
43
|
pdoc3 = "^0.10.0"
|
|
44
44
|
|
|
45
45
|
[tool.coverage.run]
|
|
46
|
-
omit = [".*", "*/site-packages/*", "
|
|
46
|
+
omit = [".*", "*/site-packages/*", "*/*_test.py"]
|
|
47
47
|
|
|
48
48
|
[tool.coverage.report]
|
|
49
49
|
fail_under = 70
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<html lang="en">
|
|
2
|
-
|
|
3
|
-
<head>
|
|
4
|
-
<title>bcs</title>
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
-
<meta name="charset" content="utf-8">
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
body {
|
|
14
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
html, body {
|
|
18
|
-
display: flex;
|
|
19
|
-
align-items: center;
|
|
20
|
-
justify-content: center;
|
|
21
|
-
height: 100%;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.message {
|
|
25
|
-
text-align: center;
|
|
26
|
-
padding: 40px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.animation-ctn {
|
|
30
|
-
text-align: center;
|
|
31
|
-
margin-top: 5em;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@keyframes checkmark {
|
|
35
|
-
0% {
|
|
36
|
-
stroke-dashoffset: 100px
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
100% {
|
|
40
|
-
stroke-dashoffset: 0px
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@keyframes checkmark-circle {
|
|
45
|
-
0% {
|
|
46
|
-
stroke-dashoffset: 480px
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
100% {
|
|
50
|
-
stroke-dashoffset: 960px
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
@keyframes colored-circle {
|
|
55
|
-
0% {
|
|
56
|
-
opacity: 0
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
100% {
|
|
60
|
-
opacity: 100
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.icon svg polyline {
|
|
66
|
-
-webkit-animation: checkmark 0.25s ease-in-out 0.7s backwards;
|
|
67
|
-
animation: checkmark 0.25s ease-in-out 0.7s backwards
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.icon svg circle {
|
|
71
|
-
-webkit-animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
72
|
-
animation: checkmark-circle 0.6s ease-in-out backwards;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.icon svg circle#colored {
|
|
76
|
-
-webkit-animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
77
|
-
animation: colored-circle 0.6s ease-in-out 0.7s backwards;
|
|
78
|
-
}
|
|
79
|
-
</style>
|
|
80
|
-
</head>
|
|
81
|
-
|
|
82
|
-
<body>
|
|
83
|
-
<div class="message">
|
|
84
|
-
<div class="animation-ctn">
|
|
85
|
-
<div class="icon">
|
|
86
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
87
|
-
<g fill="none" stroke="#22AE73" stroke-width="2">
|
|
88
|
-
<circle cx="77" cy="77" r="72"
|
|
89
|
-
style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
90
|
-
<circle id="colored" fill="#22AE73" cx="77" cy="77" r="72"
|
|
91
|
-
style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
92
|
-
<polyline class="st0" stroke="#fff" stroke-width="10" points="43.5,77.8 63.7,97.9 112.2,49.4 "
|
|
93
|
-
style="stroke-dasharray:100px, 100px; stroke-dashoffset: 200px;"/>
|
|
94
|
-
</g>
|
|
95
|
-
</svg>
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
<div class="animation-ctn">
|
|
100
|
-
<div class="icon">
|
|
101
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="154px" height="154px">
|
|
102
|
-
<g fill="none" stroke="#F44812" stroke-width="2">
|
|
103
|
-
<circle cx="77" cy="77" r="72"
|
|
104
|
-
style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
105
|
-
<circle id="colored" fill="#F44812" cx="77" cy="77" r="72"
|
|
106
|
-
style="stroke-dasharray:480px, 480px; stroke-dashoffset: 960px;"></circle>
|
|
107
|
-
<polyline class="st0" stroke="#fff" stroke-width="10" points="43.5,77.8 112.2,77.8 "
|
|
108
|
-
style="stroke-dasharray:100px, 100px; stroke-dashoffset: 200px;"/>
|
|
109
|
-
</g>
|
|
110
|
-
</svg>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<h1>$title</h1>
|
|
115
|
-
<p>$message</p>
|
|
116
|
-
</div>
|
|
117
|
-
</body>
|
|
118
|
-
</html>
|
|
File without changes
|
|
File without changes
|