Skip to content
Draft
2 changes: 0 additions & 2 deletions eng/common/pipelines/templates/steps/verify-agent-os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,3 @@ steps:
filePath: ${{ parameters.ScriptDirectory }}/Verify-AgentOS.ps1
arguments: >
-AgentImage "${{ parameters.AgentImage }}"

- template: /eng/common/pipelines/templates/steps/bypass-local-dns.yml
5 changes: 5 additions & 0 deletions sdk/storage/azure-storage-blob/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

### Bugs Fixed

- Fixed an issue where the service's proactive `x-ms-auth-info: session_expiring` hint was ignored when the
client's own session-refresh timer had not yet elapsed, allowing a container session to be used past the
point the service rotated its network-context binding and surfacing as a `401 InvalidAuthenticationInfo`
(`session_token_invalid` / network context mismatch). The hint now forces a proactive background refresh.

### Other Changes

## 12.33.3 (2026-03-30)
Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azure-storage-blob/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "java",
"TagPrefix": "java/storage/azure-storage-blob",
"Tag": "java/storage/azure-storage-blob_47f4243e59"
"Tag": "java/storage/azure-storage-blob_dbe8c45320"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<suppress files="com.azure.storage.blob.implementation.util.BlobSasImplUtil.java" checks="io.clientcore.linting.extensions.checkstyle.checks.EnforceFinalFieldsCheck" />
<suppress files="com.azure.storage.blob.specialized.BlobOutputStream.java" checks="io.clientcore.linting.extensions.checkstyle.checks.EnforceFinalFieldsCheck" />
<suppress files="com.azure.storage.blob.implementation.util.BlobUserAgentModificationPolicy.java" checks="io.clientcore.linting.extensions.checkstyle.checks.HttpPipelinePolicyCheck" />
<suppress files="com.azure.storage.blob.implementation.util.SessionTokenCredentialPolicy.java" checks="io.clientcore.linting.extensions.checkstyle.checks.HttpPipelinePolicyCheck" />
<suppress files="com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ServiceClientBuilderCheck" />
<suppress files="com.azure.storage.blob.BlobClient.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ServiceClientCheck" />
<suppress files="com.azure.storage.blob.specialized.BlobLeaseAsyncClient.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ServiceClientCheck" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import com.azure.storage.blob.models.BlobAudience;
import com.azure.storage.blob.models.CpkInfo;
import com.azure.storage.blob.models.CustomerProvidedKey;
import com.azure.storage.blob.models.SessionMode;
import com.azure.storage.blob.models.SessionOptions;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.implementation.connectionstring.StorageAuthenticationSettings;
import com.azure.storage.common.implementation.connectionstring.StorageConnectionString;
Expand Down Expand Up @@ -92,6 +94,7 @@ public final class BlobClientBuilder
private Configuration configuration;
private BlobServiceVersion version;
private BlobAudience audience;
private SessionOptions sessionOptions = new SessionOptions();

/**
* Creates a builder instance that is able to configure and construct {@link BlobClient BlobClients} and {@link
Expand Down Expand Up @@ -133,6 +136,12 @@ public BlobClient buildClient() {
new IllegalArgumentException("Customer provided key and encryption " + "scope cannot both be set"));
}

if (CoreUtils.isNullOrEmpty(containerName) && !CoreUtils.isNullOrEmpty(sessionOptions.getContainerName())) {
containerName = sessionOptions.getContainerName();
}

BuilderHelper.validateSessionMode(sessionOptions, containerName, LOGGER);

/*
Implicit and explicit root container access are functionally equivalent, but explicit references are easier
to read and debug.
Expand Down Expand Up @@ -180,6 +189,11 @@ public BlobAsyncClient buildAsyncClient() {
new IllegalArgumentException("Customer provided key and encryption " + "scope cannot both be set"));
}

if (CoreUtils.isNullOrEmpty(containerName) && !CoreUtils.isNullOrEmpty(sessionOptions.getContainerName())) {
containerName = sessionOptions.getContainerName();
}
BuilderHelper.validateSessionMode(sessionOptions, containerName, LOGGER);

/*
Implicit and explicit root container access are functionally equivalent, but explicit references are easier
to read and debug.
Expand All @@ -189,18 +203,27 @@ public BlobAsyncClient buildAsyncClient() {

BlobServiceVersion serviceVersion = version != null ? version : BlobServiceVersion.getLatest();

HttpPipeline pipeline = constructPipeline();
HttpPipeline pipeline = constructPipeline(blobContainerName, serviceVersion);

return new BlobAsyncClient(pipeline, endpoint, serviceVersion, accountName, blobContainerName, blobName,
snapshot, customerProvidedKey, encryptionScope, versionId);
}

private HttpPipeline constructPipeline() {
return (httpPipeline != null)
? httpPipeline
: BuilderHelper.buildPipeline(storageSharedKeyCredential, tokenCredential, azureSasCredential, sasToken,
endpoint, retryOptions, coreRetryOptions, logOptions, clientOptions, httpClient, perCallPolicies,
perRetryPolicies, configuration, audience, LOGGER);
private HttpPipeline constructPipeline(String containerName, BlobServiceVersion serviceVersion) {
if (httpPipeline != null) {
return httpPipeline;
}

if (containerName != null) {
sessionOptions.setContainerName(containerName);
}
if (sessionOptions.getAccountName() == null) {
sessionOptions.setAccountName(accountName);
}

return BuilderHelper.buildPipeline(storageSharedKeyCredential, tokenCredential, azureSasCredential, sasToken,
endpoint, retryOptions, coreRetryOptions, logOptions, clientOptions, httpClient, perCallPolicies,
perRetryPolicies, configuration, audience, LOGGER, sessionOptions, serviceVersion);
}

/**
Expand Down Expand Up @@ -650,4 +673,20 @@ public BlobClientBuilder audience(BlobAudience audience) {
this.audience = audience;
return this;
}

/**
* Sets the {@link SessionOptions} that controls how the SDK manages session-based authentication for this blob.
* <p>
* Sessions amortize authentication and authorization cost across many requests by signing them with a lightweight
* HMAC key instead of a full bearer token. When the session mode within the options is set to a value other than
* {@link SessionMode#NONE}, this builder's configured container name is used when the options don't specify one.
*
* @param sessionOptions The session options to use. If {@code null}, defaults to {@link SessionMode#AUTO}
* when identity-based authentication (bearer token) is configured.
* @return the updated BlobClientBuilder object.
*/
public BlobClientBuilder sessionOptions(SessionOptions sessionOptions) {
this.sessionOptions = SessionOptions.orDefault(sessionOptions);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
import com.azure.storage.blob.implementation.models.EncryptionScope;
import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse;
import com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse;
import com.azure.storage.blob.implementation.models.AuthenticationType;
import com.azure.storage.blob.implementation.models.CreateSessionConfiguration;
import com.azure.storage.blob.implementation.models.CreateSessionResponse;
import com.azure.storage.blob.implementation.util.BlobConstants;
import com.azure.storage.blob.implementation.util.BlobSasImplUtil;
import com.azure.storage.blob.implementation.util.ModelHelper;
Expand Down Expand Up @@ -1691,11 +1694,39 @@ public String generateSas(BlobServiceSasSignatureValues blobServiceSasSignatureV
.generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context);
}

// private boolean validateNoTime(BlobRequestConditions modifiedRequestConditions) {
// if (modifiedRequestConditions == null) {
// return true;
// }
// return modifiedRequestConditions.getIfModifiedSince() == null
// && modifiedRequestConditions.getIfUnmodifiedSince() == null;
// }
/**
* Creates a session scoped to this container. The session provides temporary credentials (a session token and
* session key) that can be used to sign subsequent requests using the Shared Key protocol.
*
* @return A {@link Mono} containing the {@link CreateSessionResponse} with session credentials.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
Mono<CreateSessionResponse> createSession() {
return createSessionWithResponse().flatMap(FluxUtil::toMono);
}

/**
* Creates a session scoped to this container. The session provides temporary credentials (a session token and
* session key) that can be used to sign subsequent requests using the Shared Key protocol.
*
* @return A {@link Mono} containing a {@link Response} with the {@link CreateSessionResponse}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
Mono<Response<CreateSessionResponse>> createSessionWithResponse() {
try {
return withContext(this::createSessionWithResponse);
} catch (RuntimeException ex) {
return monoError(LOGGER, ex);
}
}

Mono<Response<CreateSessionResponse>> createSessionWithResponse(Context context) {
context = context == null ? Context.NONE : context;
CreateSessionConfiguration config
= new CreateSessionConfiguration().setAuthenticationType(AuthenticationType.HMAC);
return this.azureBlobStorage.getContainers()
.createSessionWithResponseAsync(containerName, config, null, null, context)
.map(response -> new SimpleResponse<>(response, response.getValue()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
import com.azure.storage.blob.implementation.models.FilterBlobSegment;
import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse;
import com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse;
import com.azure.storage.blob.implementation.models.AuthenticationType;
import com.azure.storage.blob.implementation.models.CreateSessionConfiguration;
import com.azure.storage.blob.implementation.models.CreateSessionResponse;
import com.azure.storage.blob.implementation.util.BlobConstants;
import com.azure.storage.blob.implementation.util.BlobSasImplUtil;
import com.azure.storage.blob.implementation.util.ModelHelper;
Expand Down Expand Up @@ -1509,4 +1512,37 @@ public String generateSas(BlobServiceSasSignatureValues blobServiceSasSignatureV
.generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context);
}

/**
* Creates a session scoped to this container. The session provides temporary credentials (a session token and
* session key) that can be used to sign subsequent requests using the Shared Key protocol.
*
* @return The {@link CreateSessionResponse} with session credentials.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
CreateSessionResponse createSession() {
return createSessionWithResponse(null, Context.NONE).getValue();
}

/**
* Creates a session scoped to this container. The session provides temporary credentials (a session token and
* session key) that can be used to sign subsequent requests using the Shared Key protocol.
*
* @param timeout An optional timeout value beyond which a {@link RuntimeException} will be raised.
* @param context Additional context that is passed through the Http pipeline during the service call.
* @return A {@link Response} containing the {@link CreateSessionResponse}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
Response<CreateSessionResponse> createSessionWithResponse(Duration timeout, Context context) {
Context finalContext = context == null ? Context.NONE : context;
CreateSessionConfiguration config
= new CreateSessionConfiguration().setAuthenticationType(AuthenticationType.HMAC);

Callable<Response<CreateSessionResponse>> operation = () -> {
Response<CreateSessionResponse> response = this.azureBlobStorage.getContainers()
.createSessionWithResponse(containerName, config, null, null, finalContext);
return new SimpleResponse<>(response, response.getValue());
};

return sendRequest(operation, timeout, BlobStorageException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import com.azure.storage.blob.models.BlobContainerEncryptionScope;
import com.azure.storage.blob.models.CpkInfo;
import com.azure.storage.blob.models.CustomerProvidedKey;
import com.azure.storage.blob.models.SessionOptions;
import com.azure.storage.blob.models.SessionMode;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.implementation.connectionstring.StorageAuthenticationSettings;
import com.azure.storage.common.implementation.connectionstring.StorageConnectionString;
Expand Down Expand Up @@ -91,6 +93,7 @@ public final class BlobContainerClientBuilder implements TokenCredentialTrait<Bl
private Configuration configuration;
private BlobServiceVersion version;
private BlobAudience audience;
private SessionOptions sessionOptions = new SessionOptions();

/**
* Creates a builder instance that is able to configure and construct {@link BlobContainerClient ContainerClients}
Expand Down Expand Up @@ -124,6 +127,12 @@ public BlobContainerClient buildClient() {
new IllegalArgumentException("Customer provided key and encryption " + "scope cannot both be set"));
}

if (CoreUtils.isNullOrEmpty(containerName) && !CoreUtils.isNullOrEmpty(sessionOptions.getContainerName())) {
containerName = sessionOptions.getContainerName();
}

BuilderHelper.validateSessionMode(sessionOptions, containerName, LOGGER);

/*
Implicit and explicit root container access are functionally equivalent, but explicit references are easier
to read and debug.
Expand All @@ -133,7 +142,7 @@ public BlobContainerClient buildClient() {

BlobServiceVersion serviceVersion = version != null ? version : BlobServiceVersion.getLatest();

HttpPipeline pipeline = constructPipeline();
HttpPipeline pipeline = constructPipeline(blobContainerName, serviceVersion);

return new BlobContainerClient(pipeline, endpoint, serviceVersion, accountName, blobContainerName,
customerProvidedKey, encryptionScope, blobContainerEncryptionScope);
Expand Down Expand Up @@ -165,6 +174,12 @@ public BlobContainerAsyncClient buildAsyncClient() {
new IllegalArgumentException("Customer provided key and encryption " + "scope cannot both be set"));
}

if (CoreUtils.isNullOrEmpty(containerName) && !CoreUtils.isNullOrEmpty(sessionOptions.getContainerName())) {
containerName = sessionOptions.getContainerName();
}

BuilderHelper.validateSessionMode(sessionOptions, containerName, LOGGER);

/*
Implicit and explicit root container access are functionally equivalent, but explicit references are easier
to read and debug.
Expand All @@ -174,18 +189,25 @@ public BlobContainerAsyncClient buildAsyncClient() {

BlobServiceVersion serviceVersion = version != null ? version : BlobServiceVersion.getLatest();

HttpPipeline pipeline = constructPipeline();
HttpPipeline pipeline = constructPipeline(blobContainerName, serviceVersion);

return new BlobContainerAsyncClient(pipeline, endpoint, serviceVersion, accountName, blobContainerName,
customerProvidedKey, encryptionScope, blobContainerEncryptionScope);
}

private HttpPipeline constructPipeline() {
return (httpPipeline != null)
? httpPipeline
: BuilderHelper.buildPipeline(storageSharedKeyCredential, tokenCredential, azureSasCredential, sasToken,
endpoint, retryOptions, coreRetryOptions, logOptions, clientOptions, httpClient, perCallPolicies,
perRetryPolicies, configuration, audience, LOGGER);
private HttpPipeline constructPipeline(String containerName, BlobServiceVersion serviceVersion) {
if (httpPipeline != null) {
return httpPipeline;
}
if (containerName != null) {
sessionOptions.setContainerName(containerName);
}
if (sessionOptions.getAccountName() == null) {
sessionOptions.setAccountName(accountName);
}
return BuilderHelper.buildPipeline(storageSharedKeyCredential, tokenCredential, azureSasCredential, sasToken,
endpoint, retryOptions, coreRetryOptions, logOptions, clientOptions, httpClient, perCallPolicies,
perRetryPolicies, configuration, audience, LOGGER, sessionOptions, serviceVersion);
}

/**
Expand Down Expand Up @@ -606,4 +628,22 @@ public BlobContainerClientBuilder audience(BlobAudience audience) {
this.audience = audience;
return this;
}

/**
* Sets the {@link SessionOptions} that controls how the SDK manages session-based authentication
* for this container.
* <p>
* Sessions amortize authentication and authorization cost across many requests by signing them
* with a lightweight HMAC key instead of a full bearer token. When the session mode within the options
* is set to a value other than {@link SessionMode#NONE},
* {@link #containerName(String) containerName} must also be set.
*
* @param sessionOptions The session options to use. If {@code null}, defaults to {@link SessionMode#AUTO}
* when identity-based authentication (bearer token) is configured.
* @return the updated BlobContainerClientBuilder object.
*/
public BlobContainerClientBuilder sessionOptions(SessionOptions sessionOptions) {
this.sessionOptions = SessionOptions.orDefault(sessionOptions);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ public BlobContainerClient getBlobContainerClient(String containerName) {
if (CoreUtils.isNullOrEmpty(containerName)) {
containerName = BlobContainerClient.ROOT_CONTAINER_NAME;
}

return new BlobContainerClient(getHttpPipeline(), getAccountUrl(), getServiceVersion(), getAccountName(),
containerName, customerProvidedKey, encryptionScope, blobContainerEncryptionScope);
}
Expand Down
Loading
Loading