Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement token autorefresh #21834

Merged
merged 52 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e195168
removed options bag, enabled and fixed tests
petrsvihlik Nov 19, 2021
0cc9878
fix build problems
petrsvihlik Nov 21, 2021
0f91e80
initial implementation of configurable autorefresh
petrsvihlik Nov 24, 2021
b375e2f
python 2.7 compat changes
petrsvihlik Nov 25, 2021
2e26dfb
py27 compat changes
petrsvihlik Nov 26, 2021
64ac5df
fixed linting problems + comments
petrsvihlik Nov 26, 2021
4c11b16
py27 fixed flaky test
petrsvihlik Nov 26, 2021
623bb1b
linting issues
petrsvihlik Nov 27, 2021
13c4469
CommunicationTokenCredential async implemenation & tests are added
Dec 1, 2021
599c47c
split async code not to break py27
petrsvihlik Dec 2, 2021
2c70227
lock issue for python 3.10 is fixed
Dec 2, 2021
c4ee92e
asyncio.sleep in async tests are removed
Dec 2, 2021
7589e7a
test refactored
petrsvihlik Dec 2, 2021
dda3a63
updates in _shared duplicated in chat
Dec 2, 2021
5f9addb
updates in _shared duplicated in sms
Dec 2, 2021
f2aa5be
updates in _shared duplicated in networktraversal
Dec 2, 2021
c05185b
updates in _shared duplicated in phonenumbers
Dec 2, 2021
c000311
lint issue fix in utils
Dec 3, 2021
1f5516a
python 2 compatibility fix for generate_token_with_custom_expiry & fi…
Dec 3, 2021
b410688
removed unneccasary user credential tests from sms,chat, networktrave…
Dec 15, 2021
a283f24
reduced the default refresh interval (api review)
petrsvihlik Dec 15, 2021
1a9eea3
time renamed to interval (api review)
petrsvihlik Dec 15, 2021
70df098
removed config for refresh time interval
petrsvihlik Jan 17, 2022
f1be525
sync changes across modalities
petrsvihlik Jan 17, 2022
5d3653b
linting issues
petrsvihlik Jan 17, 2022
6965577
linting issues
petrsvihlik Jan 17, 2022
4773029
implemented fractional backoff + fixed tests
petrsvihlik Jan 18, 2022
784d837
unify test with the sync version
petrsvihlik Jan 18, 2022
00b1cdd
fractional backoff tests + linting
petrsvihlik Jan 18, 2022
7bbf212
added changelog records + bumped versions
petrsvihlik Jan 19, 2022
3553b88
Removed ayncio.Lock workaround for a bug in Python 3.10
Feb 16, 2022
385917e
fixed linting issues
Feb 16, 2022
859fb8d
phonenumbers changelog updated
Feb 18, 2022
026aebf
fixed PR comments
Feb 18, 2022
bd9647f
removed user_token_refresh_options from communication SDKs
Feb 21, 2022
8dc6336
fix cspell issues
Feb 21, 2022
b9dec62
type hinting fix
Mar 3, 2022
b239725
Merge branch 'main' into petrsvihlik/feature/autorefresh
AikoBB Mar 3, 2022
fd69d2b
reverted back type hint fix
Mar 3, 2022
f9324b9
PR comment fix
Mar 4, 2022
030273a
reflected changes to the identity package & updated tests
Mar 7, 2022
730d895
added samples for CommunicationTokenCredential
Mar 7, 2022
2c44b1a
renaming proactive refresh flag
Mar 8, 2022
22bf69f
latest PR comments fix
Mar 10, 2022
4ec2b1d
samples are refactored
Mar 11, 2022
d412335
reflecting shared folder changes to other modalitites
Mar 11, 2022
834431a
Merge branch 'main' into petrsvihlik/feature/autorefresh
petrsvihlik May 10, 2022
8b374d1
fixed a typo
petrsvihlik May 11, 2022
226d12f
Merge branch 'petrsvihlik/feature/autorefresh' of https://github.com/…
petrsvihlik May 11, 2022
3fec4f9
fix for pypy threading issue
AikoBB May 25, 2022
dcf0e82
fixed test files
AikoBB May 25, 2022
be1aa9f
fixed latest PR comments
AikoBB Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@
"msrest",
"msrestazure",
"MSSQL",
"mutex",
"myacr",
"nazsdk",
"noarch",
Expand Down Expand Up @@ -372,13 +373,14 @@
]
},
{
"filename": "sdk/communication/azure-communication-identity/tests/*.py",
"filename": "sdk/communication/azure-communication-identity/tests/**",
"words": [
"XVCJ",
"Njgw",
"FNNHHJT",
"Zwiz",
"nypg"
"nypg",
"PBOF"
]
},
{
Expand Down
20 changes: 19 additions & 1 deletion sdk/communication/azure-communication-chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 1.2.0 (Unreleased)

- Added support for proactive refreshing of tokens
- `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.
- Added disposal function `close` for `CommunicationTokenCredential`.

### Features Added

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

## 1.1.0 (2021-09-15)

- Updated `azure-communication-chat` version.

## 1.1.0b1 (2021-08-16)

### Added

- Added support to add `metadata` for `message`
- Added support to add `sender_display_name` for `ChatThreadClient.send_typing_notification`

## 1.0.0 (2021-03-29)

### Breaking Changes

- Renamed `ChatThread` to `ChatThreadProperties`.
- Renamed `get_chat_thread` to `get_properties`.
- Moved `get_properties` under `ChatThreadClient`.
Expand All @@ -37,22 +45,29 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later.
- Refactored implementation of `CommunicationUserIdentifier`, `PhoneNumberIdentifier`, `MicrosoftTeamsUserIdentifier`, `UnknownIdentifier` to use a `dict` property bag.

## 1.0.0b5 (2021-03-09)

### Breaking Changes

- Added support for communication identifiers instead of raw strings.
- Changed return type of `create_chat_thread`: `ChatThreadClient -> CreateChatThreadResult`
- Changed return types `add_participants`: `None -> list[(ChatThreadParticipant, CommunicationError)]`
- Added check for failure in `add_participant`
- Dropped support for Python 3.5

### Added

- Removed nullable references from method signatures.

## 1.0.0b4 (2021-02-09)

### Breaking Changes

- Uses `CommunicationUserIdentifier` and `CommunicationIdentifier` in place of `CommunicationUser`, and `CommunicationTokenCredential` instead of `CommunicationUserCredential`.
- Removed priority field (ChatMessage.Priority).
- Renamed PhoneNumber to PhoneNumberIdentifier.

### Added

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

## 1.0.0b3 (2020-11-16)

- Updated `azure-communication-chat` version.

## 1.0.0b2 (2020-10-06)

- Updated `azure-communication-chat` version.

## 1.0.0b1 (2020-09-22)
- Add ChatClient and ChatThreadClient.

- Add ChatClient and ChatThreadClient.
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,143 @@
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from threading import Lock, Condition
from datetime import timedelta
from typing import ( # pylint: disable=unused-import
cast,
Tuple,
)

from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event
from datetime import timedelta
from typing import Any
import six
from .utils import get_current_utc_as_int
from .user_token_refresh_options import CommunicationTokenRefreshOptions
from .utils import create_access_token


class CommunicationTokenCredential(object):
"""Credential type used for authenticating to an Azure Communication service.
:param str token: The token used to authenticate to an Azure Communication service
:keyword token_refresher: The token refresher to provide capacity to fetch fresh token
:raises: TypeError
:param str token: The token used to authenticate to an Azure Communication service.
:keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token.
The returned token must be valid (expiration date must be in the future).
:paramtype token_refresher: Callable[[], AccessToken]
:keyword bool proactive_refresh: Whether to refresh the token proactively or not.
AikoBB marked this conversation as resolved.
Show resolved Hide resolved
If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use
a background thread to attempt to refresh the token within 10 minutes before the cached token expires,
the proactive refresh will request a new token by calling the 'token_refresher' callback.
AikoBB marked this conversation as resolved.
Show resolved Hide resolved
When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager
or the 'close' method must be called once the object usage has been finished.
:raises: TypeError if paramater 'token' is not a string
:raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable.
"""

_ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2

def __init__(self,
token, # type: str
**kwargs
):
token_refresher = kwargs.pop('token_refresher', None)
communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token,
token_refresher=token_refresher)
self._token = communication_token_refresh_options.get_token()
self._token_refresher = communication_token_refresh_options.get_token_refresher()
_DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10
petrsvihlik marked this conversation as resolved.
Show resolved Hide resolved
AikoBB marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, token: str, **kwargs: Any):
if not isinstance(token, six.string_types):
raise TypeError("Token must be a string.")
self._token = create_access_token(token)
self._token_refresher = kwargs.pop('token_refresher', None)
self._proactive_refresh = kwargs.pop('proactive_refresh', False)
if(self._proactive_refresh and self._token_refresher is None):
raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.")
self._timer = None
self._lock = Condition(Lock())
self._some_thread_refreshing = False
self._is_closed = Event()

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

if not self._token_refresher or not self._token_expiring():
if not self._token_refresher or not self._is_token_expiring_soon(self._token):
return self._token
self._update_token_and_reschedule()
return self._token

def _update_token_and_reschedule(self):
should_this_thread_refresh = False

with self._lock:
while self._token_expiring():
while self._is_token_expiring_soon(self._token):
if self._some_thread_refreshing:
if self._is_currenttoken_valid():
if self._is_token_valid(self._token):
return self._token

self._wait_till_inprogress_thread_finish_refreshing()
self._wait_till_lock_owner_finishes_refreshing()
else:
should_this_thread_refresh = True
self._some_thread_refreshing = True
break

if should_this_thread_refresh:
try:
newtoken = self._token_refresher() # pylint:disable=not-callable

new_token = self._token_refresher()
if not self._is_token_valid(new_token):
raise ValueError(
"The token returned from the token_refresher is expired.")
petrsvihlik marked this conversation as resolved.
Show resolved Hide resolved
with self._lock:
self._token = newtoken
self._token = new_token
self._some_thread_refreshing = False
self._lock.notify_all()
except:
with self._lock:
self._some_thread_refreshing = False
self._lock.notify_all()

raise
if self._proactive_refresh:
self._schedule_refresh()
return self._token

def _wait_till_inprogress_thread_finish_refreshing(self):
def _schedule_refresh(self):
if self._is_closed.is_set():
return
AikoBB marked this conversation as resolved.
Show resolved Hide resolved
if self._timer is not None:
self._timer.cancel()

token_ttl = self._token.expires_on - get_current_utc_as_int()

if self._is_token_expiring_soon(self._token):
# Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime.
timespan = token_ttl // 2
else:
# Schedule the next refresh for when it gets in to the soon-to-expire window.
timespan = token_ttl - timedelta(
minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds()
if timespan <= TIMEOUT_MAX:
self._timer = Timer(timespan, self._update_token_and_reschedule)
self._timer.daemon = True
self._timer.start()

def _wait_till_lock_owner_finishes_refreshing(self):
self._lock.release()
self._lock.acquire()

def _token_expiring(self):
return self._token.expires_on - get_current_utc_as_int() <\
timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds()

def _is_currenttoken_valid(self):
return get_current_utc_as_int() < self._token.expires_on
def _is_token_expiring_soon(self, token):
if self._proactive_refresh:
interval = timedelta(
minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)
else:
interval = timedelta(
minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES)
return ((token.expires_on - get_current_utc_as_int())
< interval.total_seconds())

@classmethod
def _is_token_valid(cls, token):
return get_current_utc_as_int() < token.expires_on

def __enter__(self):
AikoBB marked this conversation as resolved.
Show resolved Hide resolved
if self._proactive_refresh:
if self._is_closed.is_set():
raise RuntimeError(
"An instance of CommunicationTokenCredential cannot be reused once it has been closed.")
self._schedule_refresh()
return self

def __exit__(self, *args):
self.close()

def close(self) -> None:
if self._timer is not None:
self._timer.cancel()
self._timer = None
self._is_closed.set()
Loading