Skip to content

Commit 8396401

Browse files
petrsvihlikAigerim BeishenbekovaAigerim BeishenbekovaAikoBB
authored
Implement token autorefresh (Azure#21834)
* removed options bag, enabled and fixed tests * fix build problems * initial implementation of configurable autorefresh * python 2.7 compat changes * py27 compat changes * fixed linting problems + comments * py27 fixed flaky test * linting issues * CommunicationTokenCredential async implemenation & tests are added * split async code not to break py27 * lock issue for python 3.10 is fixed * asyncio.sleep in async tests are removed * test refactored * updates in _shared duplicated in chat * updates in _shared duplicated in sms * updates in _shared duplicated in networktraversal * updates in _shared duplicated in phonenumbers * lint issue fix in utils * python 2 compatibility fix for generate_token_with_custom_expiry & fixed sync tests termination * removed unneccasary user credential tests from sms,chat, networktraversal,phonenumber * reduced the default refresh interval (api review) * time renamed to interval (api review) * removed config for refresh time interval * sync changes across modalities * linting issues * linting issues * implemented fractional backoff + fixed tests * unify test with the sync version * fractional backoff tests + linting * added changelog records + bumped versions * Removed ayncio.Lock workaround for a bug in Python 3.10 * fixed linting issues * phonenumbers changelog updated * fixed PR comments * removed user_token_refresh_options from communication SDKs * fix cspell issues * type hinting fix * reverted back type hint fix * PR comment fix * reflected changes to the identity package & updated tests * added samples for CommunicationTokenCredential * renaming proactive refresh flag * latest PR comments fix * samples are refactored * reflecting shared folder changes to other modalitites * fixed a typo * fix for pypy threading issue * fixed test files * fixed latest PR comments Co-authored-by: Aigerim Beishenbekova <aigerimb@WIN-8O5AC9CE1AP.reddog.microsoft.com> Co-authored-by: Aigerim Beishenbekova <aigerimb@Aigerims-MacBook-Pro-2.local> Co-authored-by: Aigerim <aykobb@gmail.com> Co-authored-by: Aigerim Beishenbekova <aigerimb@microsoft.com>
1 parent 8b18061 commit 8396401

File tree

57 files changed

+2550
-739
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2550
-739
lines changed

.vscode/cspell.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223
"msrest",
224224
"msrestazure",
225225
"MSSQL",
226+
"mutex",
226227
"myacr",
227228
"nazsdk",
228229
"noarch",
@@ -383,13 +384,14 @@
383384
]
384385
},
385386
{
386-
"filename": "sdk/communication/azure-communication-identity/tests/*.py",
387+
"filename": "sdk/communication/azure-communication-identity/tests/**",
387388
"words": [
388389
"XVCJ",
389390
"Njgw",
390391
"FNNHHJT",
391392
"Zwiz",
392-
"nypg"
393+
"nypg",
394+
"PBOF"
393395
]
394396
},
395397
{

sdk/communication/azure-communication-chat/CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 1.2.0 (Unreleased)
44

5+
- Added support for proactive refreshing of tokens
6+
- `CommunicationTokenCredential` exposes a new boolean keyword argument `proactive_refresh` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state.
7+
- Added disposal function `close` for `CommunicationTokenCredential`.
8+
59
### Features Added
610

711
### Breaking Changes
@@ -12,16 +16,20 @@
1216
Python 2.7 is no longer supported. Please use Python version 3.6 or later.
1317

1418
## 1.1.0 (2021-09-15)
19+
1520
- Updated `azure-communication-chat` version.
1621

1722
## 1.1.0b1 (2021-08-16)
1823

1924
### Added
25+
2026
- Added support to add `metadata` for `message`
2127
- Added support to add `sender_display_name` for `ChatThreadClient.send_typing_notification`
2228

2329
## 1.0.0 (2021-03-29)
30+
2431
### Breaking Changes
32+
2533
- Renamed `ChatThread` to `ChatThreadProperties`.
2634
- Renamed `get_chat_thread` to `get_properties`.
2735
- Moved `get_properties` under `ChatThreadClient`.
@@ -37,22 +45,29 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later.
3745
- Refactored implementation of `CommunicationUserIdentifier`, `PhoneNumberIdentifier`, `MicrosoftTeamsUserIdentifier`, `UnknownIdentifier` to use a `dict` property bag.
3846

3947
## 1.0.0b5 (2021-03-09)
48+
4049
### Breaking Changes
50+
4151
- Added support for communication identifiers instead of raw strings.
4252
- Changed return type of `create_chat_thread`: `ChatThreadClient -> CreateChatThreadResult`
4353
- Changed return types `add_participants`: `None -> list[(ChatThreadParticipant, CommunicationError)]`
4454
- Added check for failure in `add_participant`
4555
- Dropped support for Python 3.5
56+
4657
### Added
58+
4759
- Removed nullable references from method signatures.
4860

4961
## 1.0.0b4 (2021-02-09)
62+
5063
### Breaking Changes
64+
5165
- Uses `CommunicationUserIdentifier` and `CommunicationIdentifier` in place of `CommunicationUser`, and `CommunicationTokenCredential` instead of `CommunicationUserCredential`.
5266
- Removed priority field (ChatMessage.Priority).
5367
- Renamed PhoneNumber to PhoneNumberIdentifier.
5468

5569
### Added
70+
5671
- Support for CreateChatThreadResult and AddChatParticipantsResult to handle partial errors in batch calls.
5772
- Added idempotency identifier parameter for chat creation calls.
5873
- Added support for readreceipts and getparticipants pagination.
@@ -61,10 +76,13 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later.
6176
- Added `MicrosoftTeamsUserIdentifier`.
6277

6378
## 1.0.0b3 (2020-11-16)
79+
6480
- Updated `azure-communication-chat` version.
6581

6682
## 1.0.0b2 (2020-10-06)
83+
6784
- Updated `azure-communication-chat` version.
6885

6986
## 1.0.0b1 (2020-09-22)
70-
- Add ChatClient and ChatThreadClient.
87+
88+
- Add ChatClient and ChatThreadClient.

sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py

Lines changed: 96 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,143 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# --------------------------------------------------------------------------
6-
from threading import Lock, Condition
7-
from datetime import timedelta
8-
from typing import ( # pylint: disable=unused-import
9-
cast,
10-
Tuple,
11-
)
126

7+
from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event
8+
from datetime import timedelta
9+
from typing import Any
10+
import six
1311
from .utils import get_current_utc_as_int
14-
from .user_token_refresh_options import CommunicationTokenRefreshOptions
12+
from .utils import create_access_token
1513

1614

1715
class CommunicationTokenCredential(object):
1816
"""Credential type used for authenticating to an Azure Communication service.
19-
:param str token: The token used to authenticate to an Azure Communication service
20-
:keyword token_refresher: The token refresher to provide capacity to fetch fresh token
21-
:raises: TypeError
17+
:param str token: The token used to authenticate to an Azure Communication service.
18+
:keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token.
19+
The returned token must be valid (expiration date must be in the future).
20+
:paramtype token_refresher: Callable[[], AccessToken]
21+
:keyword bool proactive_refresh: Whether to refresh the token proactively or not.
22+
If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use
23+
a background thread to attempt to refresh the token within 10 minutes before the cached token expires,
24+
the proactive refresh will request a new token by calling the 'token_refresher' callback.
25+
When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager
26+
or the 'close' method must be called once the object usage has been finished.
27+
:raises: TypeError if paramater 'token' is not a string
28+
:raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable.
2229
"""
2330

2431
_ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2
25-
26-
def __init__(self,
27-
token, # type: str
28-
**kwargs
29-
):
30-
token_refresher = kwargs.pop('token_refresher', None)
31-
communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token,
32-
token_refresher=token_refresher)
33-
self._token = communication_token_refresh_options.get_token()
34-
self._token_refresher = communication_token_refresh_options.get_token_refresher()
32+
_DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10
33+
34+
def __init__(self, token: str, **kwargs: Any):
35+
if not isinstance(token, six.string_types):
36+
raise TypeError("Token must be a string.")
37+
self._token = create_access_token(token)
38+
self._token_refresher = kwargs.pop('token_refresher', None)
39+
self._proactive_refresh = kwargs.pop('proactive_refresh', False)
40+
if(self._proactive_refresh and self._token_refresher is None):
41+
raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.")
42+
self._timer = None
3543
self._lock = Condition(Lock())
3644
self._some_thread_refreshing = False
45+
self._is_closed = Event()
3746

3847
def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument
3948
# type (*str, **Any) -> AccessToken
4049
"""The value of the configured token.
4150
:rtype: ~azure.core.credentials.AccessToken
4251
"""
52+
if self._proactive_refresh and self._is_closed.is_set():
53+
raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.")
4354

44-
if not self._token_refresher or not self._token_expiring():
55+
if not self._token_refresher or not self._is_token_expiring_soon(self._token):
4556
return self._token
57+
self._update_token_and_reschedule()
58+
return self._token
4659

60+
def _update_token_and_reschedule(self):
4761
should_this_thread_refresh = False
48-
4962
with self._lock:
50-
while self._token_expiring():
63+
while self._is_token_expiring_soon(self._token):
5164
if self._some_thread_refreshing:
52-
if self._is_currenttoken_valid():
65+
if self._is_token_valid(self._token):
5366
return self._token
54-
55-
self._wait_till_inprogress_thread_finish_refreshing()
67+
self._wait_till_lock_owner_finishes_refreshing()
5668
else:
5769
should_this_thread_refresh = True
5870
self._some_thread_refreshing = True
5971
break
6072

6173
if should_this_thread_refresh:
6274
try:
63-
newtoken = self._token_refresher() # pylint:disable=not-callable
64-
75+
new_token = self._token_refresher()
76+
if not self._is_token_valid(new_token):
77+
raise ValueError(
78+
"The token returned from the token_refresher is expired.")
6579
with self._lock:
66-
self._token = newtoken
80+
self._token = new_token
6781
self._some_thread_refreshing = False
6882
self._lock.notify_all()
6983
except:
7084
with self._lock:
7185
self._some_thread_refreshing = False
7286
self._lock.notify_all()
73-
7487
raise
88+
if self._proactive_refresh:
89+
self._schedule_refresh()
7590
return self._token
7691

77-
def _wait_till_inprogress_thread_finish_refreshing(self):
92+
def _schedule_refresh(self):
93+
if self._is_closed.is_set():
94+
return
95+
if self._timer is not None:
96+
self._timer.cancel()
97+
98+
token_ttl = self._token.expires_on - get_current_utc_as_int()
99+
100+
if self._is_token_expiring_soon(self._token):
101+
# Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime.
102+
timespan = token_ttl // 2
103+
else:
104+
# Schedule the next refresh for when it gets in to the soon-to-expire window.
105+
timespan = token_ttl - timedelta(
106+
minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds()
107+
if timespan <= TIMEOUT_MAX:
108+
self._timer = Timer(timespan, self._update_token_and_reschedule)
109+
self._timer.daemon = True
110+
self._timer.start()
111+
112+
def _wait_till_lock_owner_finishes_refreshing(self):
78113
self._lock.release()
79114
self._lock.acquire()
80115

81-
def _token_expiring(self):
82-
return self._token.expires_on - get_current_utc_as_int() <\
83-
timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds()
84-
85-
def _is_currenttoken_valid(self):
86-
return get_current_utc_as_int() < self._token.expires_on
116+
def _is_token_expiring_soon(self, token):
117+
if self._proactive_refresh:
118+
interval = timedelta(
119+
minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)
120+
else:
121+
interval = timedelta(
122+
minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES)
123+
return ((token.expires_on - get_current_utc_as_int())
124+
< interval.total_seconds())
125+
126+
@classmethod
127+
def _is_token_valid(cls, token):
128+
return get_current_utc_as_int() < token.expires_on
129+
130+
def __enter__(self):
131+
if self._proactive_refresh:
132+
if self._is_closed.is_set():
133+
raise RuntimeError(
134+
"An instance of CommunicationTokenCredential cannot be reused once it has been closed.")
135+
self._schedule_refresh()
136+
return self
137+
138+
def __exit__(self, *args):
139+
self.close()
140+
141+
def close(self) -> None:
142+
if self._timer is not None:
143+
self._timer.cancel()
144+
self._timer = None
145+
self._is_closed.set()

0 commit comments

Comments
 (0)