aiohttp-msal 1.0.0__py3-none-any.whl → 1.0.2__py3-none-any.whl
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.
- aiohttp_msal/msal_async.py +38 -73
- aiohttp_msal/routes.py +1 -1
- aiohttp_msal/settings.py +2 -4
- aiohttp_msal/settings_base.py +0 -5
- aiohttp_msal/user_info.py +1 -28
- aiohttp_msal/utils.py +51 -0
- {aiohttp_msal-1.0.0.dist-info → aiohttp_msal-1.0.2.dist-info}/METADATA +4 -4
- aiohttp_msal-1.0.2.dist-info/RECORD +11 -0
- {aiohttp_msal-1.0.0.dist-info → aiohttp_msal-1.0.2.dist-info}/WHEEL +1 -1
- aiohttp_msal-1.0.0.dist-info/RECORD +0 -10
aiohttp_msal/msal_async.py
CHANGED
|
@@ -8,9 +8,10 @@ Once you have the OAuth tokens store in the session, you are free to make reques
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
-
from functools import
|
|
11
|
+
from functools import cached_property, partialmethod
|
|
12
12
|
from typing import Any, ClassVar, Literal, Unpack
|
|
13
13
|
|
|
14
|
+
import attrs
|
|
14
15
|
from aiohttp import web
|
|
15
16
|
from aiohttp.client import (
|
|
16
17
|
ClientResponse,
|
|
@@ -35,30 +36,13 @@ HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
|
|
|
35
36
|
DEFAULT_SCOPES = ["User.Read", "User.Read.All"]
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def async_wrap(func: Callable) -> Callable:
|
|
39
|
-
"""Wrap a function doing I/O to run in an executor thread."""
|
|
40
|
-
|
|
41
|
-
@wraps(func)
|
|
42
|
-
async def run(
|
|
43
|
-
*args: Any,
|
|
44
|
-
loop: asyncio.AbstractEventLoop | None = None,
|
|
45
|
-
executor: Any = None,
|
|
46
|
-
**kwargs: dict[str, Any],
|
|
47
|
-
) -> Callable:
|
|
48
|
-
if loop is None:
|
|
49
|
-
loop = asyncio.get_event_loop()
|
|
50
|
-
pfunc = partial(func, *args, **kwargs)
|
|
51
|
-
return await loop.run_in_executor(executor, pfunc)
|
|
52
|
-
|
|
53
|
-
return run
|
|
54
|
-
|
|
55
|
-
|
|
56
39
|
# These keys will be used on the aiohttp session
|
|
57
40
|
TOKEN_CACHE = "token_cache"
|
|
58
41
|
FLOW_CACHE = "flow_cache"
|
|
59
42
|
USER_EMAIL = "mail"
|
|
60
43
|
|
|
61
44
|
|
|
45
|
+
@attrs.define()
|
|
62
46
|
class AsyncMSAL:
|
|
63
47
|
"""AsycMSAL class.
|
|
64
48
|
|
|
@@ -70,52 +54,37 @@ class AsyncMSAL:
|
|
|
70
54
|
Use until such time as MSAL Python gets a true async version.
|
|
71
55
|
"""
|
|
72
56
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
session: Session | dict[str, Any],
|
|
80
|
-
save_callback: Callable[[Session | dict[str, Any]], None] | None = None,
|
|
81
|
-
):
|
|
82
|
-
"""Init the class.
|
|
57
|
+
session: Session | dict[str, Any]
|
|
58
|
+
save_callback: Callable[[Session | dict[str, Any]], None] | None = None
|
|
59
|
+
"""Called if the token cache changes. Optional.
|
|
60
|
+
Not required when the session parameter is an aiohttp_session.Session.
|
|
61
|
+
"""
|
|
62
|
+
app: ConfidentialClientApplication = attrs.field(init=False)
|
|
83
63
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.session = session
|
|
88
|
-
self.save_callback = save_callback
|
|
89
|
-
if not isinstance(session, Session | dict):
|
|
90
|
-
raise ValueError(f"session or dict-like object required {session}")
|
|
64
|
+
app_kwargs: ClassVar[dict[str, Any] | None] = None
|
|
65
|
+
"""ConfidentialClientApplication kwargs."""
|
|
66
|
+
client_session: ClassVar[ClientSession | None] = None
|
|
91
67
|
|
|
92
|
-
|
|
68
|
+
def __attrs_post_init__(self) -> None:
|
|
69
|
+
"""Init."""
|
|
70
|
+
kwargs = dict(self.app_kwargs) if self.app_kwargs else {}
|
|
71
|
+
for key, val in {
|
|
72
|
+
"client_id": ENV.SP_APP_ID,
|
|
73
|
+
"client_credential": ENV.SP_APP_PW,
|
|
74
|
+
"authority": ENV.SP_AUTHORITY,
|
|
75
|
+
"validate_authority": False,
|
|
76
|
+
"token_cache": self.token_cache,
|
|
77
|
+
}.items():
|
|
78
|
+
kwargs.setdefault(key, val)
|
|
79
|
+
self.app = ConfidentialClientApplication(**kwargs)
|
|
80
|
+
|
|
81
|
+
@cached_property
|
|
93
82
|
def token_cache(self) -> SerializableTokenCache:
|
|
94
83
|
"""Get the token_cache."""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self._token_cache.deserialize(self.session[TOKEN_CACHE])
|
|
100
|
-
|
|
101
|
-
return self._token_cache
|
|
102
|
-
|
|
103
|
-
@property
|
|
104
|
-
def app(self) -> ConfidentialClientApplication:
|
|
105
|
-
"""Create the application using the cache.
|
|
106
|
-
|
|
107
|
-
Based on: https://github.com/Azure-Samples/ms-identity-python-webapp/blob/master/app.py#L76
|
|
108
|
-
"""
|
|
109
|
-
if not self._app:
|
|
110
|
-
token_cache = self.token_cache
|
|
111
|
-
self._app = ConfidentialClientApplication(
|
|
112
|
-
client_id=ENV.SP_APP_ID,
|
|
113
|
-
client_credential=ENV.SP_APP_PW,
|
|
114
|
-
authority=ENV.SP_AUTHORITY, # common/oauth2/v2.0/token'
|
|
115
|
-
validate_authority=False,
|
|
116
|
-
token_cache=token_cache,
|
|
117
|
-
)
|
|
118
|
-
return self._app
|
|
84
|
+
res = SerializableTokenCache()
|
|
85
|
+
if self.session and self.session.get(TOKEN_CACHE):
|
|
86
|
+
res.deserialize(self.session[TOKEN_CACHE])
|
|
87
|
+
return res
|
|
119
88
|
|
|
120
89
|
def save_token_cache(self) -> None:
|
|
121
90
|
"""Save the token cache if it changed."""
|
|
@@ -124,7 +93,7 @@ class AsyncMSAL:
|
|
|
124
93
|
if self.save_callback:
|
|
125
94
|
self.save_callback(self.session)
|
|
126
95
|
|
|
127
|
-
def
|
|
96
|
+
def initiate_auth_code_flow(
|
|
128
97
|
self,
|
|
129
98
|
redirect_uri: str,
|
|
130
99
|
scopes: list[str] | None = None,
|
|
@@ -146,12 +115,16 @@ class AsyncMSAL:
|
|
|
146
115
|
# https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.initiate_auth_code_flow
|
|
147
116
|
return str(res["auth_uri"])
|
|
148
117
|
|
|
149
|
-
def acquire_token_by_auth_code_flow(
|
|
118
|
+
def acquire_token_by_auth_code_flow(
|
|
119
|
+
self, auth_response: Any, scopes: list[str] | None = None
|
|
120
|
+
) -> None:
|
|
150
121
|
"""Second step - Acquire token."""
|
|
151
122
|
# Assume we have it in the cache (added by /login)
|
|
152
123
|
# will raise keryerror if no cache
|
|
153
124
|
auth_code_flow = self.session.pop(FLOW_CACHE)
|
|
154
|
-
result = self.app.acquire_token_by_auth_code_flow(
|
|
125
|
+
result = self.app.acquire_token_by_auth_code_flow(
|
|
126
|
+
auth_code_flow, auth_response, scopes=scopes
|
|
127
|
+
)
|
|
155
128
|
if "error" in result:
|
|
156
129
|
raise web.HTTPBadRequest(text=str(result["error"]))
|
|
157
130
|
if "id_token_claims" not in result:
|
|
@@ -227,18 +200,10 @@ class AsyncMSAL:
|
|
|
227
200
|
get = partialmethod(request_ctx, HTTP_GET)
|
|
228
201
|
post = partialmethod(request_ctx, HTTP_POST)
|
|
229
202
|
|
|
230
|
-
# def get(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
231
|
-
# """GET Request."""
|
|
232
|
-
# return _RequestContextManager(self.request(HTTP_GET, url, **kwargs))
|
|
233
|
-
|
|
234
|
-
# def post(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
235
|
-
# """POST request."""
|
|
236
|
-
# return _RequestContextManager(self.request(HTTP_POST, url, **kwargs))
|
|
237
|
-
|
|
238
203
|
@property
|
|
239
204
|
def mail(self) -> str:
|
|
240
205
|
"""User email."""
|
|
241
|
-
return self.session.get(
|
|
206
|
+
return self.session.get(USER_EMAIL, "")
|
|
242
207
|
|
|
243
208
|
@property
|
|
244
209
|
def manager_mail(self) -> str:
|
aiohttp_msal/routes.py
CHANGED
|
@@ -44,7 +44,7 @@ async def user_login(request: web.Request) -> web.Response:
|
|
|
44
44
|
session[SESSION_REDIRECT] = urljoin(_to, request.match_info.get("to", ""))
|
|
45
45
|
|
|
46
46
|
msredirect = get_route(request, URI_USER_AUTHORIZED.lstrip("/"))
|
|
47
|
-
redir = AsyncMSAL(session).
|
|
47
|
+
redir = AsyncMSAL(session).initiate_auth_code_flow(redirect_uri=msredirect)
|
|
48
48
|
return web.HTTPFound(redir)
|
|
49
49
|
|
|
50
50
|
|
aiohttp_msal/settings.py
CHANGED
|
@@ -9,8 +9,6 @@ from aiohttp_msal.settings_base import VAR_REQ, VAR_REQ_HIDE, SettingsBase
|
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from redis.asyncio import Redis
|
|
12
|
-
else:
|
|
13
|
-
Redis = None
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
@attrs.define
|
|
@@ -28,7 +26,7 @@ class MSALSettings(SettingsBase):
|
|
|
28
26
|
"https://login.microsoftonline.com/common" # For multi-tenant app
|
|
29
27
|
"https://login.microsoftonline.com/Tenant_Name_or_UUID_Here"."""
|
|
30
28
|
|
|
31
|
-
DOMAIN: str =
|
|
29
|
+
DOMAIN: str = attrs.field(metadata=VAR_REQ, default="")
|
|
32
30
|
"""Your domain. Used by routes & Redis functions."""
|
|
33
31
|
|
|
34
32
|
COOKIE_NAME: str = "AIOHTTP_SESSION"
|
|
@@ -41,7 +39,7 @@ class MSALSettings(SettingsBase):
|
|
|
41
39
|
|
|
42
40
|
REDIS: str = "redis://redis1:6379"
|
|
43
41
|
"""OPTIONAL: Redis database connection used by app_init_redis_session()."""
|
|
44
|
-
database: Redis =
|
|
42
|
+
database: "Redis" = attrs.field(init=False)
|
|
45
43
|
"""Store the Redis connection when using app_init_redis_session()."""
|
|
46
44
|
|
|
47
45
|
|
aiohttp_msal/settings_base.py
CHANGED
|
@@ -14,11 +14,6 @@ VAR_REQ = {KEY_REQ: True}
|
|
|
14
14
|
VAR_HIDE = {KEY_HIDE: True}
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def _is_hidden(atr: attrs.Attribute) -> bool:
|
|
18
|
-
"""Is this field hidden."""
|
|
19
|
-
return bool(atr.metadata.get(KEY_HIDE))
|
|
20
|
-
|
|
21
|
-
|
|
22
17
|
@attrs.define
|
|
23
18
|
class SettingsBase:
|
|
24
19
|
"""Retrieve Settings from environment variables.
|
aiohttp_msal/user_info.py
CHANGED
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
"""Graph User Info."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
from collections.abc import Awaitable, Callable
|
|
5
|
-
from functools import wraps
|
|
6
|
-
from typing import ParamSpec, TypeVar
|
|
7
|
-
|
|
8
3
|
from aiohttp_msal.msal_async import AsyncMSAL
|
|
9
|
-
|
|
10
|
-
_T = TypeVar("_T")
|
|
11
|
-
_P = ParamSpec("_P")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def retry(func: Callable[_P, Awaitable[_T]]) -> Callable[_P, Awaitable[_T]]:
|
|
15
|
-
"""Retry if tenacity is installed."""
|
|
16
|
-
|
|
17
|
-
@wraps(func)
|
|
18
|
-
async def _retry(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
|
19
|
-
"""Retry the request."""
|
|
20
|
-
retries = [2, 4, 8]
|
|
21
|
-
while True:
|
|
22
|
-
try:
|
|
23
|
-
res = await func(*args, **kwargs)
|
|
24
|
-
return res
|
|
25
|
-
except Exception as err:
|
|
26
|
-
if retries:
|
|
27
|
-
await asyncio.sleep(retries.pop())
|
|
28
|
-
else:
|
|
29
|
-
raise err
|
|
30
|
-
|
|
31
|
-
return _retry
|
|
4
|
+
from aiohttp_msal.utils import retry
|
|
32
5
|
|
|
33
6
|
|
|
34
7
|
@retry
|
aiohttp_msal/utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Graph User Info."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import ParamSpec, TypeVar, Any
|
|
7
|
+
from functools import partial
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
P = ParamSpec("P")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def async_wrap(
|
|
15
|
+
func: Callable[..., T],
|
|
16
|
+
) -> Callable[..., Awaitable[T]]:
|
|
17
|
+
"""Wrap a function doing I/O to run in an executor thread."""
|
|
18
|
+
|
|
19
|
+
@wraps(func)
|
|
20
|
+
async def run(
|
|
21
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
22
|
+
executor: Any = None,
|
|
23
|
+
*args: Any,
|
|
24
|
+
**kwargs: Any,
|
|
25
|
+
) -> T:
|
|
26
|
+
if loop is None:
|
|
27
|
+
loop = asyncio.get_event_loop()
|
|
28
|
+
pfunc = partial(func, *args, **kwargs)
|
|
29
|
+
return await loop.run_in_executor(executor, pfunc)
|
|
30
|
+
|
|
31
|
+
return run
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def retry(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
35
|
+
"""Retry if tenacity is installed."""
|
|
36
|
+
|
|
37
|
+
@wraps(func)
|
|
38
|
+
async def _retry(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
39
|
+
"""Retry the request."""
|
|
40
|
+
retries = [2, 4, 8]
|
|
41
|
+
while True:
|
|
42
|
+
try:
|
|
43
|
+
res = await func(*args, **kwargs)
|
|
44
|
+
return res
|
|
45
|
+
except Exception as err:
|
|
46
|
+
if retries:
|
|
47
|
+
await asyncio.sleep(retries.pop())
|
|
48
|
+
else:
|
|
49
|
+
raise err
|
|
50
|
+
|
|
51
|
+
return _retry
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: aiohttp-msal
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Helper Library to use the Microsoft Authentication Library (MSAL) with aiohttp
|
|
5
5
|
Keywords: aiohttp,asyncio,msal,oauth
|
|
6
6
|
Author: Johann Kellerman
|
|
@@ -23,7 +23,7 @@ Project-URL: Homepage, https://github.com/kellerza/aiohttp_msal
|
|
|
23
23
|
Provides-Extra: aioredis
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
|
-
# aiohttp_msal Python library
|
|
26
|
+
# Async based MSAL helper for aiohttp - aiohttp_msal Python library
|
|
27
27
|
|
|
28
28
|
Authorization Code Flow Helper. Learn more about auth-code-flow at
|
|
29
29
|
<https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow>
|
|
@@ -124,8 +124,8 @@ from aiohttp_msal.redis_tools import get_session
|
|
|
124
124
|
def main()
|
|
125
125
|
# Uses the redis.asyncio driver to retrieve the current token
|
|
126
126
|
# Will update the token_cache if a RefreshToken was used
|
|
127
|
-
|
|
128
|
-
client = GraphClient(
|
|
127
|
+
ses = asyncio.run(get_session(MYEMAIL))
|
|
128
|
+
client = GraphClient(ses.get_token)
|
|
129
129
|
# ...
|
|
130
130
|
# use the Graphclient
|
|
131
131
|
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
|
|
2
|
+
aiohttp_msal/msal_async.py,sha256=JUtyTro57rLiHQYqgYq8LFv3keznyhISP1emgN3y31E,8232
|
|
3
|
+
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
4
|
+
aiohttp_msal/routes.py,sha256=WyLBuoPMkkG6Cx4gFUu_ER71FyJbeXKhOQRQu5ALG2M,8138
|
|
5
|
+
aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
|
|
6
|
+
aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
|
|
7
|
+
aiohttp_msal/user_info.py,sha256=lxjFxjm16rvC-0LS81y7SG5pCOa5Zl0s62uxi97yu_k,1171
|
|
8
|
+
aiohttp_msal/utils.py,sha256=SgGpE1eFdVh48FaKvtbnQqJKTReXa9OPBKiYGY7SYq8,1303
|
|
9
|
+
aiohttp_msal-1.0.2.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
10
|
+
aiohttp_msal-1.0.2.dist-info/METADATA,sha256=b9HRcY4HaOKhXVhBQIuYK-h0Cia9g922FjOdTyOuPpw,4514
|
|
11
|
+
aiohttp_msal-1.0.2.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
aiohttp_msal/__init__.py,sha256=867ca27f2272908ecd32f33ddcaac722c4e5aeb7c2554ae929d6d128be86b9bc,4027
|
|
2
|
-
aiohttp_msal/msal_async.py,sha256=8efdf9608c55e41f99ed66294a18303192e921739f49986cf90a015b07a50c55,9470
|
|
3
|
-
aiohttp_msal/redis_tools.py,sha256=ea40b0d3fcc341cbc872c25a3df1becc752f4f7bf392b37248d4d5e72d6d7241,6539
|
|
4
|
-
aiohttp_msal/routes.py,sha256=f305368d4f6a4a5a87e5fabd92f901647020af141d981f83bed402f969576e1d,8135
|
|
5
|
-
aiohttp_msal/settings.py,sha256=b6d6ea19bd97d6bec3b0bbca6f50250c32816195a32301cb1c8f926bef0afa52,1562
|
|
6
|
-
aiohttp_msal/settings_base.py,sha256=b516e3829851d6dbc70ab14271f394868140c949eafd79c90553b834d88f74d8,3150
|
|
7
|
-
aiohttp_msal/user_info.py,sha256=b4efaf03f9313ec787b1e5b136584673f3650df80919d7f538c09a1a6bc37fc4,1875
|
|
8
|
-
aiohttp_msal-1.0.0.dist-info/WHEEL,sha256=76443c98c0efcfdd1191eac5fa1d8223dba1c474dbd47676674a255e7ca48770,79
|
|
9
|
-
aiohttp_msal-1.0.0.dist-info/METADATA,sha256=dd4a69bc47da5e6c559fda12c41505fdc047c8edde36d498d25a93fc0b7b4ec8,4478
|
|
10
|
-
aiohttp_msal-1.0.0.dist-info/RECORD,,
|