aiohttp-msal 1.0.1__py3-none-any.whl → 1.0.3__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 +40 -62
- 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.1.dist-info → aiohttp_msal-1.0.3.dist-info}/METADATA +1 -1
- aiohttp_msal-1.0.3.dist-info/RECORD +11 -0
- aiohttp_msal-1.0.1.dist-info/RECORD +0 -10
- {aiohttp_msal-1.0.1.dist-info → aiohttp_msal-1.0.3.dist-info}/WHEEL +0 -0
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 cached_property,
|
|
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,
|
|
@@ -33,32 +34,10 @@ HTTP_DELETE = "delete"
|
|
|
33
34
|
HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
|
|
34
35
|
|
|
35
36
|
DEFAULT_SCOPES = ["User.Read", "User.Read.All"]
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
# These keys will be used on the aiohttp session
|
|
57
|
-
TOKEN_CACHE = "token_cache"
|
|
58
37
|
FLOW_CACHE = "flow_cache"
|
|
59
|
-
USER_EMAIL = "mail"
|
|
60
38
|
|
|
61
39
|
|
|
40
|
+
@attrs.define()
|
|
62
41
|
class AsyncMSAL:
|
|
63
42
|
"""AsycMSAL class.
|
|
64
43
|
|
|
@@ -70,53 +49,48 @@ class AsyncMSAL:
|
|
|
70
49
|
Use until such time as MSAL Python gets a true async version.
|
|
71
50
|
"""
|
|
72
51
|
|
|
52
|
+
session: Session | dict[str, Any]
|
|
53
|
+
save_callback: Callable[[Session | dict[str, Any]], None] | None = None
|
|
54
|
+
"""Called if the token cache changes. Optional.
|
|
55
|
+
Not required when the session parameter is an aiohttp_session.Session.
|
|
56
|
+
"""
|
|
57
|
+
app_kwargs: dict[str, Any] | None = None
|
|
58
|
+
"""ConfidentialClientApplication kwargs."""
|
|
73
59
|
client_session: ClassVar[ClientSession | None] = None
|
|
74
60
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
session: Session | dict[str, Any],
|
|
78
|
-
save_callback: Callable[[Session | dict[str, Any]], None] | None = None,
|
|
79
|
-
):
|
|
80
|
-
"""Init the class.
|
|
61
|
+
token_cache_key: str = "token_cache"
|
|
62
|
+
user_email_key: str = "mail"
|
|
81
63
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
64
|
+
@cached_property
|
|
65
|
+
def app(self) -> ConfidentialClientApplication:
|
|
66
|
+
"""Get the app."""
|
|
67
|
+
kwargs = {
|
|
68
|
+
"client_id": ENV.SP_APP_ID,
|
|
69
|
+
"client_credential": ENV.SP_APP_PW,
|
|
70
|
+
"authority": ENV.SP_AUTHORITY,
|
|
71
|
+
"validate_authority": False,
|
|
72
|
+
"token_cache": self.token_cache,
|
|
73
|
+
}
|
|
74
|
+
if self.app_kwargs:
|
|
75
|
+
kwargs.update(self.app_kwargs)
|
|
76
|
+
return ConfidentialClientApplication(**kwargs)
|
|
89
77
|
|
|
90
78
|
@cached_property
|
|
91
79
|
def token_cache(self) -> SerializableTokenCache:
|
|
92
80
|
"""Get the token_cache."""
|
|
93
81
|
res = SerializableTokenCache()
|
|
94
|
-
if
|
|
95
|
-
res.deserialize(
|
|
82
|
+
if tc := self.session.get(self.token_cache_key):
|
|
83
|
+
res.deserialize(tc)
|
|
96
84
|
return res
|
|
97
85
|
|
|
98
|
-
@cached_property
|
|
99
|
-
def app(self) -> ConfidentialClientApplication:
|
|
100
|
-
"""Create the application using the cache.
|
|
101
|
-
|
|
102
|
-
Based on: https://github.com/Azure-Samples/ms-identity-python-webapp/blob/master/app.py#L76
|
|
103
|
-
"""
|
|
104
|
-
return ConfidentialClientApplication(
|
|
105
|
-
client_id=ENV.SP_APP_ID,
|
|
106
|
-
client_credential=ENV.SP_APP_PW,
|
|
107
|
-
authority=ENV.SP_AUTHORITY, # common/oauth2/v2.0/token'
|
|
108
|
-
validate_authority=False,
|
|
109
|
-
token_cache=self.token_cache,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
86
|
def save_token_cache(self) -> None:
|
|
113
87
|
"""Save the token cache if it changed."""
|
|
114
88
|
if self.token_cache.has_state_changed:
|
|
115
|
-
self.session[
|
|
89
|
+
self.session[self.token_cache_key] = self.token_cache.serialize()
|
|
116
90
|
if self.save_callback:
|
|
117
91
|
self.save_callback(self.session)
|
|
118
92
|
|
|
119
|
-
def
|
|
93
|
+
def initiate_auth_code_flow(
|
|
120
94
|
self,
|
|
121
95
|
redirect_uri: str,
|
|
122
96
|
scopes: list[str] | None = None,
|
|
@@ -124,8 +98,8 @@ class AsyncMSAL:
|
|
|
124
98
|
**kwargs: Any,
|
|
125
99
|
) -> str:
|
|
126
100
|
"""First step - Start the flow."""
|
|
127
|
-
self.session
|
|
128
|
-
self.session
|
|
101
|
+
self.session.pop(self.token_cache_key, None)
|
|
102
|
+
self.session.pop(self.user_email_key, None)
|
|
129
103
|
self.session[FLOW_CACHE] = res = self.app.initiate_auth_code_flow(
|
|
130
104
|
scopes or DEFAULT_SCOPES,
|
|
131
105
|
redirect_uri=redirect_uri,
|
|
@@ -138,19 +112,23 @@ class AsyncMSAL:
|
|
|
138
112
|
# https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.initiate_auth_code_flow
|
|
139
113
|
return str(res["auth_uri"])
|
|
140
114
|
|
|
141
|
-
def acquire_token_by_auth_code_flow(
|
|
115
|
+
def acquire_token_by_auth_code_flow(
|
|
116
|
+
self, auth_response: Any, scopes: list[str] | None = None
|
|
117
|
+
) -> None:
|
|
142
118
|
"""Second step - Acquire token."""
|
|
143
119
|
# Assume we have it in the cache (added by /login)
|
|
144
120
|
# will raise keryerror if no cache
|
|
145
121
|
auth_code_flow = self.session.pop(FLOW_CACHE)
|
|
146
|
-
result = self.app.acquire_token_by_auth_code_flow(
|
|
122
|
+
result = self.app.acquire_token_by_auth_code_flow(
|
|
123
|
+
auth_code_flow, auth_response, scopes=scopes
|
|
124
|
+
)
|
|
147
125
|
if "error" in result:
|
|
148
126
|
raise web.HTTPBadRequest(text=str(result["error"]))
|
|
149
127
|
if "id_token_claims" not in result:
|
|
150
128
|
raise web.HTTPBadRequest(text=f"Expected id_token_claims in {result}")
|
|
151
129
|
self.save_token_cache()
|
|
152
130
|
if tok := result.get("id_token_claims"):
|
|
153
|
-
self.session[
|
|
131
|
+
self.session[self.user_email_key] = tok.get("preferred_username")
|
|
154
132
|
|
|
155
133
|
async def async_acquire_token_by_auth_code_flow(self, auth_response: Any) -> None:
|
|
156
134
|
"""Second step - Acquire token, async version."""
|
|
@@ -222,7 +200,7 @@ class AsyncMSAL:
|
|
|
222
200
|
@property
|
|
223
201
|
def mail(self) -> str:
|
|
224
202
|
"""User email."""
|
|
225
|
-
return self.session.get(
|
|
203
|
+
return self.session.get(self.user_email_key, "")
|
|
226
204
|
|
|
227
205
|
@property
|
|
228
206
|
def manager_mail(self) -> str:
|
|
@@ -242,4 +220,4 @@ class AsyncMSAL:
|
|
|
242
220
|
@property
|
|
243
221
|
def authenticated(self) -> bool:
|
|
244
222
|
"""If the user is logged in."""
|
|
245
|
-
return bool(self.session.get(
|
|
223
|
+
return bool(self.session.get(self.user_email_key))
|
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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
|
|
2
|
+
aiohttp_msal/msal_async.py,sha256=urQvaMTi0mJnCboCsj8A9F9VpWcjPOYaechpZ5XrnbY,8153
|
|
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.3.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
10
|
+
aiohttp_msal-1.0.3.dist-info/METADATA,sha256=raujaCawOODO6HYM79crhTECU28f_IjlkzIPmR8ck48,4514
|
|
11
|
+
aiohttp_msal-1.0.3.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
|
|
2
|
-
aiohttp_msal/msal_async.py,sha256=p0OkF48ZDmCAUYN4qjn3UdMyH1oS4iBQSB5Z_C3yIBU,8816
|
|
3
|
-
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
4
|
-
aiohttp_msal/routes.py,sha256=8wU2jU9qSlqH5fq9kvkBZHAgrxQdmB-DvtQC-WlXbh0,8135
|
|
5
|
-
aiohttp_msal/settings.py,sha256=ttbqGb2X1r7DsLvKb1AlDDKBYZWjIwHLHI-Sa-8K-lI,1562
|
|
6
|
-
aiohttp_msal/settings_base.py,sha256=tRbjgphR1tvHCrFCcfOUhoFAyUnq_XnJBVO4NNiPdNg,3150
|
|
7
|
-
aiohttp_msal/user_info.py,sha256=tO-vA_kxPseHseWxNlhGc_NlDfgJGdf1OMCaGmvDf8Q,1875
|
|
8
|
-
aiohttp_msal-1.0.1.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
9
|
-
aiohttp_msal-1.0.1.dist-info/METADATA,sha256=NJEzFuQyWHai3QcqvyTfeAWWfjq98nucwoYFt3qIOvc,4514
|
|
10
|
-
aiohttp_msal-1.0.1.dist-info/RECORD,,
|
|
File without changes
|