aiohttp-msal 1.0.4__py3-none-any.whl → 1.0.6__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/__init__.py +2 -2
- aiohttp_msal/helpers.py +11 -71
- aiohttp_msal/msal_async.py +107 -5
- aiohttp_msal/redis_tools.py +33 -22
- aiohttp_msal/routes.py +6 -21
- {aiohttp_msal-1.0.4.dist-info → aiohttp_msal-1.0.6.dist-info}/METADATA +3 -1
- aiohttp_msal-1.0.6.dist-info/RECORD +11 -0
- {aiohttp_msal-1.0.4.dist-info → aiohttp_msal-1.0.6.dist-info}/WHEEL +1 -1
- aiohttp_msal-1.0.4.dist-info/RECORD +0 -11
aiohttp_msal/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ from aiohttp_msal.msal_async import AsyncMSAL
|
|
|
15
15
|
from aiohttp_msal.settings import ENV
|
|
16
16
|
from aiohttp_msal.utils import retry
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
_LOG = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
_T = TypeVar("_T")
|
|
21
21
|
Ts = TypeVarTuple("Ts")
|
|
@@ -107,7 +107,7 @@ async def app_init_redis_session(
|
|
|
107
107
|
else:
|
|
108
108
|
await check_proxy()
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
_LOG.info("Connect to Redis %s", ENV.REDIS)
|
|
111
111
|
try:
|
|
112
112
|
ENV.database = from_url(ENV.REDIS)
|
|
113
113
|
# , encoding="utf-8", decode_responses=True
|
aiohttp_msal/helpers.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"""Graph User Info."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping, Sequence
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from aiohttp import web
|
|
7
|
-
from aiohttp_session import get_session
|
|
8
7
|
|
|
9
|
-
from aiohttp_msal import ENV
|
|
10
|
-
from aiohttp_msal.msal_async import AsyncMSAL
|
|
8
|
+
from aiohttp_msal.settings import ENV
|
|
11
9
|
from aiohttp_msal.utils import retry
|
|
12
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from aiohttp_msal.msal_async import AsyncMSAL
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
@retry
|
|
15
|
-
async def get_user_info(aiomsal: AsyncMSAL) -> None:
|
|
16
|
+
async def get_user_info(aiomsal: "AsyncMSAL") -> None:
|
|
16
17
|
"""Load user info from MS graph API. Requires User.Read permissions."""
|
|
17
18
|
async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
|
|
18
19
|
body = await res.json()
|
|
@@ -26,7 +27,7 @@ async def get_user_info(aiomsal: AsyncMSAL) -> None:
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
@retry
|
|
29
|
-
async def get_manager_info(aiomsal: AsyncMSAL) -> None:
|
|
30
|
+
async def get_manager_info(aiomsal: "AsyncMSAL") -> None:
|
|
30
31
|
"""Load manager info from MS graph API. Requires User.Read.All permissions."""
|
|
31
32
|
async with aiomsal.get("https://graph.microsoft.com/v1.0/me/manager") as res:
|
|
32
33
|
body = await res.json()
|
|
@@ -69,68 +70,7 @@ def html_wrap(msgs: Sequence[str]) -> str:
|
|
|
69
70
|
"""
|
|
70
71
|
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
request: web.Request,
|
|
77
|
-
asyncmsal_class: type[TA] = AsyncMSAL, # type:ignore[assignment]
|
|
78
|
-
get_info: Literal["user", "manager", ""] = "manager",
|
|
79
|
-
) -> tuple[TA | None, list[str]]:
|
|
80
|
-
"""Parse the MS auth response."""
|
|
81
|
-
# Expecting response_mode="form_post"
|
|
82
|
-
auth_response = dict(await request.post())
|
|
83
|
-
|
|
84
|
-
msg = list[str]()
|
|
85
|
-
|
|
86
|
-
# Ensure all expected variables were returned...
|
|
87
|
-
if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
|
|
88
|
-
msg.append("Expected 'code', 'session_state', 'state' in auth_response")
|
|
89
|
-
msg.append(f"Received auth_response: {list(auth_response)}")
|
|
90
|
-
return None, msg
|
|
91
|
-
|
|
92
|
-
if not request.cookies.get(ENV.COOKIE_NAME):
|
|
93
|
-
cookies = dict(request.cookies.items())
|
|
94
|
-
msg.append(f"<b>Expected '{ENV.COOKIE_NAME}' in cookies</b>")
|
|
95
|
-
msg.append(html_table(cookies))
|
|
96
|
-
msg.append("Cookie should be set with Samesite:None")
|
|
97
|
-
|
|
98
|
-
session = await get_session(request)
|
|
99
|
-
if session.new:
|
|
100
|
-
msg.append(
|
|
101
|
-
"Warning: This is a new session and may not have all expected values."
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if not session.get(asyncmsal_class.flow_cache_key):
|
|
105
|
-
msg.append(f"<b>Expected '{asyncmsal_class.flow_cache_key}' in session</b>")
|
|
106
|
-
msg.append(html_table(session))
|
|
107
|
-
|
|
108
|
-
aiomsal = asyncmsal_class(session)
|
|
109
|
-
aiomsal.redirect = "/" + aiomsal.redirect.lstrip("/")
|
|
110
|
-
|
|
111
|
-
if msg:
|
|
112
|
-
return aiomsal, msg
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
await aiomsal.async_acquire_token_by_auth_code_flow(auth_response)
|
|
116
|
-
except Exception as err:
|
|
117
|
-
msg.append("<b>Could not get token</b> - async_acquire_token_by_auth_code_flow")
|
|
118
|
-
msg.append(str(err))
|
|
119
|
-
|
|
120
|
-
if not msg:
|
|
121
|
-
try:
|
|
122
|
-
if get_info in ("user", "manager"):
|
|
123
|
-
await get_user_info(aiomsal)
|
|
124
|
-
if get_info == "manager":
|
|
125
|
-
await get_manager_info(aiomsal)
|
|
126
|
-
except Exception as err:
|
|
127
|
-
msg.append("Could not get org info from MS graph")
|
|
128
|
-
msg.append(str(err))
|
|
129
|
-
aiomsal.mail = ""
|
|
130
|
-
aiomsal.name = ""
|
|
131
|
-
|
|
132
|
-
if session.get("mail"):
|
|
133
|
-
for lcb in ENV.login_callback:
|
|
134
|
-
await lcb(aiomsal)
|
|
135
|
-
|
|
136
|
-
return aiomsal, msg
|
|
73
|
+
def get_url(request: web.Request, path: str = "", https_proxy: bool = True) -> str:
|
|
74
|
+
"""Return the full outside URL."""
|
|
75
|
+
res = str(request.url.with_path(path))
|
|
76
|
+
return res.replace("http://", "https://") if https_proxy else res
|
aiohttp_msal/msal_async.py
CHANGED
|
@@ -7,9 +7,10 @@ Once you have the OAuth tokens store in the session, you are free to make reques
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
|
+
import logging
|
|
10
11
|
from collections.abc import Callable
|
|
11
12
|
from functools import cached_property, partialmethod
|
|
12
|
-
from typing import Any, ClassVar, Literal, Unpack, cast
|
|
13
|
+
from typing import Any, ClassVar, Literal, Self, TypeVar, Unpack, cast
|
|
13
14
|
|
|
14
15
|
import attrs
|
|
15
16
|
from aiohttp import web
|
|
@@ -20,12 +21,15 @@ from aiohttp.client import (
|
|
|
20
21
|
_RequestOptions,
|
|
21
22
|
)
|
|
22
23
|
from aiohttp.typedefs import StrOrURL
|
|
23
|
-
from aiohttp_session import Session
|
|
24
|
+
from aiohttp_session import Session, get_session, new_session
|
|
24
25
|
from msal import ConfidentialClientApplication, SerializableTokenCache
|
|
25
26
|
|
|
27
|
+
from aiohttp_msal import helpers
|
|
26
28
|
from aiohttp_msal.settings import ENV
|
|
27
29
|
from aiohttp_msal.utils import dict_property
|
|
28
30
|
|
|
31
|
+
_LOG = logging.getLogger(__name__)
|
|
32
|
+
|
|
29
33
|
HttpMethods = Literal["get", "post", "put", "patch", "delete"]
|
|
30
34
|
HTTP_GET = "get"
|
|
31
35
|
HTTP_POST = "post"
|
|
@@ -34,6 +38,8 @@ HTTP_PATCH = "patch"
|
|
|
34
38
|
HTTP_DELETE = "delete"
|
|
35
39
|
HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
|
|
36
40
|
|
|
41
|
+
T = TypeVar("T")
|
|
42
|
+
|
|
37
43
|
|
|
38
44
|
@attrs.define(slots=False)
|
|
39
45
|
class AsyncMSAL:
|
|
@@ -54,14 +60,46 @@ class AsyncMSAL:
|
|
|
54
60
|
"""
|
|
55
61
|
app_kwargs: dict[str, Any] | None = None
|
|
56
62
|
"""ConfidentialClientApplication kwargs."""
|
|
57
|
-
client_session: ClassVar[ClientSession | None] = None
|
|
58
63
|
|
|
64
|
+
client_session: ClassVar[ClientSession | None] = None
|
|
59
65
|
token_cache_key: ClassVar[str] = "token_cache"
|
|
60
66
|
user_email_key: ClassVar[str] = "mail"
|
|
61
67
|
flow_cache_key: ClassVar[str] = "flow_cache"
|
|
62
|
-
redirect_key = "redirect"
|
|
68
|
+
redirect_key: ClassVar[str] = "redirect"
|
|
63
69
|
default_scopes: ClassVar[list[str]] = ["User.Read", "User.Read.All"]
|
|
64
70
|
|
|
71
|
+
@classmethod
|
|
72
|
+
async def from_request(
|
|
73
|
+
cls,
|
|
74
|
+
request: web.Request,
|
|
75
|
+
/,
|
|
76
|
+
allow_new: bool = False,
|
|
77
|
+
app_kwargs: dict[str, Any] | None = None,
|
|
78
|
+
) -> Self:
|
|
79
|
+
"""Get the session or raise an exception."""
|
|
80
|
+
try:
|
|
81
|
+
session = await get_session(request)
|
|
82
|
+
return cls(session, app_kwargs=app_kwargs)
|
|
83
|
+
except TypeError as err:
|
|
84
|
+
cookie = request.get(ENV.COOKIE_NAME)
|
|
85
|
+
_LOG.error(
|
|
86
|
+
"Invalid session, %s: %s [cookie: %s]",
|
|
87
|
+
"create new" if allow_new else "fail",
|
|
88
|
+
err,
|
|
89
|
+
request.get(ENV.COOKIE_NAME),
|
|
90
|
+
cookie,
|
|
91
|
+
)
|
|
92
|
+
if allow_new:
|
|
93
|
+
return cls(await new_session(request), app_kwargs=app_kwargs)
|
|
94
|
+
|
|
95
|
+
text = "Invalid session. Login for a new session: '/usr/login'"
|
|
96
|
+
|
|
97
|
+
if not cookie:
|
|
98
|
+
cookies = dict(request.cookies.items())
|
|
99
|
+
text = f"Cookie empty. Expected '{ENV.COOKIE_NAME}' in cookies: {list(cookies)}. Cookie should be set with Samesite:None"
|
|
100
|
+
|
|
101
|
+
raise web.HTTPException(text=text) from None
|
|
102
|
+
|
|
65
103
|
@cached_property
|
|
66
104
|
def app(self) -> ConfidentialClientApplication:
|
|
67
105
|
"""Get the app."""
|
|
@@ -118,7 +156,7 @@ class AsyncMSAL:
|
|
|
118
156
|
) -> None:
|
|
119
157
|
"""Second step - Acquire token."""
|
|
120
158
|
# Assume we have it in the cache (added by /login)
|
|
121
|
-
# will raise
|
|
159
|
+
# will raise KeyError if not in cache
|
|
122
160
|
auth_code_flow = self.session.pop(self.flow_cache_key)
|
|
123
161
|
result = self.app.acquire_token_by_auth_code_flow(
|
|
124
162
|
auth_code_flow, auth_response, scopes=scopes
|
|
@@ -208,3 +246,67 @@ class AsyncMSAL:
|
|
|
208
246
|
manager_name = dict_property("session", "m_name")
|
|
209
247
|
manager_mail = dict_property("session", "m_mail")
|
|
210
248
|
redirect = dict_property("session", redirect_key)
|
|
249
|
+
|
|
250
|
+
async def async_acquire_token_by_auth_code_flow_plus(
|
|
251
|
+
self,
|
|
252
|
+
request: web.Request,
|
|
253
|
+
get_info: Literal["user", "manager", ""] = "manager",
|
|
254
|
+
) -> tuple[bool, list[str]]:
|
|
255
|
+
"""Enhanced version of async_acquire_token_by_auth_code_flow. Returns issues.
|
|
256
|
+
|
|
257
|
+
Parse the auth response from the request, checks for valid keys,
|
|
258
|
+
acquire the token and get_info.
|
|
259
|
+
|
|
260
|
+
response_mode for the auth_code flow should be "form_post"
|
|
261
|
+
"""
|
|
262
|
+
auth_response = dict(await request.post())
|
|
263
|
+
assert isinstance(self.session, Session)
|
|
264
|
+
|
|
265
|
+
msg = list[str]()
|
|
266
|
+
|
|
267
|
+
# Ensure all expected variables were returned...
|
|
268
|
+
if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
|
|
269
|
+
msg.append("Expected 'code', 'session_state', 'state' in auth_response")
|
|
270
|
+
msg.append(f"Received auth_response: {list(auth_response)}")
|
|
271
|
+
return False, msg
|
|
272
|
+
|
|
273
|
+
if self.session.new:
|
|
274
|
+
msg.append(
|
|
275
|
+
"Warning: This is a new session and may not have all expected values."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if not self.session.get(self.flow_cache_key):
|
|
279
|
+
msg.append(f"<b>Expected '{self.flow_cache_key}' in session</b>")
|
|
280
|
+
msg.append(helpers.html_table(self.session))
|
|
281
|
+
|
|
282
|
+
self.redirect = "/" + self.redirect.lstrip("/")
|
|
283
|
+
|
|
284
|
+
if msg:
|
|
285
|
+
return False, msg
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
await self.async_acquire_token_by_auth_code_flow(auth_response)
|
|
289
|
+
except Exception as err:
|
|
290
|
+
msg.append(
|
|
291
|
+
"<b>Could not get token</b> - async_acquire_token_by_auth_code_flow"
|
|
292
|
+
)
|
|
293
|
+
msg.append(str(err))
|
|
294
|
+
return False, msg
|
|
295
|
+
|
|
296
|
+
if not msg:
|
|
297
|
+
try:
|
|
298
|
+
if get_info in ("user", "manager"):
|
|
299
|
+
await helpers.get_user_info(self)
|
|
300
|
+
if get_info == "manager":
|
|
301
|
+
await helpers.get_manager_info(self)
|
|
302
|
+
except Exception as err:
|
|
303
|
+
msg.append("Could not get org info from MS graph")
|
|
304
|
+
msg.append(str(err))
|
|
305
|
+
self.mail = ""
|
|
306
|
+
self.name = ""
|
|
307
|
+
|
|
308
|
+
if self.session.get("mail"):
|
|
309
|
+
for lcb in ENV.login_callback:
|
|
310
|
+
await lcb(self)
|
|
311
|
+
|
|
312
|
+
return True, msg
|
aiohttp_msal/redis_tools.py
CHANGED
|
@@ -6,14 +6,14 @@ import logging
|
|
|
6
6
|
import time
|
|
7
7
|
from collections.abc import AsyncGenerator
|
|
8
8
|
from contextlib import AsyncExitStack, asynccontextmanager
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
10
|
|
|
11
11
|
from redis.asyncio import Redis, from_url
|
|
12
12
|
|
|
13
13
|
from aiohttp_msal.msal_async import AsyncMSAL
|
|
14
14
|
from aiohttp_msal.settings import ENV as MENV
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
_LOG = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
18
|
SES_KEYS = ("mail", "name", "m_mail", "m_name")
|
|
19
19
|
|
|
@@ -22,10 +22,10 @@ SES_KEYS = ("mail", "name", "m_mail", "m_name")
|
|
|
22
22
|
async def get_redis() -> AsyncGenerator[Redis, None]:
|
|
23
23
|
"""Get a Redis connection."""
|
|
24
24
|
if MENV.database:
|
|
25
|
-
|
|
25
|
+
_LOG.debug("Using redis from environment")
|
|
26
26
|
yield MENV.database
|
|
27
27
|
return
|
|
28
|
-
|
|
28
|
+
_LOG.info("Connect to Redis %s", MENV.REDIS)
|
|
29
29
|
redis = from_url(MENV.REDIS) # decode_responses=True not allowed aiohttp_session
|
|
30
30
|
MENV.database = redis
|
|
31
31
|
try:
|
|
@@ -37,6 +37,7 @@ async def get_redis() -> AsyncGenerator[Redis, None]:
|
|
|
37
37
|
|
|
38
38
|
async def session_iter(
|
|
39
39
|
redis: Redis,
|
|
40
|
+
/,
|
|
40
41
|
*,
|
|
41
42
|
match: dict[str, str] | None = None,
|
|
42
43
|
key_match: str | None = None,
|
|
@@ -74,7 +75,7 @@ async def session_iter(
|
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
async def session_clean(
|
|
77
|
-
redis: Redis, *, max_age: int = 90, expected_keys: dict[str, Any] | None = None
|
|
78
|
+
redis: Redis, /, *, max_age: int = 90, expected_keys: dict[str, Any] | None = None
|
|
78
79
|
) -> None:
|
|
79
80
|
"""Clear session entries older than max_age days."""
|
|
80
81
|
rem, keep = 0, 0
|
|
@@ -89,12 +90,12 @@ async def session_clean(
|
|
|
89
90
|
keep += 1
|
|
90
91
|
finally:
|
|
91
92
|
if rem:
|
|
92
|
-
|
|
93
|
+
_LOG.info("Sessions removed: %s (%s total)", rem, keep)
|
|
93
94
|
else:
|
|
94
|
-
|
|
95
|
+
_LOG.debug("No sessions removed (%s total)", keep)
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
async def invalid_sessions(redis: Redis) -> None:
|
|
98
|
+
async def invalid_sessions(redis: Redis, /) -> None:
|
|
98
99
|
"""Find & clean invalid sessions."""
|
|
99
100
|
async for key in redis.scan_iter(count=100, match=f"{MENV.COOKIE_NAME}*"):
|
|
100
101
|
if not isinstance(key, str):
|
|
@@ -107,12 +108,17 @@ async def invalid_sessions(redis: Redis) -> None:
|
|
|
107
108
|
assert isinstance(val["created"], int)
|
|
108
109
|
assert isinstance(val["session"], dict)
|
|
109
110
|
except Exception as err:
|
|
110
|
-
|
|
111
|
+
_LOG.warning("Removing session %s: %s", key, err)
|
|
111
112
|
await redis.delete(key)
|
|
112
113
|
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
T = TypeVar("T", bound=AsyncMSAL)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def async_msal_factory(
|
|
119
|
+
cls: type[T], key: str, created: int, session: dict[str, Any], /
|
|
120
|
+
) -> T:
|
|
121
|
+
"""Create a AsyncMSAL session with a save_callback.
|
|
116
122
|
|
|
117
123
|
When get_token refreshes the token retrieved from Redis, the save_cache callback
|
|
118
124
|
will be responsible to update the cache in Redis.
|
|
@@ -130,12 +136,17 @@ def _session_factory(key: str, created: int, session: dict) -> AsyncMSAL:
|
|
|
130
136
|
except RuntimeError:
|
|
131
137
|
asyncio.run(async_save_cache(*args))
|
|
132
138
|
|
|
133
|
-
return
|
|
139
|
+
return cls(session, save_callback=save_cache)
|
|
134
140
|
|
|
135
141
|
|
|
136
142
|
async def get_session(
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
cls: type[T],
|
|
144
|
+
email: str,
|
|
145
|
+
/,
|
|
146
|
+
*,
|
|
147
|
+
redis: Redis | None = None,
|
|
148
|
+
scope: str = "",
|
|
149
|
+
) -> T:
|
|
139
150
|
"""Get a session from Redis."""
|
|
140
151
|
cnt = 0
|
|
141
152
|
async with AsyncExitStack() as stack:
|
|
@@ -143,22 +154,22 @@ async def get_session(
|
|
|
143
154
|
redis = await stack.enter_async_context(get_redis())
|
|
144
155
|
async for key, created, session in session_iter(redis, match={"mail": email}):
|
|
145
156
|
cnt += 1
|
|
146
|
-
if scope and scope not in str(session.get(
|
|
157
|
+
if scope and scope not in str(session.get(cls.token_cache_key)).lower():
|
|
147
158
|
continue
|
|
148
|
-
return
|
|
159
|
+
return async_msal_factory(cls, key, created, session)
|
|
149
160
|
msg = f"Session for {email}"
|
|
150
161
|
if not scope:
|
|
151
162
|
raise ValueError(f"{msg} not found")
|
|
152
163
|
raise ValueError(f"{msg} with scope {scope} not found ({cnt} checked)")
|
|
153
164
|
|
|
154
165
|
|
|
155
|
-
async def redis_get_json(key: str) -> list | dict | None:
|
|
166
|
+
async def redis_get_json(key: str) -> list[str] | dict[str, Any] | None:
|
|
156
167
|
"""Get a key from redis."""
|
|
157
168
|
res = await MENV.database.get(key)
|
|
158
169
|
if isinstance(res, str | bytes | bytearray):
|
|
159
170
|
return json.loads(res)
|
|
160
171
|
if res is not None:
|
|
161
|
-
|
|
172
|
+
_LOG.warning("Unexpected type for %s: %s", key, type(res))
|
|
162
173
|
return None
|
|
163
174
|
|
|
164
175
|
|
|
@@ -170,7 +181,7 @@ async def redis_get(key: str) -> str | None:
|
|
|
170
181
|
if isinstance(res, bytes | bytearray):
|
|
171
182
|
return res.decode()
|
|
172
183
|
if res is not None:
|
|
173
|
-
|
|
184
|
+
_LOG.warning("Unexpected type for %s: %s", key, type(res))
|
|
174
185
|
return None
|
|
175
186
|
|
|
176
187
|
|
|
@@ -182,16 +193,16 @@ async def redis_set_set(key: str, new_set: set[str]) -> None:
|
|
|
182
193
|
)
|
|
183
194
|
dif = list(cur_set - new_set)
|
|
184
195
|
if dif:
|
|
185
|
-
|
|
196
|
+
_LOG.warning("%s: removing %s", key, dif)
|
|
186
197
|
await MENV.database.srem(key, *dif)
|
|
187
198
|
|
|
188
199
|
dif = list(new_set - cur_set)
|
|
189
200
|
if dif:
|
|
190
|
-
|
|
201
|
+
_LOG.info("%s: adding %s", key, dif)
|
|
191
202
|
await MENV.database.sadd(key, *dif)
|
|
192
203
|
|
|
193
204
|
|
|
194
|
-
async def
|
|
205
|
+
async def redis_scan_keys(match_str: str) -> list[str]:
|
|
195
206
|
"""Return a list of matching keys."""
|
|
196
207
|
return [
|
|
197
208
|
s if isinstance(s, str) else s.decode()
|
aiohttp_msal/routes.py
CHANGED
|
@@ -9,12 +9,7 @@ from aiohttp import web
|
|
|
9
9
|
from aiohttp_session import get_session, new_session
|
|
10
10
|
|
|
11
11
|
from aiohttp_msal import ENV, auth_ok, msal_session
|
|
12
|
-
from aiohttp_msal.helpers import
|
|
13
|
-
check_auth_response,
|
|
14
|
-
get_manager_info,
|
|
15
|
-
get_user_info,
|
|
16
|
-
html_wrap,
|
|
17
|
-
)
|
|
12
|
+
from aiohttp_msal.helpers import get_manager_info, get_url, get_user_info, html_wrap
|
|
18
13
|
from aiohttp_msal.msal_async import AsyncMSAL
|
|
19
14
|
|
|
20
15
|
ROUTES = web.RouteTableDef()
|
|
@@ -24,17 +19,6 @@ URI_USER_AUTHORIZED = "/user/authorized"
|
|
|
24
19
|
SESSION_REDIRECT = "redirect"
|
|
25
20
|
|
|
26
21
|
|
|
27
|
-
def get_route(request: web.Request, url: str) -> str:
|
|
28
|
-
"""Retrieve server route from request.
|
|
29
|
-
|
|
30
|
-
localhost and production on http:// with nginx proxy that adds TLS/SSL.
|
|
31
|
-
"""
|
|
32
|
-
url = str(request.url.origin() / url)
|
|
33
|
-
if "localhost" not in url:
|
|
34
|
-
url = url.replace("http:", "https:", 1)
|
|
35
|
-
return url
|
|
36
|
-
|
|
37
|
-
|
|
38
22
|
@ROUTES.get(URI_USER_LOGIN)
|
|
39
23
|
@ROUTES.get(f"{URI_USER_LOGIN}/{{to:.+$}}")
|
|
40
24
|
async def user_login(request: web.Request) -> web.Response:
|
|
@@ -47,7 +31,7 @@ async def user_login(request: web.Request) -> web.Response:
|
|
|
47
31
|
_to = "/"
|
|
48
32
|
session[SESSION_REDIRECT] = urljoin(_to, request.match_info.get("to", ""))
|
|
49
33
|
|
|
50
|
-
msredirect =
|
|
34
|
+
msredirect = get_url(request, URI_USER_AUTHORIZED.lstrip("/"))
|
|
51
35
|
redir = AsyncMSAL(session).initiate_auth_code_flow(redirect_uri=msredirect)
|
|
52
36
|
raise web.HTTPFound(redir)
|
|
53
37
|
|
|
@@ -55,9 +39,10 @@ async def user_login(request: web.Request) -> web.Response:
|
|
|
55
39
|
@ROUTES.post(URI_USER_AUTHORIZED)
|
|
56
40
|
async def user_authorized(request: web.Request) -> web.Response:
|
|
57
41
|
"""Complete the auth code flow."""
|
|
58
|
-
aiomsal
|
|
42
|
+
aiomsal = await AsyncMSAL.from_request(request)
|
|
43
|
+
ok, msg = await aiomsal.async_acquire_token_by_auth_code_flow_plus(request)
|
|
59
44
|
|
|
60
|
-
if
|
|
45
|
+
if ok and not msg:
|
|
61
46
|
try:
|
|
62
47
|
raise web.HTTPFound(aiomsal.redirect)
|
|
63
48
|
finally:
|
|
@@ -149,7 +134,7 @@ async def user_logout(request: web.Request, ses: AsyncMSAL) -> web.Response:
|
|
|
149
134
|
if ref:
|
|
150
135
|
_to = urljoin(ref, _to)
|
|
151
136
|
else:
|
|
152
|
-
_to =
|
|
137
|
+
_to = get_url(request, _to)
|
|
153
138
|
|
|
154
139
|
return web.HTTPFound(
|
|
155
140
|
f"https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri={_to}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: aiohttp-msal
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
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
|
|
@@ -137,4 +137,6 @@ uv sync --all-extras
|
|
|
137
137
|
uv tool install ruff
|
|
138
138
|
uv tool install codespell
|
|
139
139
|
uv tool install pyproject-fmt
|
|
140
|
+
uv tool install prek
|
|
141
|
+
prek install # add pre-commit hooks
|
|
140
142
|
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aiohttp_msal/__init__.py,sha256=bCBQhbjM5IHKnHKzSDkHQ8Lk_YgW3flO11dgaDn8-Yg,4369
|
|
2
|
+
aiohttp_msal/helpers.py,sha256=L0DFIZD9OfCepckfk64ZtxrVX0-y0aAqRFowBYipepE,2474
|
|
3
|
+
aiohttp_msal/msal_async.py,sha256=VG5L6PGBHzRBNg_XfIqvfU1V-Snznv_Srd_kXzBgTts,11702
|
|
4
|
+
aiohttp_msal/redis_tools.py,sha256=vAA3pvMT8jXd1sDL7wUZ8Z1FWO0xfHTWzi3_pnA_f8E,6672
|
|
5
|
+
aiohttp_msal/routes.py,sha256=Cc6FHqs8dyHSTCC6AaT-yPttSDyBlsngoiz7j9QCKRU,5143
|
|
6
|
+
aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
|
|
7
|
+
aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
|
|
8
|
+
aiohttp_msal/utils.py,sha256=ll303J58nwCEB9QCAm13urUOa6cPqsAE7z_iP9OlRJw,2390
|
|
9
|
+
aiohttp_msal-1.0.6.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
|
|
10
|
+
aiohttp_msal-1.0.6.dist-info/METADATA,sha256=YvbKSl1STeQhHmyb9n9vKpsoHfalctOKStQIA_NZHNk,4572
|
|
11
|
+
aiohttp_msal-1.0.6.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
aiohttp_msal/__init__.py,sha256=nrjPAIy0kNbBvHZsy9qUSKs5r_-Kiea-KJM17bsBIkw,4375
|
|
2
|
-
aiohttp_msal/helpers.py,sha256=8sW-7FW4t1Ztazql11ng67SqdKBlbOWuOMjOBRfBc_c,4495
|
|
3
|
-
aiohttp_msal/msal_async.py,sha256=RUV_XlhNNCOZslT1sEkEPyaYcHDOmsIyiGmRpZuA3I0,8097
|
|
4
|
-
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
5
|
-
aiohttp_msal/routes.py,sha256=3rAejWcHfL5czVA91TY3wZGT5EkRxmfyvc8vr6rhyxU,5438
|
|
6
|
-
aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
|
|
7
|
-
aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
|
|
8
|
-
aiohttp_msal/utils.py,sha256=ll303J58nwCEB9QCAm13urUOa6cPqsAE7z_iP9OlRJw,2390
|
|
9
|
-
aiohttp_msal-1.0.4.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
10
|
-
aiohttp_msal-1.0.4.dist-info/METADATA,sha256=OBl_U9OTy602OfSL9-d5myNJPUYuaGjtBF7voW5lu2Y,4514
|
|
11
|
-
aiohttp_msal-1.0.4.dist-info/RECORD,,
|