Skip to content

Commit 2c209e7

Browse files
authored
feat: add SAS token authentication for Azure Blob Storage (#148)
1 parent dc83fb4 commit 2c209e7

10 files changed

Lines changed: 128 additions & 16 deletions

File tree

cpp/azure/azcache_provider/azcache_provider_loader.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ CacheLibHandle::~CacheLibHandle()
204204
// Note: we intentionally do NOT call dlclose() here.
205205
lib_handle = nullptr;
206206

207-
LOG(DEBUG) << "AzCacheProvider: released cache provider handle";
207+
LOG(INFO) << "AzCacheProvider: released cache provider handle";
208208
}
209209

210210
// --- AzCacheProviderLoader implementation ---
@@ -297,6 +297,7 @@ AzCacheProviderLoader::AzCacheProviderLoader(const CacheProviderConfig& config)
297297
{
298298
_enabled = true;
299299
}
300+
LOG(INFO) << "AzCacheProvider: cache provider is " << (_enabled ? "enabled" : "disabled");
300301
}
301302

302303
AzCacheProviderLoader::~AzCacheProviderLoader()

cpp/azure/azure.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
// 2. Authentication methods (checked in this order):
1212
// a. Connection string (Azurite testing only, requires AZURITE_TESTING build):
1313
// - Set AZURE_STORAGE_CONNECTION_STRING environment variable
14-
// b. Storage account key:
14+
// b. SAS token:
15+
// - Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_SAS_TOKEN environment variables
16+
// - Token is appended as query string to the service URL
17+
// c. Storage account key:
1518
// - Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY environment variables
1619
// - Uses StorageSharedKeyCredential
17-
// c. DefaultAzureCredential:
20+
// d. DefaultAzureCredential (Recommended):
1821
// - Set AZURE_STORAGE_ACCOUNT_NAME environment variable
1922
// - DefaultAzureCredential tries multiple authentication methods in order:
2023
// * Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET) for service principal
@@ -29,8 +32,10 @@
2932
//
3033
// Example usage:
3134
// key-based: AZURE_STORAGE_ACCOUNT_NAME="account" AZURE_STORAGE_ACCOUNT_KEY="key" <streamer app> az://container/path
35+
// sas-token: AZURE_STORAGE_ACCOUNT_NAME="account" AZURE_STORAGE_SAS_TOKEN="sv=2021-08-06&ss=b&..." <streamer app> az://container/path
3236
// managed: AZURE_STORAGE_ACCOUNT_NAME="account" <streamer app> az://container/path
3337
// azurite: AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;..." <streamer app> az://container/path
38+
// sovereign: AZURE_STORAGE_ACCOUNT_NAME="account" AZURE_STORAGE_ENDPOINT_SUFFIX="blob.core.chinacloudapi.cn" <streamer app> az://container/path
3439
// programmatic: Pass credentials in ObjectClientConfig_t.initial_params
3540

3641
namespace runai::llm::streamer::impl::azure

cpp/azure/client/client.cc

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@ AzureClient::AzureClient(const common::backend_api::ObjectClientConfig_t& config
3535
// ClientConfiguration reads environment variables
3636
_account_name = _client_config.account_name;
3737
_account_key = _client_config.account_key;
38+
_sas_token = _client_config.sas_token;
39+
_endpoint_suffix = _client_config.endpoint_suffix;
3840
#ifdef AZURITE_TESTING
3941
_connection_string = _client_config.connection_string;
4042
#endif
4143

4244
// Parse configuration parameters from API (overrides environment)
45+
// Empty values are treated as unset to avoid overriding env vars with no-ops
4346
auto ptr = config.initial_params;
4447
if (ptr)
4548
{
@@ -48,6 +51,11 @@ AzureClient::AzureClient(const common::backend_api::ObjectClientConfig_t& config
4851
const char* key = ptr->key;
4952
const char* value = ptr->value;
5053

54+
if (!value || strlen(value) == 0)
55+
{
56+
continue;
57+
}
58+
5159
if (strcmp(key, "account_name") == 0)
5260
{
5361
_account_name = std::string(value);
@@ -56,6 +64,14 @@ AzureClient::AzureClient(const common::backend_api::ObjectClientConfig_t& config
5664
{
5765
_account_key = std::string(value);
5866
}
67+
else if (strcmp(key, "sas_token") == 0)
68+
{
69+
_sas_token = std::string(value);
70+
}
71+
else if (strcmp(key, "endpoint_suffix") == 0)
72+
{
73+
_endpoint_suffix = std::string(value);
74+
}
5975
#ifdef AZURITE_TESTING
6076
else if (strcmp(key, "connection_string") == 0)
6177
{
@@ -94,17 +110,27 @@ AzureClient::AzureClient(const common::backend_api::ObjectClientConfig_t& config
94110
} else
95111
#endif
96112
if (_account_name.has_value()) {
97-
if (_account_key.has_value()) {
113+
if (_sas_token.has_value()) {
114+
// Use SAS token authentication (no credential object needed)
115+
// The token is appended as query string to the service URL
116+
std::string token = _sas_token.value();
117+
if (!token.empty() && token[0] == '?') {
118+
token = token.substr(1);
119+
}
120+
std::string url = "https://" + _account_name.value() + "." + _endpoint_suffix + "?" + token;
121+
_blob_service_client = std::make_shared<BlobServiceClient>(url, options);
122+
LOG(DEBUG) << "Azure client initialized with SAS token for account: " << _account_name.value();
123+
} else if (_account_key.has_value()) {
98124
// Use StorageSharedKeyCredential (account name + account key)
99-
std::string url = "https://" + _account_name.value() + ".blob.core.windows.net";
125+
std::string url = "https://" + _account_name.value() + "." + _endpoint_suffix;
100126
auto credential = std::make_shared<Azure::Storage::StorageSharedKeyCredential>(
101127
_account_name.value(), _account_key.value());
102128
_blob_service_client = std::make_shared<BlobServiceClient>(url, credential, options);
103129
LOG(DEBUG) << "Azure client initialized with StorageSharedKeyCredential for account: " << _account_name.value();
104130
} else {
105131
// Use DefaultAzureCredential (managed identity, Azure CLI, environment variables, etc.)
106132
// Reference: https://learn.microsoft.com/en-us/azure/developer/cpp/sdk/authentication
107-
std::string url = "https://" + _account_name.value() + ".blob.core.windows.net";
133+
std::string url = "https://" + _account_name.value() + "." + _endpoint_suffix;
108134
// Share a single DefaultAzureCredential across all clients in the process to better
109135
// utilize token caching and reduce chances of overwhelming authentication sources
110136
// (e.g., IMDS) which can result in fatal throttling errors.
@@ -145,6 +171,8 @@ bool AzureClient::verify_credentials(const common::backend_api::ObjectClientConf
145171
ClientConfiguration temp_config;
146172
std::optional<std::string> temp_account_name = temp_config.account_name;
147173
std::optional<std::string> temp_account_key = temp_config.account_key;
174+
std::optional<std::string> temp_sas_token = temp_config.sas_token;
175+
std::string temp_endpoint_suffix = temp_config.endpoint_suffix;
148176
#ifdef AZURITE_TESTING
149177
std::optional<std::string> temp_connection_string = temp_config.connection_string;
150178
#endif
@@ -155,6 +183,8 @@ bool AzureClient::verify_credentials(const common::backend_api::ObjectClientConf
155183
for (size_t i = 0; i < config.num_initial_params; ++i, ++ptr) {
156184
if (strcmp(ptr->key, "account_name") == 0) temp_account_name = std::string(ptr->value);
157185
else if (strcmp(ptr->key, "account_key") == 0) temp_account_key = std::string(ptr->value);
186+
else if (strcmp(ptr->key, "sas_token") == 0) temp_sas_token = std::string(ptr->value);
187+
else if (strcmp(ptr->key, "endpoint_suffix") == 0) temp_endpoint_suffix = std::string(ptr->value);
158188
#ifdef AZURITE_TESTING
159189
else if (strcmp(ptr->key, "connection_string") == 0) temp_connection_string = std::string(ptr->value);
160190
#endif
@@ -166,7 +196,7 @@ bool AzureClient::verify_credentials(const common::backend_api::ObjectClientConf
166196
return (_connection_string == temp_connection_string);
167197
}
168198
#endif
169-
return (_account_name == temp_account_name) && (_account_key == temp_account_key);
199+
return (_account_name == temp_account_name) && (_account_key == temp_account_key) && (_sas_token == temp_sas_token) && (_endpoint_suffix == temp_endpoint_suffix);
170200
}
171201

172202
common::backend_api::Response AzureClient::async_read_response()

cpp/azure/client/client.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ struct AzureClient : common::IClient
4747
// Azure credentials
4848
std::optional<std::string> _account_name;
4949
std::optional<std::string> _account_key;
50+
std::optional<std::string> _sas_token;
51+
std::string _endpoint_suffix;
5052
#ifdef AZURITE_TESTING
5153
std::optional<std::string> _connection_string;
5254
#endif

cpp/azure/client_configuration/client_configuration.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ ClientConfiguration::ClientConfiguration()
2828
account_key = acct_key;
2929
}
3030

31+
// SAS token for Shared Access Signature authentication
32+
const auto sas = utils::getenv<std::string>("AZURE_STORAGE_SAS_TOKEN", "");
33+
if (!sas.empty()) {
34+
LOG(DEBUG) << "Using AZURE_STORAGE_SAS_TOKEN for authentication";
35+
sas_token = sas;
36+
}
37+
38+
// Endpoint suffix for sovereign clouds (default: blob.core.windows.net)
39+
const auto suffix = utils::getenv<std::string>("AZURE_STORAGE_ENDPOINT_SUFFIX", "");
40+
if (!suffix.empty()) {
41+
LOG(DEBUG) << "Using custom endpoint suffix: " << suffix;
42+
endpoint_suffix = suffix;
43+
}
44+
3145
// Account name configuration from environment variable
3246
// Authentication uses DefaultAzureCredential which supports:
3347
// - Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET)

cpp/azure/client_configuration/client_configuration.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ struct ClientConfiguration
1111
// Azure client configuration options
1212
std::optional<std::string> account_name;
1313
std::optional<std::string> account_key;
14+
std::optional<std::string> sas_token;
15+
std::string endpoint_suffix = "blob.core.windows.net";
1416
#ifdef AZURITE_TESTING
1517
// Connection string is only available for Azurite/local testing
1618
std::optional<std::string> connection_string;

docs/src/env-vars.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ String
114114

115115
### AZURE_STORAGE_ACCOUNT_NAME
116116

117-
Azure Storage account name. Used with DefaultAzureCredential for authentication.
117+
Azure Storage account name. Required for all Azure Blob Storage authentication methods.
118118

119119
#### Values accepted
120120

@@ -124,6 +124,32 @@ String
124124

125125
None
126126

127+
### AZURE_STORAGE_SAS_TOKEN
128+
129+
Shared Access Signature (SAS) token for Azure Blob Storage authentication. Used with AZURE_STORAGE_ACCOUNT_NAME.
130+
131+
The value should be the query string portion of the SAS URI (with or without leading `?`), e.g. `sv=2021-08-06&ss=b&srt=co&sp=rl&se=...&sig=...`
132+
133+
#### Values accepted
134+
135+
String
136+
137+
#### Default value
138+
139+
None
140+
141+
### AZURE_STORAGE_ENDPOINT_SUFFIX
142+
143+
Azure Blob Storage endpoint suffix. Override for sovereign clouds (China, US Government) or Azure Stack.
144+
145+
#### Values accepted
146+
147+
String (e.g. `blob.core.chinacloudapi.cn`, `blob.core.usgovcloudapi.net`)
148+
149+
#### Default value
150+
151+
`blob.core.windows.net`
152+
127153
### RUNAI_STREAMER_EXPERIMENTAL_AZURE_CACHE_ENABLED
128154

129155
> **Experimental** — This feature is under active development and may change in future releases.

docs/src/usage.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,11 @@ file_path = "az://my-container/my/file/path.safetensors"
159159

160160
##### Azure Authentication
161161

162-
The streamer uses Azure's DefaultAzureCredential for authentication, which provides a seamless authentication experience across development and production environments.
162+
The streamer supports multiple authentication methods for Azure Blob Storage, checked in this order:
163+
164+
1. **SAS token** (`AZURE_STORAGE_SAS_TOKEN`)
165+
2. **Storage account key** (`AZURE_STORAGE_ACCOUNT_KEY`)
166+
3. **DefaultAzureCredential** (Recommended) — managed identity, Azure CLI, service principal, etc.
163167

164168
###### Default Azure Credential (Recommended)
165169

@@ -198,6 +202,17 @@ export AZURE_STORAGE_ACCOUNT_NAME="myaccount"
198202
# No additional configuration needed - managed identity is detected automatically
199203
```
200204

205+
###### SAS Token
206+
207+
To authenticate using a Shared Access Signature:
208+
209+
```bash
210+
export AZURE_STORAGE_ACCOUNT_NAME="myaccount"
211+
export AZURE_STORAGE_SAS_TOKEN="sv=2021-08-06&ss=b&srt=co&sp=rl&se=2026-01-01T00:00:00Z&sig=..."
212+
```
213+
214+
> **Note:** The SAS token value should be the query string portion of the SAS URI without the leading `?`.
215+
201216
##### Azure Blob Cache Provider (Experimental)
202217

203218
> **Experimental** — This feature is under active development and may change in future releases.

py/runai_model_streamer_azure/runai_model_streamer_azure/credentials/credentials.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,32 @@ class AzureCredentials:
1111
1212
Authentication methods (checked in this order):
1313
1. Connection string: Set AZURE_STORAGE_CONNECTION_STRING
14-
2. Storage account key: Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY
15-
3. DefaultAzureCredential: Set AZURE_STORAGE_ACCOUNT_NAME (uses Managed Identity, Azure CLI, etc.)
14+
2. SAS token: Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_SAS_TOKEN
15+
3. Storage account key: Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY
16+
4. DefaultAzureCredential: Set AZURE_STORAGE_ACCOUNT_NAME (uses Managed Identity, Azure CLI, etc.)
1617
1718
If values are not provided explicitly, they are loaded from environment variables:
1819
- AZURE_STORAGE_CONNECTION_STRING
1920
- AZURE_STORAGE_ACCOUNT_NAME
2021
- AZURE_STORAGE_ACCOUNT_KEY
22+
- AZURE_STORAGE_SAS_TOKEN
2123
"""
2224

2325
def __init__(
2426
self,
2527
account_name: Optional[str] = None,
2628
account_key: Optional[str] = None,
29+
sas_token: Optional[str] = None,
2730
connection_string: Optional[str] = None,
31+
endpoint_suffix: Optional[str] = None,
2832
credential: Optional[DefaultAzureCredential] = None
2933
):
3034
self.connection_string = connection_string or os.environ.get("AZURE_STORAGE_CONNECTION_STRING")
3135
self.account_name = account_name or os.environ.get("AZURE_STORAGE_ACCOUNT_NAME")
3236
self.account_key = account_key or os.environ.get("AZURE_STORAGE_ACCOUNT_KEY")
33-
if credential is None and not self.connection_string and not self.account_key:
37+
self.sas_token = sas_token or os.environ.get("AZURE_STORAGE_SAS_TOKEN")
38+
self.endpoint_suffix = endpoint_suffix or os.environ.get("AZURE_STORAGE_ENDPOINT_SUFFIX", "blob.core.windows.net")
39+
if credential is None and not self.connection_string and not self.account_key and not self.sas_token:
3440
credential = DefaultAzureCredential()
3541
self.credential = credential
3642
self._validate()

py/runai_model_streamer_azure/runai_model_streamer_azure/files/files.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ def _create_client(credentials: Optional[AzureCredentials] = None) -> BlobServic
2020
2121
Authentication priority:
2222
1. Connection string (AZURE_STORAGE_CONNECTION_STRING) - for local testing with Azurite
23-
2. Storage account key (AZURE_STORAGE_ACCOUNT_NAME + AZURE_STORAGE_ACCOUNT_KEY)
24-
3. DefaultAzureCredential with account URL - for production
23+
2. SAS token (AZURE_STORAGE_ACCOUNT_NAME + AZURE_STORAGE_SAS_TOKEN)
24+
3. Storage account key (AZURE_STORAGE_ACCOUNT_NAME + AZURE_STORAGE_ACCOUNT_KEY)
25+
4. DefaultAzureCredential with account URL - for production
2526
2627
Args:
2728
credentials: Optional AzureCredentials object
@@ -39,17 +40,27 @@ def _create_client(credentials: Optional[AzureCredentials] = None) -> BlobServic
3940
user_agent=_USER_AGENT
4041
)
4142

43+
# Use account name + SAS token if available
44+
if credentials.sas_token and credentials.account_name:
45+
token = credentials.sas_token.lstrip("?")
46+
account_url = f"https://{credentials.account_name}.{credentials.endpoint_suffix}"
47+
return BlobServiceClient(
48+
account_url=account_url,
49+
credential=token,
50+
user_agent=_USER_AGENT
51+
)
52+
4253
# Use account name + account key if available (StorageSharedKeyCredential)
4354
if credentials.account_key and credentials.account_name:
44-
account_url = f"https://{credentials.account_name}.blob.core.windows.net"
55+
account_url = f"https://{credentials.account_name}.{credentials.endpoint_suffix}"
4556
return BlobServiceClient(
4657
account_url=account_url,
4758
credential=credentials.account_key,
4859
user_agent=_USER_AGENT
4960
)
5061

5162
# Use account name + DefaultAzureCredential (for production)
52-
account_url = f"https://{credentials.account_name}.blob.core.windows.net"
63+
account_url = f"https://{credentials.account_name}.{credentials.endpoint_suffix}"
5364
return BlobServiceClient(
5465
account_url=account_url,
5566
credential=credentials.credential,

0 commit comments

Comments
 (0)