Skip to content

Commit 171be66

Browse files
authored
Add a backoff mechanism for the CommunicationTokenCredential (Azure#25299)
* Added a backoff mechanism for the CommunicationTokenCredential
1 parent 0301834 commit 171be66

File tree

8 files changed

+408
-146
lines changed

8 files changed

+408
-146
lines changed

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
### Features Added
66

7+
- Added new constructor with required param `tokenRefresher` for `CommunicationTokenRefreshOptions`
8+
- Deprecated old constructor overloads in `CommunicationTokenRefreshOptions` and replaced by fluent setters
9+
- Added fluent setters for optional properties:
10+
- Added `setRefreshProactively(boolean refreshProactively)` setter that allows setting whether the token should be proactively renewed prior to its expiry or on demand.
11+
- Added `setInitialToken(String initialToken)` setter that allows setting the optional serialized JWT token
12+
- Added a synchronous token refresher getter `getTokenRefresherSync` for `CommunicationTokenRefreshOptions`
13+
- Optimization added: When the proactive refreshing is enabled and the token refresher fails to provide a token that's not about to expire soon, the subsequent refresh attempts will be scheduled for when the token reaches half of its remaining lifetime until a token with long enough validity (>10 minutes) is obtained.
14+
715
### Breaking Changes
816

917
### Bugs Fixed
@@ -45,55 +53,71 @@
4553
- Upgraded azure-core to 1.21.0.
4654

4755
## 1.0.4 (2021-09-09)
56+
4857
### Dependency updates
58+
4959
- Added `azure-communication-networktraversal` package
5060

5161
## 1.0.3 (2021-06-28)
62+
5263
Updated `azure-communication-common` version
5364

5465
## 1.0.2 (2021-06-09)
66+
5567
Updated `azure-communication-common` version
5668

5769
## 1.0.1 (2021-05-27)
70+
5871
- Dependency versions updated.
5972

6073
### Bug Fixes
74+
6175
- Fixed bug with AzureKeyCredential authentication
6276

6377
## 1.0.0 (2021-03-29)
6478
### Breaking Changes
79+
6580
- Updated `CommunicationCloudEnvironment(String environmentValue)` constructor to `CommunicationCloudEnvironment()`.
6681
- Updated `public CommunicationCloudEnvironment fromString(String environmentValue)` to `public static CommunicationCloudEnvironment fromString(String environmentValue)`.
6782
- Renamed `TokenRefresher.getTokenAsync()` to `TokenRefresher.getToken()`.
6883

6984
## 1.0.0-beta.6 (2021-03-09)
85+
7086
### Breaking Changes
87+
7188
- Renamed `CommunicationTokenRefreshOptions.getRefreshProactively()` to `CommunicationTokenRefreshOptions.isRefreshProactively()`
7289
- Constructor for `CommunicationCloudEnvironment` has been removed and now to set an environment value, the `fromString()` method must be called
73-
- `CommunicationCloudEnvironment`, `CommunicationTokenRefreshOptions `, `CommunicationUserIdentifier`, `MicrosoftTeamsUserIdentifier`,
90+
- `CommunicationCloudEnvironment`, `CommunicationTokenRefreshOptions`, `CommunicationUserIdentifier`, `MicrosoftTeamsUserIdentifier`,
7491
`PhoneNumberIdentifier`, `UnknownIdentifier`, are all final classes now.
7592

7693
## 1.0.0-beta.5 (2021-03-02)
94+
7795
- Updated `azure-communication-common` version
7896

7997
## 1.0.0-beta.4 (2021-02-09)
98+
8099
### Breaking Changes
100+
81101
- Renamed `CommunicationUserCredential` to `CommunicationTokenCredential`
82102
- Replaced constructor `CommunicationTokenCredential(TokenRefresher tokenRefresher, String initialToken, boolean refreshProactively)` and `CommunicationTokenCredential(TokenRefresher tokenRefresher)` with `CommunicationTokenCredential(CommunicationTokenRefreshOptions tokenRefreshOptions)`
83103
- Renamed `PhoneNumber` to `PhoneNumberIdentifier`
84-
- Renamed `CommunicationUser` to `CommunicationUserIdentifier `
104+
- Renamed `CommunicationUser` to `CommunicationUserIdentifier`
85105
- Renamed `CallingApplication` to `CallingApplicationIdentifier`
86106

87107
### Added
108+
88109
- Added `MicrosoftTeamsUserIdentifier`
89110

90111
## 1.0.0-beta.3 (2020-11-16)
112+
91113
Updated `azure-communication-common` version
92114

93115
## 1.0.0-beta.2 (2020-10-06)
116+
94117
Updated `azure-communication-common` version
95118

96119
## 1.0.0-beta.1 (2020-09-22)
120+
97121
This package contains common code for Azure Communication Service libraries. For more information, please see the [README][read_me].
98122

99123
This is a Public Preview version, so breaking changes are possible in subsequent releases as we improve the product. To provide feedback, please submit an issue in our [Azure SDK for Java GitHub repo](https://github.com/Azure/azure-sdk-for-java/issues).

sdk/communication/azure-communication-common/README.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ and then include the direct dependency in the dependencies section without the v
4242
```
4343

4444
#### Include direct dependency
45+
4546
If you want to take dependency on a particular version of the library that is not present in the BOM,
4647
add the direct dependency to your project as follows.
4748

@@ -57,29 +58,50 @@ add the direct dependency to your project as follows.
5758

5859
## Key concepts
5960

60-
To work with Azure Communication Services, a resource access key is used for authentication.
61+
To work with Azure Communication Services, a resource access key is used for authentication.
6162

6263
Azure Communication Service supports HMAC authentication with resource access key. To
63-
apply HMAC authentication, construct CommunicationClientCredential with the access key and instantiate
64-
a CommunicationIdentityClient to manage users and tokens.
64+
apply HMAC authentication, construct `CommunicationClientCredential` with the access key and instantiate
65+
a `CommunicationIdentityClient` to manage users and tokens.
6566

6667
### CommunicationTokenCredential
6768

68-
It is up to you the developer to first create valid user tokens with the Communication Identity SDK. Then you use these tokens with the `CommunicationTokenCredential`.
69+
The `CommunicationTokenCredential` object is used to authenticate a user with Communication Services, such as Chat or Calling. It optionally provides an auto-refresh mechanism to ensure a continuously stable authentication state during communications.
6970

70-
`CommunicationTokenCredential` authenticates a user with Communication Services, such as Chat or Calling. It optionally provides an auto-refresh mechanism to ensure a continuously stable authentication state during communications.
71+
Depending on your scenario, you may want to initialize the `CommunicationTokenCredential` with:
7172

72-
## Contributing
73+
- a static token (suitable for short-lived clients used to e.g. send one-off Chat messages) or
74+
- a callback function that ensures a continuous authentication state (ideal e.g. for long Calling sessions).
7375

74-
This project welcomes contributions and suggestions. Most contributions require you to agree to a [Contributor License Agreement (CLA)][cla] declaring that you have the right to, and actually do, grant us the rights to use your contribution.
76+
The tokens supplied to the `CommunicationTokenCredential` either through the constructor or via the token refresher callback can be obtained using the Azure Communication Identity library.
7577

76-
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
78+
## Examples
7779

78-
This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For more information see the [Code of Conduct FAQ][coc_faq] or contact [opencode@microsoft.com][coc_contact] with any additional questions or comments.
80+
### Create a credential with a static token
7981

80-
## Examples
82+
For short-lived clients, refreshing the token upon expiry is not necessary and `CommunicationTokenCredential` may be instantiated with a static token.
8183

82-
In progress.
84+
```java
85+
String token = System.getenv("COMMUNICATION_SERVICES_USER_TOKEN");
86+
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(token);
87+
```
88+
89+
### Create a credential with proactive refreshing with a callback
90+
91+
Alternatively, for long-lived clients, you can create a `CommunicationTokenCredential` with a callback to renew tokens if expired.
92+
Here we assume that we have a function `fetchTokenFromMyServerForUser` that makes a network request to retrieve a token string for a user.
93+
It's necessary that the `fetchTokenFromMyServerForUser` function returns a valid token (with an expiration date set in the future) at all times.
94+
95+
Optionally, you can enable proactive token refreshing where a fresh token will be acquired as soon as the
96+
previous token approaches expiry. Using this method, your requests are less likely to be blocked to acquire a fresh token:
97+
98+
```java
99+
String token = System.getenv("COMMUNICATION_SERVICES_USER_TOKEN");
100+
CommunicationTokenRefreshOptions tokenRefreshOptions = new CommunicationTokenRefreshOptions(fetchTokenFromMyServerForUser)
101+
.setRefreshProactively(true)
102+
.setInitialToken(token);
103+
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(tokenRefreshOptions);
104+
```
83105

84106
## Troubleshooting
85107

@@ -89,6 +111,14 @@ In progress.
89111

90112
Check out other client libraries for Azure communication service
91113

114+
## Contributing
115+
116+
This project welcomes contributions and suggestions. Most contributions require you to agree to a [Contributor License Agreement (CLA)][cla] declaring that you have the right to, and actually do, grant us the rights to use your contribution.
117+
118+
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
119+
120+
This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For more information see the [Code of Conduct FAQ][coc_faq] or contact [opencode@microsoft.com][coc_contact] with any additional questions or comments.
121+
92122
<!-- LINKS -->
93123
[cla]: https://cla.microsoft.com
94124
[coc]: https://opensource.microsoft.com/codeofconduct/

sdk/communication/azure-communication-common/src/main/java/com/azure/communication/common/CommunicationTokenCredential.java

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,27 @@
22
// Licensed under the MIT License.
33
package com.azure.communication.common;
44

5+
import com.azure.communication.common.implementation.TokenParser;
6+
import com.azure.core.credential.AccessToken;
57
import com.azure.core.util.FluxUtil;
68
import com.azure.core.util.logging.ClientLogger;
7-
89
import reactor.core.publisher.Mono;
910

10-
import com.azure.core.credential.AccessToken;
11-
1211
import java.io.IOException;
1312
import java.time.OffsetDateTime;
1413
import java.util.Date;
1514
import java.util.Objects;
1615
import java.util.Timer;
1716
import java.util.TimerTask;
17+
import java.util.concurrent.TimeUnit;
1818
import java.util.function.Supplier;
1919

20-
import com.azure.communication.common.implementation.TokenParser;
21-
2220
/**
2321
* Provide user credential for Communication service user
2422
*/
2523
public final class CommunicationTokenCredential implements AutoCloseable {
2624
private static final int DEFAULT_EXPIRING_OFFSET_MINUTES = 10;
25+
private static final int DEFAULT_REFRESH_AFTER_TTL_DIVIDER = 2;
2726

2827
private final ClientLogger logger = new ClientLogger(CommunicationTokenCredential.class);
2928

@@ -47,22 +46,45 @@ public CommunicationTokenCredential(String token) {
4746
* Create with tokenRefreshOptions, which includes a token supplier and optional serialized JWT token.
4847
* If refresh proactively is true, callback function tokenRefresher will be called
4948
* ahead of the token expiry by the number of minutes specified by
50-
* CallbackOffsetMinutes defaulted to ten minutes. To modify this default, call
51-
* setCallbackOffsetMinutes after construction
49+
* CallbackOffsetMinutes defaulted to ten minutes.
5250
*
5351
* @param tokenRefreshOptions implementation to supply fresh token when reqested
5452
*/
5553
public CommunicationTokenCredential(CommunicationTokenRefreshOptions tokenRefreshOptions) {
56-
Supplier<Mono<String>> tokenRefresher = tokenRefreshOptions.getTokenRefresher();
57-
Objects.requireNonNull(tokenRefresher, "'tokenRefresher' cannot be null.");
58-
refresher = tokenRefresher;
54+
Supplier<String> tokenRefresher = tokenRefreshOptions.getTokenRefresherSync();
55+
refresher = tokenRefresher != null
56+
? () -> Mono.fromSupplier(tokenRefresher)
57+
: tokenRefreshOptions.getTokenRefresher();
58+
Objects.requireNonNull(refresher, "'tokenRefresher' cannot be null.");
5959
if (tokenRefreshOptions.getInitialToken() != null) {
6060
setToken(tokenRefreshOptions.getInitialToken());
61-
if (tokenRefreshOptions.isRefreshProactively()) {
62-
OffsetDateTime nextFetchTime = accessToken.getExpiresAt().minusMinutes(DEFAULT_EXPIRING_OFFSET_MINUTES);
63-
fetchingTask = new FetchingTask(this, nextFetchTime);
64-
}
6561
}
62+
if (tokenRefreshOptions.isRefreshProactively()) {
63+
scheduleRefresher();
64+
}
65+
}
66+
67+
private void scheduleRefresher() {
68+
OffsetDateTime nextFetchTime;
69+
if (isTokenExpired(accessToken)) {
70+
nextFetchTime = OffsetDateTime.now();
71+
} else {
72+
OffsetDateTime now = OffsetDateTime.now();
73+
long tokenTtlMs = accessToken.getExpiresAt().toInstant().toEpochMilli() - now.toInstant().toEpochMilli();
74+
long nextFetchTimeMs = isTokenExpiringSoon()
75+
? tokenTtlMs / DEFAULT_REFRESH_AFTER_TTL_DIVIDER
76+
: tokenTtlMs - TimeUnit.MILLISECONDS.convert(DEFAULT_EXPIRING_OFFSET_MINUTES, TimeUnit.MINUTES);
77+
nextFetchTime = now.plusNanos(TimeUnit.NANOSECONDS.convert(nextFetchTimeMs, TimeUnit.MILLISECONDS));
78+
}
79+
fetchingTask = new FetchingTask(this, nextFetchTime);
80+
}
81+
82+
private boolean isTokenExpired(AccessToken accessToken) {
83+
return accessToken == null || accessToken.isExpired();
84+
}
85+
86+
private boolean isTokenExpiringSoon() {
87+
return accessToken == null || OffsetDateTime.now().compareTo(accessToken.getExpiresAt().minusMinutes(DEFAULT_EXPIRING_OFFSET_MINUTES)) > 0;
6688
}
6789

6890
/**
@@ -73,17 +95,21 @@ public CommunicationTokenCredential(CommunicationTokenRefreshOptions tokenRefres
7395
public Mono<AccessToken> getToken() {
7496
if (isClosed) {
7597
return FluxUtil.monoError(logger,
76-
new RuntimeException("getToken called on closed CommunicationTokenCredential object"));
98+
new RuntimeException("getToken called on closed CommunicationTokenCredential object"));
7799
}
78-
if ((accessToken == null || accessToken.isExpired()) && refresher != null) {
100+
if (isTokenExpired(accessToken) && refresher != null) {
79101
synchronized (this) {
80102
// no valid token to return and can refresh
81-
if ((accessToken == null || accessToken.isExpired()) && refresher != null) {
103+
if (isTokenExpired(accessToken) && refresher != null) {
82104
return fetchFreshToken()
83-
.map(token -> {
84-
accessToken = tokenParser.parseJWTToken(token);
85-
return accessToken;
86-
});
105+
.flatMap(token -> {
106+
accessToken = tokenParser.parseJWTToken(token);
107+
if (isTokenExpired(accessToken)) {
108+
return FluxUtil.monoError(logger,
109+
new IllegalArgumentException("The token returned from the tokenRefresher is expired."));
110+
}
111+
return Mono.just(accessToken);
112+
});
87113
}
88114
}
89115
}
@@ -107,18 +133,16 @@ boolean hasProactiveFetcher() {
107133

108134
private void setToken(String freshToken) {
109135
accessToken = tokenParser.parseJWTToken(freshToken);
110-
111-
if (fetchingTask != null) {
112-
OffsetDateTime nextFetchTime = accessToken.getExpiresAt().minusMinutes(DEFAULT_EXPIRING_OFFSET_MINUTES);
113-
fetchingTask.setNextFetchTime(nextFetchTime);
136+
if (hasProactiveFetcher()) {
137+
scheduleRefresher();
114138
}
115139
}
116140

117141
private Mono<String> fetchFreshToken() {
118142
Mono<String> tokenAsync = refresher.get();
119143
if (tokenAsync == null) {
120144
return FluxUtil.monoError(logger,
121-
new RuntimeException("get() function of the token refresher should not return null."));
145+
new RuntimeException("get() function of the token refresher should not return null."));
122146
}
123147
return tokenAsync;
124148
}
@@ -129,14 +153,9 @@ private static class FetchingTask {
129153
private OffsetDateTime nextFetchTime;
130154

131155
FetchingTask(CommunicationTokenCredential tokenHost,
132-
OffsetDateTime nextFetchAt) {
156+
OffsetDateTime nextFetchAt) {
133157
host = tokenHost;
134158
nextFetchTime = nextFetchAt;
135-
startTimer();
136-
}
137-
138-
private synchronized void setNextFetchTime(OffsetDateTime newFetchTime) {
139-
nextFetchTime = newFetchTime;
140159
stopTimer();
141160
startTimer();
142161
}
@@ -165,6 +184,10 @@ private void setToken(String freshTokenString) {
165184
host.setToken(freshTokenString);
166185
}
167186

187+
private boolean isTokenExpired(String freshTokenString) {
188+
return host.tokenParser.parseJWTToken(freshTokenString).isExpired();
189+
}
190+
168191
private class TokenExpiringTask extends TimerTask {
169192
private final ClientLogger logger = new ClientLogger(TokenExpiringTask.class);
170193
private final FetchingTask tokenCache;
@@ -177,7 +200,13 @@ private class TokenExpiringTask extends TimerTask {
177200
public void run() {
178201
try {
179202
Mono<String> tokenAsync = tokenCache.fetchFreshToken();
180-
tokenCache.setToken(tokenAsync.block());
203+
tokenAsync.subscribe(token -> {
204+
if (!tokenCache.isTokenExpired(token)) {
205+
tokenCache.setToken(token);
206+
} else {
207+
logger.logExceptionAsError(new IllegalArgumentException("The token returned from the tokenRefresher is expired."));
208+
}
209+
});
181210
} catch (Exception exception) {
182211
logger.logExceptionAsError(new RuntimeException(exception));
183212
}

0 commit comments

Comments
 (0)