Skip to content

Commit b4e8f45

Browse files
berndverstBernd VerstCopilot
authored
Add configurable Azure Managed token audiences (#305)
* Add configurable Azure Managed token audiences Normalize resource IDs across options, connection strings, and client/worker helpers; select government defaults per instance and forward supported credential authorities. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Address token audience test review feedback Remove the unused authority-test parameter and project its argument source to the two required fields. Explicitly allow insecure credentials for the worker loopback transport test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Handle Unicode whitespace in token audiences Use Java 8-compatible trimming for Unicode whitespace while retaining existing ASCII behavior. Cover whitespace-only rejection and normalized token scopes across client, worker, and connection-string paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Bernd Verst <beverst@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent cfdfbf5 commit b4e8f45

12 files changed

Lines changed: 996 additions & 20 deletions

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
## Unreleased
2+
* Add normalized token audience configuration through Azure Managed client/worker options, convenience overloads, and connection-string `ResourceId`. Missing or empty audiences now default to `https://durabletask.azure.us` when `REGION_NAME` starts with `usgov` or `usdod` (case-insensitively), otherwise `https://durabletask.io`. Explicit audiences override this default; surrounding Unicode whitespace is trimmed, and whitespace-only or otherwise empty normalized values are rejected.
3+
* Add optional connection-string `AuthorityHost` forwarding for Azure Identity credentials that support it. Audience selection does not change the service endpoint or credential authority; managed identity and developer-tool cloud configuration remain separate.
24
* Add the `exporthistory` module for durable, checkpointed export of terminal orchestration history to Azure Blob Storage ([#293](https://github.com/microsoft/durabletask-java/pull/293))
35
* Add client APIs to list terminal instance IDs by completion time (`listInstanceIds`) and read orchestration history (`getOrchestrationHistory`) ([#292](https://github.com/microsoft/durabletask-java/pull/292))
46
* Add `createReplaySafeLogger` to suppress orchestration log output during replay ([#295](https://github.com/microsoft/durabletask-java/pull/295)).

‎README.md‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,100 @@ The following packages are produced from this repo.
105105
| Durable Task - Client | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft/durabletask-client?label=durabletask-client)](https://mvnrepository.com/artifact/com.microsoft/durabletask-client/1.0.0) |
106106
| Durable Task - Azure Functions | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft/durabletask-azure-functions?label=durabletask-azure-functions)](https://mvnrepository.com/artifact/com.microsoft/durabletask-azure-functions/1.0.1) |
107107

108+
## Azure Durable Task Scheduler authentication
109+
110+
The `com.microsoft:durabletask-azuremanaged` package configures clients and workers
111+
through `DurableTaskSchedulerClientOptions`, `DurableTaskSchedulerWorkerOptions`,
112+
or the corresponding `DurableTaskSchedulerClientExtensions` and
113+
`DurableTaskSchedulerWorkerExtensions` convenience methods.
114+
115+
### Token audience
116+
117+
Set `resourceId` using `setResourceId(...)` on either options class, the optional
118+
last argument of the `createClientBuilder`, `createWorkerBuilder`, and
119+
`useDurableTaskScheduler` overloads, or `ResourceId` in a connection string.
120+
This is a **token audience URI**, not an Azure Resource Manager resource path.
121+
Existing overloads remain supported.
122+
123+
| Configuration | Selected audience |
124+
| --- | --- |
125+
| Explicit nonempty `resourceId` / `ResourceId` | The normalized explicit value |
126+
| Missing, null, or empty value, with `REGION_NAME` starting with `usgov` or `usdod` (case-insensitive) | `https://durabletask.azure.us` |
127+
| All other cases | `https://durabletask.io` |
128+
129+
**Default behavior change:** applications running in US Government or DoD regions
130+
now select the government audience when no explicit audience is provided.
131+
Set `ResourceId=https://durabletask.io` to retain the public audience in those
132+
regions. Prefixes, not substrings, are matched: `chinaeast2`, `notusgov`, and
133+
`notusdod` still use the public default. No audience is inferred from the endpoint.
134+
135+
Explicit values have surrounding whitespace (including Unicode whitespace such as
136+
em spaces and non-breaking spaces) and trailing `/` characters removed,
137+
then one existing `/.default` suffix removed case-insensitively, followed by any
138+
remaining trailing `/` characters. URI casing is otherwise preserved. For example,
139+
`https://durabletask.azure.us//.DEFAULT//` requests
140+
`https://durabletask.azure.us/.default`, and `api://CustomAudience/resource/.DEFAULT/`
141+
requests `api://CustomAudience/resource/.default`. Whitespace-only input, `///`,
142+
`/.default`, and `/.DEFAULT///` throw `IllegalArgumentException`; use an omitted
143+
or genuinely empty value for the default.
144+
145+
Defaults are resolved per options instance or parsed connection string, rather
146+
than at class initialization. Setting a null or empty audience explicitly resolves
147+
the default again at that point. The selected audience is retained when creating
148+
channels, refreshing tokens, and reconnecting. Connection-string conversion does
149+
not normalize the audience again.
150+
151+
### Government-cloud example and credential authority
152+
153+
The **audience**, **credential authority/cloud**, and **service endpoint** are
154+
independent settings. Neither `resourceId` nor `REGION_NAME` changes the endpoint
155+
or credential authority. For an already-created `TokenCredential`, configure
156+
authority on that credential; token requests do not override it.
157+
158+
```java
159+
import com.azure.core.credential.TokenCredential;
160+
import com.azure.identity.AzureAuthorityHosts;
161+
import com.azure.identity.DefaultAzureCredentialBuilder;
162+
import com.microsoft.durabletask.DurableTaskGrpcClientBuilder;
163+
import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder;
164+
import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions;
165+
import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions;
166+
167+
// Set these to your scheduler's actual endpoint and task hub.
168+
String endpoint = System.getenv("DTS_ENDPOINT");
169+
String taskHub = System.getenv("DTS_TASK_HUB");
170+
TokenCredential credential = new DefaultAzureCredentialBuilder()
171+
.authorityHost(AzureAuthorityHosts.AZURE_GOVERNMENT)
172+
.build();
173+
174+
DurableTaskGrpcClientBuilder clientBuilder =
175+
DurableTaskSchedulerClientExtensions.createClientBuilder(
176+
endpoint, taskHub, credential, "https://durabletask.azure.us");
177+
DurableTaskGrpcWorkerBuilder workerBuilder =
178+
DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(
179+
endpoint, taskHub, credential, "https://durabletask.azure.us");
180+
```
181+
182+
When the SDK constructs the credential from a connection string, use the optional
183+
`AuthorityHost` property:
184+
185+
```text
186+
Endpoint=<your-scheduler-endpoint>;TaskHub=<your-task-hub>;Authentication=DefaultAzure;ResourceId=https://durabletask.azure.us;AuthorityHost=https://login.microsoftonline.us/
187+
```
188+
189+
`AuthorityHost` is forwarded to Azure Identity for `DefaultAzure`, `Environment`,
190+
`WorkloadIdentity`, and `InteractiveBrowser` authentication. Omission or an empty
191+
value leaves Azure Identity's defaults intact, including `AZURE_AUTHORITY_HOST`
192+
where supported. It is not an authority override on client or worker options.
193+
194+
Managed identity uses the hosting environment's identity endpoint; an Entra
195+
authority override does not apply. Developer-tool credentials (`AzureCli`,
196+
`AzurePowerShell`, `VisualStudioCode`, and `IntelliJ`) use those tools' cloud
197+
configuration, not the connection string's `AuthorityHost`. Configure them
198+
separately, including when they are used by `DefaultAzureCredential` (for example,
199+
`az cloud set --name AzureUSGovernment` before signing in with Azure CLI).
200+
`Authentication=None` remains anonymous.
201+
108202
## Getting started with Azure Functions
109203

110204
For information about how to get started with Durable Functions for Java, see the [Azure Functions README.md](/azurefunctions/README.md) content.

‎azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,37 @@ public static void useDurableTaskScheduler(
4848
String endpoint,
4949
String taskHubName,
5050
@Nullable TokenCredential tokenCredential) {
51+
useDurableTaskScheduler(builder, endpoint, taskHubName, tokenCredential, null);
52+
}
53+
54+
/**
55+
* Configures a client builder with an explicit token audience.
56+
*
57+
* @param builder The builder to configure.
58+
* @param endpoint The service endpoint, independent of the audience and credential authority.
59+
* @param taskHubName The name of the task hub.
60+
* @param tokenCredential The credential, with its authority/cloud configured by the caller,
61+
* or null for anonymous access.
62+
* @param resourceId The token audience URI, or null/empty for the region-based default.
63+
* See {@link DurableTaskSchedulerClientOptions#setResourceId(String)}
64+
* for normalization and default selection.
65+
* @throws NullPointerException if builder, endpoint, or taskHubName is null.
66+
* @throws IllegalArgumentException if resourceId becomes empty after normalization.
67+
*/
68+
public static void useDurableTaskScheduler(
69+
DurableTaskGrpcClientBuilder builder,
70+
String endpoint,
71+
String taskHubName,
72+
@Nullable TokenCredential tokenCredential,
73+
@Nullable String resourceId) {
5174
Objects.requireNonNull(builder, "builder must not be null");
5275
Objects.requireNonNull(endpoint, "endpoint must not be null");
5376
Objects.requireNonNull(taskHubName, "taskHubName must not be null");
5477

5578
configureBuilder(builder, new DurableTaskSchedulerClientOptions()
5679
.setEndpointAddress(endpoint)
5780
.setTaskHubName(taskHubName)
81+
.setResourceId(resourceId)
5882
.setCredential(tokenCredential));
5983
}
6084

@@ -85,12 +109,35 @@ public static DurableTaskGrpcClientBuilder createClientBuilder(
85109
String endpoint,
86110
String taskHubName,
87111
@Nullable TokenCredential tokenCredential) {
112+
return createClientBuilder(endpoint, taskHubName, tokenCredential, null);
113+
}
114+
115+
/**
116+
* Creates a client builder with an explicit token audience.
117+
*
118+
* @param endpoint The service endpoint, independent of the audience and credential authority.
119+
* @param taskHubName The name of the task hub.
120+
* @param tokenCredential The credential, with its authority/cloud configured by the caller,
121+
* or null for anonymous access.
122+
* @param resourceId The token audience URI, or null/empty for the region-based default.
123+
* See {@link DurableTaskSchedulerClientOptions#setResourceId(String)}
124+
* for normalization and default selection.
125+
* @return A new configured DurableTaskGrpcClientBuilder instance.
126+
* @throws NullPointerException if endpoint or taskHubName is null.
127+
* @throws IllegalArgumentException if resourceId becomes empty after normalization.
128+
*/
129+
public static DurableTaskGrpcClientBuilder createClientBuilder(
130+
String endpoint,
131+
String taskHubName,
132+
@Nullable TokenCredential tokenCredential,
133+
@Nullable String resourceId) {
88134
Objects.requireNonNull(endpoint, "endpoint must not be null");
89135
Objects.requireNonNull(taskHubName, "taskHubName must not be null");
90136

91137
return createBuilderFromOptions(new DurableTaskSchedulerClientOptions()
92138
.setEndpointAddress(endpoint)
93139
.setTaskHubName(taskHubName)
140+
.setResourceId(resourceId)
94141
.setCredential(tokenCredential)
95142
.setAllowInsecureCredentials(tokenCredential == null));
96143
}

‎azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ public class DurableTaskSchedulerClientOptions {
1919
private String taskHubName = "";
2020

2121
private TokenCredential credential;
22-
private String resourceId = "https://durabletask.io";
22+
private String resourceId = ResourceId.getDefault();
2323
private boolean allowInsecureCredentials = false;
2424
private Duration tokenRefreshMargin = Duration.ofMinutes(5);
2525

2626
/**
2727
* Creates a new instance of DurableTaskSchedulerClientOptions.
28+
* Resolves the token audience from {@code REGION_NAME} for this instance.
29+
*
30+
* @see #setResourceId(String)
2831
*/
2932
public DurableTaskSchedulerClientOptions() {
3033
}
@@ -47,11 +50,12 @@ public static DurableTaskSchedulerClientOptions fromConnectionString(String conn
4750
* @return A new DurableTaskSchedulerClientOptions object.
4851
*/
4952
static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString) {
50-
// TODO: Parse different credential types from connection string
5153
DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions();
5254
options.setEndpointAddress(connectionString.getEndpoint());
5355
options.setTaskHubName(connectionString.getTaskHubName());
5456
options.setCredential(connectionString.getCredential());
57+
// The connection string has already resolved and normalized this audience.
58+
options.resourceId = connectionString.getResourceId();
5559
options.setAllowInsecureCredentials(options.getCredential() == null);
5660
return options;
5761
}
@@ -108,7 +112,8 @@ public TokenCredential getCredential() {
108112
/**
109113
* Sets the credential used for authentication.
110114
*
111-
* @param credential The credential.
115+
* @param credential The credential, or null for anonymous access. Configure the authority/cloud
116+
* on the credential itself; the resource ID does not change its authority.
112117
* @return This options object.
113118
*/
114119
public DurableTaskSchedulerClientOptions setCredential(TokenCredential credential) {
@@ -117,7 +122,7 @@ public DurableTaskSchedulerClientOptions setCredential(TokenCredential credentia
117122
}
118123

119124
/**
120-
* Gets the resource ID.
125+
* Gets the normalized token audience URI (not an Azure Resource Manager resource path).
121126
*
122127
* @return The resource ID.
123128
*/
@@ -126,13 +131,19 @@ public String getResourceId() {
126131
}
127132

128133
/**
129-
* Sets the resource ID.
130-
*
131-
* @param resourceId The resource ID.
134+
* Sets the token audience URI, independently of the endpoint and credential authority.
135+
* Surrounding whitespace (including Unicode whitespace), trailing slashes, and one case-insensitive {@code /.default}
136+
* suffix are removed. Token requests append {@code /.default} to the result.
137+
*
138+
* @param resourceId The audience URI. Null or empty selects {@code https://durabletask.azure.us}
139+
* when {@code REGION_NAME} starts with {@code usgov} or {@code usdod}
140+
* (case-insensitively), otherwise {@code https://durabletask.io}.
141+
* The selection is retained for subsequent channels and token refreshes.
132142
* @return This options object.
143+
* @throws IllegalArgumentException if a nonempty value becomes empty after normalization.
133144
*/
134145
public DurableTaskSchedulerClientOptions setResourceId(String resourceId) {
135-
this.resourceId = resourceId;
146+
this.resourceId = ResourceId.resolve(resourceId);
136147
return this;
137148
}
138149

0 commit comments

Comments
 (0)