Generate blob sasurl using SAS - c#

I want to get a reference to the blob and generate a SAS URL for it.
How? Without exposing my storage account key?
What all have I tried? Getting the reference to blob by using SAS (of blob container or storage account).
My references: https://learn.microsoft.com/en-us/azure/storage/common/storage-dotnet-shared-access-signature-part-1?toc=%2fazure%2fstorage%2fblobs%2ftoc.json
The exception that I see: "Can not create Shared Access Signature unless Account Key credentials are used"
But I do not (obviously) want to expose my account key! Is this even possible? If not, is there any other way of doing it?

In short: no, there's no other way to do that besides using one of the keys. You need one of the Access Keys to be able to create a SAS token. Here's why you cannot do that with an existing SAS token:
The signature is an HMAC computed over the string-to-sign and key using the SHA256 algorithm, and then encoded using Base64 encoding.
This means the signature that is part of your SAS token is a calculated value. Part of that calculation is based on (one of the) key(s), since that is used to calculate the non-reversible hash. The fact that this hash is non-reversible means you cannot retrieve the Access Key used to calculate the hash. And therefor, you cannot use a SAS token to create another SAS token: you don't have an Access Key available to calculate the signature.
When you create a storage account, you get two storage access keys, which provide full control over the storage account contents. These keys are admin credentials.
More information: Constructing a Service SAS

This is an interpretation of this example
First we have this to get the accountKey:
public static async Task<StorageAccountKey> GetAccountKeys(string KeyName)
{
IAzure storageAccounts;
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(#"AZURE_TENANT_ID"))
&& !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(#"AZURE_SUBSCRIPTION_ID")))
{
storageAccounts = GetStorageAccountWithTenantAndSubscription();
}
else
{
AzureCredentials credentials = SdkContext.AzureCredentialsFactory.FromSystemAssignedManagedServiceIdentity(MSIResourceType.AppService, AzureEnvironment.AzureGlobalCloud);
storageAccounts = Microsoft.Azure.Management.Fluent.Azure
.Authenticate(credentials)
.WithDefaultSubscription();
}
IStorageAccount storageAccount = await storageAccounts.StorageAccounts.GetByResourceGroupAsync(
Environment.GetEnvironmentVariable("STORAGE_ACCOUNT_GROUP"),
Environment.GetEnvironmentVariable("STORAGE_ACCOUNT_NAME")
);
IReadOnlyList<StorageAccountKey> accountKeys = storageAccount.GetKeys();
return accountKeys.FirstOrDefault(k => k.KeyName == KeyName);
}
private static IAzure GetStorageAccountWithTenantAndSubscription()
{
DefaultAzureCredential tokenCred = new DefaultAzureCredential(includeInteractiveCredentials: true);
string armToken = tokenCred.GetToken(new TokenRequestContext(scopes: new[] { "https://management.azure.com/.default" }, parentRequestId: null), default).Token;
TokenCredentials armCreds = new TokenCredentials(armToken);
string graphToken = tokenCred.GetToken(new TokenRequestContext(scopes: new[] { "https://graph.windows.net/.default" }, parentRequestId: null), default).Token;
TokenCredentials graphCreds = new TokenCredentials(graphToken);
AzureCredentials credentials = new AzureCredentials(armCreds, graphCreds, Environment.GetEnvironmentVariable(#"AZURE_TENANT_ID"), AzureEnvironment.AzureGlobalCloud);
return Microsoft.Azure.Management.Fluent.Azure
.Authenticate(credentials)
.WithSubscription(Environment.GetEnvironmentVariable(#"AZURE_SUBSCRIPTION_ID"));
}
Where you need to define the next environment variables:
AZURE_TENANT_ID
AZURE_SUBSCRIPTION_ID
STORAGE_ACCOUNT_GROUP
STORAGE_ACCOUNT_NAME
All of them can be found on the https://portal.azure.com/ and if you run az login
then you can do this to generate the connection string:
private static async Task<string> GetAccountSASToken()
{
StorageAccountKey accountKeyObj = await GetAccountKeys(Environment.GetEnvironmentVariable("STORAGE_ACCOUNT_KEY"));
string accountKey = accountKeyObj.Value;
string accountName = Environment.GetEnvironmentVariable("STORAGE_ACCOUNT_NAME");
StorageSharedKeyCredential key = new StorageSharedKeyCredential(accountName, accountKey);
AccountSasBuilder sasBuilder = new AccountSasBuilder()
{
Services = AccountSasServices.Blobs | AccountSasServices.Files,
ResourceTypes = AccountSasResourceTypes.Container | AccountSasResourceTypes.Object,
ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),
Protocol = SasProtocol.Https
};
sasBuilder.SetPermissions(AccountSasPermissions.List | AccountSasPermissions.Read);
string sasToken = sasBuilder.ToSasQueryParameters(key).ToString();
return sasToken;
}
And that's all you need.

Related

Azure C# KeyVaultErrorException: Operation returned an invalid status code 'Forbidden'

I am writing a program that tries to access a secret (OneAuthZAuthentication) to an Azure Table Storage through accessing KeyVault. I am following the steps listed in this tutorial: https://jeanpaul.cloud/2019/12/07/azure-key-vault-access-from-c/
I have created a Key Vault called ITALocalBuildSecrets:
With the following DNS Name: https://italocalbuildsecrets.vault.azure.net/
I also have another secret with the following name (OneAuthZAuthentication):
I have created an app in the active directory (OneAuthZUserApplication), and you can see the Application (client) ID displayed below:
I created a client secret for OneAuthZUserApplication:
I authorized a Console Application (OneAuthZUserApplication) as an access policy:
And you can clearly see the access policy being registered:
Below is the code I am running:
// Retrieves the access token necessary to gain authentication into the key vault
[FunctionName("GetToken")]
public static async System.Threading.Tasks.Task<string> GetToken(string authority, string resource, string scope)
{
var clientId = "5cf497b0-3467-456a-a03a-4d4414b*****"; // Stars are for security reasons :D
var clientSecret = "468.26i5Wc.nQ6TYL-eOvBmcto.t.*****"; // Stars are for security reasons
ClientCredential credential = new ClientCredential(clientId, clientSecret);
var context = new AuthenticationContext(authority, TokenCache.DefaultShared);
var result = await context.AcquireTokenAsync(resource, credential);
return result.AccessToken;
}
// Retrieves the access key vault accountKey (needed to authenticate access into the role assignments table)
public static string GetVaultValue()
{
KeyVaultClient client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
var vaultAddress = "https://italocalbuildsecrets.vault.azure.net/";
var secretName = "OneAuthZAuthentication";
var secret = client.GetSecretAsync(vaultAddress, secretName).GetAwaiter().GetResult();
return secret.Value;
}
[FunctionName("Function1")]
// Function that reads a small portion of the role assignments table (OneAuthZRoleAssignments) every
// configurable number of times
public static async System.Threading.Tasks.Task RunAsync([TimerTrigger("%TimerTriggerPeriod%")]TimerInfo myTimer, ILogger log)
{
Console.WriteLine($"Secret Value from Vault is: {GetVaultValue()}");
}
I get the following error:
Function1. Microsoft.Azure.KeyVault: Operation returned an invalid status code 'Forbidden'.
This does seems strange, considering that I authorized the OneAuthZUserApplication application to the key vault.
I follow you steps and use your code to test, and it all works very well.
Please go to confirm after adding Access policy, remember to click save button.
What is the authority you are using? Further, I think you are missing the step of configuring scopes when getting the token. Similar here, but using MSAL.
string[] scopeArray = new string[] { "https://vault.azure.net/.default" };
And provide that to your token request.
Also, if these are Azure Functions, why don't you use the function MSI to retrieve the secret? See here

Azure Blob Container, Can't Generate Token

I'm working on azure storage but I cannot create a proper SAS token to pass to my frontend javascript. Following multiple tutorials and examples, I can't seem to get a working token for JS.
I'm validating my token at on the tutorial here so that my own javascript doesn't get in my way: https://dmrelease.blob.core.windows.net/azurestoragejssample/samples/sample-blob.html
I've spent hours trying out different solutions, but my token generated looks so similar to the one generated by azure. What am I missing?
code
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference(containerName);
//Set the expiry time and permissions for the container.
//In this case no start time is specified, so the shared access signature becomes valid immediately.
SharedAccessBlobPolicy sasConstraints = new SharedAccessBlobPolicy();
sasConstraints.SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddHours(24);
sasConstraints.Permissions = SharedAccessBlobPermissions.List | SharedAccessBlobPermissions.Write;
//Generate the shared access signature on the container, setting the constraints directly on the signature.
string sasContainerToken = container.GetSharedAccessSignature(sasConstraints);
//Return the URI string for the container, including the SAS token.
return sasContainerToken;
Based on my test, the code is ok for generating SAS token. If you want to list the blobs in the container, you need to add &comp=list&restype=container to your SAS URL. Then it should work.
Get https://xxxxx.blob.core.windows.net/test?sv=2018-03-28&sr=c&sig=xxxxxxxxx&sp=rwl&comp=list&restype=container
Azure Storage Service is not able to identify if the resource you're trying to access is a blob or a container and assumes it's a blob. Since it assumes the resource type is blob, it makes use of $root blob container for SAS calculation (which you can see from your error message). Since SAS was calculated for mark blob container, you get this Signature Does Not Match error. By specifying restype=container you're telling storage service to treat the resource as container. comp=list is required as per REST API specification.
For more information, please refer to another SO thread.
Regarding the issue, have you tried to use JS to create a SAS token.
var azure = require('azure-storage');
var fs = require('fs');
var SasConstants = azure.Constants.AccountSasConstants;
var blobService = azure.createBlobService();
var containerName = 'containername';
var blobName = 'blobname';
var startDate = new Date('');
var expiryDate = new Date(startDate);
expiryDate.setDate(startDate.getDate() + 1);
var sharedAccessPolicy = {
AccessPolicy: {
Permissions: azure.BlobUtilities.SharedAccessPermissions.READ + azure.BlobUtilities.SharedAccessPermissions.ADD + azure.BlobUtilities.SharedAccessPermissions.CREATE+ azure.BlobUtilities.SharedAccessPermissions.WRITE,
Start: startDate,
Expiry: expiryDate
},
};
var token = blobService.generateSharedAccessSignature(containerName, null, sharedAccessPolicy);
Generate a token for the storage account instead. The permissions in the tutorial listed are granted by the storage account policy.
public static string GenerateAccountSASToken(string connectionString)
{
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
SharedAccessAccountPolicy accountpolicy = new SharedAccessAccountPolicy();
accountpolicy.SharedAccessStartTime = DateTimeOffset.UtcNow.AddHours(-24);
accountpolicy.SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddHours(24);
accountpolicy.Permissions = SharedAccessAccountPermissions.Add | SharedAccessAccountPermissions.Create | SharedAccessAccountPermissions.List | SharedAccessAccountPermissions.ProcessMessages | SharedAccessAccountPermissions.Read | SharedAccessAccountPermissions.Update | SharedAccessAccountPermissions.Write;
accountpolicy.Services = SharedAccessAccountServices.Blob;
accountpolicy.ResourceTypes = SharedAccessAccountResourceTypes.Container | SharedAccessAccountResourceTypes.Object | SharedAccessAccountResourceTypes.Service;
return storageAccount.GetSharedAccessSignature(accountpolicy);
}

Getting Azure Active directory token

I have an Azure Account, now I'm trying to get token in an console application to manage resources (i.e. create a resource group etc):
string userName = "xyz#gmail.com";
string password = "XXXXXXXXX";
string directoryName = "xyzgmail.onmicrosoft.com";
string clientId = "guid-of-registered-application-xxx";
var credentials = new UserPasswordCredential(userName, password);
var authenticationContext = new AuthenticationContext("https://login.windows.net/" + directoryName);
var result = await authenticationContext.AcquireTokenAsync("https://management.core.windows.net/", clientId, credentials);
On AcquireTokenAsync call I have
Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException:
'accessing_ws_metadata_exchange_failed: Accessing WS metadata exchange
failed'
Can anybody help, please?
Update: how I tried to create a resource group under newly created user
var jwtToken = result.AccessToken;
string subscriptionId = "XX-XX-XX-YY-YY-YY";
var tokenCredentials = new TokenCredentials(jwtToken);
var client = new ResourceManagementClient(tokenCredentials);
client.SubscriptionId = subscriptionId;
var rgResponse = await client.ResourceGroups.CreateOrUpdateWithHttpMessagesAsync("myresgroup77777",
new ResourceGroup("East US"));
Here I got another exception
'The client 'newaduser#xyzgmail.onmicrosoft.com' with object id
'aaa-aaa-aaa-aaa' does not have authorization to perform action
'Microsoft.Resources/subscriptions/resourcegroups/write' over scope
'/subscriptions/XX-XX-XX-YY-YY-YY/resourcegroups/myresgroup77777'.'
Not sure why you're getting the first error, but the second error is because the signed in user does not have permission to perform the operation (as mentioned in the error message).
When you assign the permission to execute Windows Azure Service Management API, it is actually assigned to the application which assumes the identity of the signed in user.
In order to perform Create Resource Group operation in Azure Subscription, that user must be in a role that allows this operation to be performed. You can try by assigning built-in Contributor role at the Azure Subscription level to this user.
Also, regarding using login.windows.net v/s login.microsoftonline.com, it is recommended that you use latter. When you use login.windows.net, it gets automatically redirected to login.microsoftonline.com. Using login.microsoftonline.com will save you one redirection.

Programmatically get Azure storage account properties

I wrote in my C# web application a method that deletes old blobs from Azure storage account.
This is my code:
public void CleanupIotHubExpiredBlobs()
{
const string StorageAccountName = "storageName";
const string StorageAccountKey = "XXXXXXXXXX";
const string StorageContainerName = "outputblob";
string storageConnectionString = string.Format("DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}", StorageAccountName, StorageAccountKey);
// Retrieve storage account from connection string.
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(storageConnectionString);
// Create the blob client.
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
// select container in which to look for old blobs.
CloudBlobContainer container = blobClient.GetContainerReference(StorageContainerName);
// set up Blob access condition option which will filter all the blobs which are not modified for X (this.m_CleanupExpirationNumOfDays) amount of days
IEnumerable<IListBlobItem> blobs = container.ListBlobs("", true);
foreach (IListBlobItem blob in blobs)
{
CloudBlockBlob cloudBlob = blob as CloudBlockBlob;
Console.WriteLine(cloudBlob.Properties);
cloudBlob.DeleteIfExists(DeleteSnapshotsOption.None, AccessCondition.GenerateIfNotModifiedSinceCondition(DateTime.Now.AddDays(-1 * 0.04)), null, null);
}
LogMessageToFile("Remove old blobs from storage account");
}
as you can see, In order to achieve that The method has to receive StorageAccountName and StorageAccountKey parameters.
One way to do that is by configuring these parameters in a config file for the app to use, But this means the user has to manually insert these two parameters to the config file.
My question is:
is there a way to programmatically retrieve at least one of these parameters in my code, so that at least the user will have to insert only one parameters and not two? my goal is to make the user's life easier.
My question is: is there a way to programmatically retrieve at least one of these parameters in my code, so that at least the user will have to insert only one parameters and not two? my goal is to make the user's life easier.
According to your description, I suggest you could use azure rest api to get the storage account key by using account name.
Besides, we could also use rest api to list all the rescourse group's storage account name, but it still need to send the rescourse group name as parameter to the azure management url.
You could send the request to the azure management as below url:
POST: https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resrouceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}/listKeys?api-version=2016-01-01
Authorization: Bearer {token}
More details, you could refer to below codes:
Notice: Using this way, you need firstly create an Azure Active Directory application and service principal. After you generate the service principal, you could get the applicationid,access key and talentid. More details, you could refer to this article.
Code:
string tenantId = " ";
string clientId = " ";
string clientSecret = " ";
string subscription = " ";
string resourcegroup = "BrandoSecondTest";
string accountname = "brandofirststorage";
string authContextURL = "https://login.windows.net/" + tenantId;
var authenticationContext = new AuthenticationContext(authContextURL);
var credential = new ClientCredential(clientId, clientSecret);
var result = authenticationContext.AcquireTokenAsync(resource: "https://management.azure.com/", clientCredential: credential).Result;
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the JWT token");
}
string token = result.AccessToken;
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(string.Format("https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}/listKeys?api-version=2016-01-01", subscription, resourcegroup, accountname));
request.Method = "POST";
request.Headers["Authorization"] = "Bearer " + token;
request.ContentType = "application/json";
request.ContentLength = 0;
//Get the response
var httpResponse = (HttpWebResponse)request.GetResponse();
using (System.IO.StreamReader r = new System.IO.StreamReader(httpResponse.GetResponseStream()))
{
string jsonResponse = r.ReadToEnd();
Console.WriteLine(jsonResponse);
}
Result:

C# - Using Azure Key Vault with Azure Storage on Native App

I'm using the following code to upload to my image container in my Azure storange account. The connection string in app.config is:
<appSettings>
<add key="StorageConnectionString" value="MyConnectionString" />
</appSettings>
CloudStorageAccount storageAccount = CloudStorageAccount.Parse
CloudConfigurationManager.GetSetting("StorageConnectionString"));
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
// Retrieve reference to a previously created container.
CloudBlobContainer container = blobClient.GetContainerReference("imagestorage");
// Retrieve reference to a blob named "myblob".
CloudBlockBlob blockBlob = container.GetBlockBlobReference("IMG1.png");
// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = System.IO.File.OpenRead(#"D:\Untitled.png"))
{
blockBlob.UploadFromStream(fileStream);
}
Question is how do I intergrate Azure Key Vault into my native application so that my API keys will not be compromised by some annoying reverse engineers?
I've registered my app in Azure Active Directory and given permissions for Azure Key Vault.
Also, who ever tries to use my native desktop app has to log in to my ASP.NET Web API app with Individual Accounts and receive a token, before using any other features. All of my controllers require authorization.
I believe what you're trying to do is integrate your Azure KeyVault with your C# Application. You can do this my using 2 API. One being the Microsoft.Azure.KeyVault and the other being ADAL.
Following these steps may get you answer:
public async Task<string> GetToken(string authority, string resource, string scope)
{
var authContext = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(ConfigurationManager.AppSettings["ClientID"], ConfigurationManager.AppSettings["ClientSecret"]);
AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
if(result == null)
{
throw new InvalidOperationException("Failed to obtain the JWT Token");
}
Console.WriteLine("Retrieved Password");
return result.AccessToken;
}
And then get the value of what you're trying to return by running this:
public async Task getvaluesAsync()
{
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
var sec = await kv.GetSecretAsync(ConfigurationManager.AppSettings["SecretURI"]);
EncryptSecret = sec.Value;
}
Replace the appropriate values of ClientID, Client Secret and SecretURI with your values in the App.config file. Use a getter and setter method with the "EcryptSecret" by doing something like,
public static string EncryptSecret { get; set; }
This will continuously store the password / DB connections for further use.
A few helpful articles would be:
https://learn.microsoft.com/en-us/azure/key-vault/key-vault-developers-guide

Categories