I create an Azure KeyVault using Pulumi:
var currentConfig = Output.Create(GetClientConfig.InvokeAsync());
var keyvault = new KeyVault(vaultname, new KeyVaultArgs
{
Name = vaultname,
Location = _resourceGroup.Location,
ResourceGroupName = _resourceGroup.Name,
TenantId = currentConfig.Apply(q => q.TenantId),
SkuName = "standard",
AccessPolicies =
{
new Pulumi.Azure.KeyVault.Inputs.KeyVaultAccessPolicyArgs
{
TenantId=currentConfig.Apply(q=>q.TenantId),
ObjectId=currentConfig.Apply(q=>q.ObjectId),
KeyPermissions={"get", "create", "list"},
SecretPermissions={"set","get","delete","purge","recover", "list"}
}, new Pulumi.Azure.KeyVault.Inputs.KeyVaultAccessPolicyArgs
}
});
As you can see I did not only create the KeyVault but also added the current ObjectId as an Access Policy.
Directly after that I try to add an entry to the KeyVault:
new Secret("secret",new SecretArgs
{
Name = "secret",
Value = "value",
KeyVaultId = keyVault.Id
});
This works fine locally when working with a user login (az login) But when using a service principle (DevOps) instead the Vault-Creation still works but adding secrets fails because of permission issues:
azure:keyvault:Secret connectionstrings-blobstorageaccountkey
creating error: checking for presence of existing Secret
"connectionstrings-blobstorageaccountkey" (Key Vault
"https://(vaultname).vault.azure.net/"):
keyvault.BaseClient#GetSecret: Failure responding to request:
StatusCode=403 -- Original Error: autorest/azure: Service returned an
error. Status=403 Code="Forbidden" Message="The user, group or
application
'appid=;oid=(objectId);iss=https://sts.windows.net/***/'
does not have secrets get permission on key vault
';location=westeurope'.
I am using the "classic" (non-nextgen)-variant at Pulumi.Azure
The cause of this issue was that I an pulumi up locally with my personal azure account. When running pulumi up as a service connection afterwards access wasn't possible because of different credentials.
When using a different stack (and different resources) for the service everything works fine.
So if testing the pulumi configuration you should always use a different stack when testing locally if permissions are required (which they almost ever are).
I will leave this question here because I suspect a few more people will fall into the same pit.
Related
How to get AzureKeyVault Secret without storing the ClientSecret in appsettings.json? We have a WinForms application in .NET 5 using Dependency Injection and we want to add two services to the ServiceProvider while only requiring one SSO login prompt and removing the KeyVault secret string from the appsettings.json file.
The call to builder.AddAzureKeyVault(SecretClient, new KeyVaultSecretManager()) works when signed in to Azure with Visual Studio. This breaks when we deploy the application for a virtual machine where Visual Studio is not signed in. We can get this to work by passing in a secret value, but we don't want to store that in either code or appsettings. How do we store the secret in the KeyVault and then retrieve that so that we can add the AddAzureKeyVault service?
When we log out of Visual Studio (i.e., ExcludeVisualStudioCredential = true) then we get the following error:
Azure.Identity.CredentialUnavailableException: 'DefaultAzureCredential
failed to retrieve a token from the included credentials.
EnvironmentCredential authentication unavailable. Environment variables are not fully configured.
ManagedIdentityCredential authentication unavailable. No Managed Identity endpoint found.
Process "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\Extensions\gwrwzu2y.rwb\TokenService\Microsoft.Asal.TokenService.exe"
has failed with unexpected error: TS003: Error, TS005: No accounts
found. Please go to Tools->Options->Azure Services Authentication,
and add an account to be to authenticate to Azure services during
development..
Stored credentials not found. Need to authenticate user in VSCode Azure Account.
Azure CLI not installed
Please run 'Connect-AzAccount' to set up account.'
Note 1: We are using SSO and can retrieve the AccessToken. So, we have access to the AccessToken.
Note 2: we are using SlowCheetah for multiple environment configs, so we do not have ENVIRONMENT_VARIABLES setup.
From Program.cs
Static Void Main()
{
...
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, builder) =>
{
builder.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
;
builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
;
// Set the Configuration. We need to build the Configuration here so that we can get access to the Appsettings in order to
// set the SecretClient and add the Azure Key Vault. We will then need to set the Configuration again in order to capture this
// new service.
Configuration = builder.Build();
SetSecretClient(Configuration);
// Add the Azure Key Vault. This only works when signed into Azure via Visual Studio
builder.AddAzureKeyVault(SecretClient, new KeyVaultSecretManager());
})
.ConfigureServices((context, services) =>
{
Configuration = context.Configuration;
SignInUserSSO();
ConfigureServices(Configuration, services);
})
.Build();
...
}
private static void SetSecretClient(IConfiguration objConfiguration)
{
DefaultAzureCredentialOptions objDefaultAzureCredentialOptions;
SecretClientOptions objSecretClientOptions;
string strAzureKeyVaultResourceIdentifier = objConfiguration.GetValue<string>("Azure:FirmId:ResourceIdentifiers:KeyVault");
string strAzureKeyVaultName = objConfiguration.GetValue<string>("Azure:FirmId:KeyVaults:NameOfKeyVault");
string strAzureKeyVaultUri = strAzureKeyVaultResourceIdentifier.Replace("{KeyVaultName}", strAzureKeyVaultName);
// Set the options on the SecretClient. These are default values recommended by Microsoft.
objSecretClientOptions = new SecretClientOptions()
{
Retry =
{
Delay= TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromSeconds(16),
MaxRetries = 5,
Mode = RetryMode.Exponential
}
};
objDefaultAzureCredentialOptions = new DefaultAzureCredentialOptions
{
ExcludeEnvironmentCredential = true,
ExcludeManagedIdentityCredential = false,
ExcludeSharedTokenCacheCredential = true,
ExcludeVisualStudioCredential = true,
ExcludeVisualStudioCodeCredential = true,
ExcludeAzureCliCredential = true,
ExcludeInteractiveBrowserCredential = true
};
SecretClient = new SecretClient(
vaultUri: new Uri(strAzureKeyVaultUri),
credential: new DefaultAzureCredential(objDefaultAzureCredentialOptions),
objSecretClientOptions
);
}
The DefaultAzureCredential uses a number of authentication methods to support both production and development environments without changing code. So while you can use your Visual Studio, Azure CLI, or other credentials during development, you can use environment variables with service principals or managed identity in production. For VMs, managed identity is probably the easiest to configure, and for services that don't support managed identity you can use environment variables configured on your service that are authorized to access your Key Vault.
FWIW, recommended default retry settings are already the default without specifying a SecretClientOptions instance if you want to simplify code.
I am creating a SAS key in my C# code, like below.
string returnValue = String.Empty;
// Create a new access policy for the account with the following properties:
SharedAccessAccountPolicy policy = new SharedAccessAccountPolicy()
{
Permissions = SharedAccessAccountPermissions.Read | SharedAccessAccountPermissions.Write | SharedAccessAccountPermissions.Create,
Services = SharedAccessAccountServices.Blob,
ResourceTypes = SharedAccessAccountResourceTypes.Container | SharedAccessAccountResourceTypes.Object,
SharedAccessExpiryTime = DateTime.UtcNow.AddHours(10),
Protocols = SharedAccessProtocol.HttpsOrHttp
};
// Create new storage credentials using the SAS token.
ExecuteWithExceptionHandling(
() =>
{
returnValue = storageAccount.GetSharedAccessSignature(policy);
});
return returnValue;
This code generates a key properly. But when I try to use this key in Azure Storage Explorer, I get an error saying Inadequate resource type access. At least service-level ('s') access is required.
When I try to use this key from javascript to create a container, I get an error saying Refused to set unsafe header "user-agent" in azure-storage-blob.min.js file (came from azure storage api by microsoft).
I have added the create permission in SharedAccessAccountPolicy, but the key generated is not working for some reason.
Edit:
Javascript code to create blobservice.
var blobService = AzureStorage.Blob.createBlobServiceWithSas(blobUri, $("#SASToken").val());
blobService.createContainerIfNotExists(folderID, function (error, result) {
if (error) {
return false;
} else {
return true;
}
});
Edit:
Below is a screenshot of the azure portal, where I tried to generate manual token. Even that token has ss=b in it instead of sr=b as per the document.
I tried the token I generated manually. And the JS code still says Refused to set unsafe header "user-agent".
Apparently it is an issue with the damn js file provided by Azure.
https://github.com/Azure/azure-storage-node/issues/463
Here is an open ticket about it.
Anyway, for now, I have added a condition to ensure header is not added. If the code still fails, I will create a different question.
I think, within 2 days, I can just take the latest update of the Azure's js and this should fix itself. Still in case, anyone needs a quick fix, here is the code change done by me.
if (e[0] != 'user-agent'){ s.setRequestHeader(e[0],e[1])}}
instead of s.setRequestHeader(e[0],e[1])}
It is only a temporary fix. Best option is still to update to version 2.10.101 or higher for azure storage js file.
My web service is currently doing basic username/password authentication in order to subscribe the exchange user for receiving the events (like new mail event etc) like below:
var service = new ExchangeService(exchangeVersion)
{
KeepAlive = true,
Url = new Uri("some autodiscovery url"),
Credentials = new NetworkCredential(username, password)
};
var subscription = service.SubscribeToPushNotifications(
new[] { inboxFolderFoldeID },
new Uri("some post back url"),
15,
null,
EventType.NewMail,
EventType.Created,
EventType.Deleted,
EventType.Modified,
EventType.Moved,
EventType.Copied);
Now, I am supposed to replace the authentication mechanism to use OAuth protocol. I saw some examples but all of them seem to be talking about authenticating the client (https://msdn.microsoft.com/en-us/library/office/dn903761%28v=exchg.150%29.aspx?f=255&MSPPError=-2147217396) but nowhere I was able to find an example of how to authenticate an exchange user with OAuth protocol. Any code sample will help a lot. Thanks.
It's not clear what you mean with 'web service' and how you currently get the username and password. If that is some kind of website where the user needs to login or pass credentials, then you'll have to start an OAuth2 grant from the browser as in redirecting the clients browser to the authorize endpoint to start implicit grant or code grant. The user will be presented a login screen on the OAuth2 server (and not in your application), once the user logs in a code or access token (depending on the grant) will be returned to your application which you can use in the ExchangeService constructor.
If that 'web' service is some service that runs on the users computer you can use one of the methods described below.
Get AccessToken using AuthenticationContext
The example seems to be based on an older version of the AuthenticationContext class.
The other version seems to be newer, also the AcquireToken is now renamed to AcquireTokenAsync / AcquireTokenSilentAsync.
No matter which version you're using, you will not be able to pass username and password like you're doing in your current code. However, you can let the AcquireToken[Async] method prompt for credentials to the user. Which, let's be honest, is more secure then letting your application deal with those user secrets directly. Before you know, you'll be storing plain text passwords in a database (hope you aren't already).
In both versions, those methods have a lot of overloads all with different parameters and slightly different functionality. For your use-case I think these are interesting:
New: AcquireTokenAsync(string, string, Uri, IPlatformParameters) where IPlatformParameters could be new PlatformParameters(PromptBehavior.Auto)
Old: AcquireToken(string, string, Uri, PromptBehavior where prompt behavior could be PromptBehavior.Auto
Prompt behavior auto, in both vesions, means: the user will be asked for credentials when they're not already cached. Both AuthenticationContext constructors allow you to pass a token-cache which is something you can implement yourself f.e. to cache tokens in memory, file or database (see this article for an example file cache implementation).
Get AccessToken manually
If you really want to pass in the user credentials from code without prompting the user, there is always a way around. In this case you'll have to implement the Resource Owner Password Credentials grant as outlined in OAuth2 specificatioin / RFC6749.
Coincidence or not, I have an open-source library called oauth2-client-handler that implements this for use with HttpClient, but anyway, if you want to go this route you can dig into that code, especially starting from this method.
Use Access Token
Once you have an access token, you can proceed with the samples on this MSDN page, f.e.:
var service = new ExchangeService(exchangeVersion)
{
KeepAlive = true,
Url = new Uri("some autodiscovery url"),
Credentials = new OAuthCredentials(authenticationResult.AccessToken))
};
In case someone is still struggling to get it to work. We need to upload a certificate manifest on azure portal for the application and then use the same certificate to authenticate the client for getting the access token. For more details please see: https://blogs.msdn.microsoft.com/exchangedev/2015/01/21/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow/
Using the example code in this Microsoft Document as the starting point and these libraries:
Microsoft Identity Client 4.27
EWS Managed API v2.2
I am able to successfully authenticate and connect with Exchange on Office 365.
public void Connect_OAuth()
{
var cca = ConfidentialClientApplicationBuilder
.Create ( ConfigurationManager.AppSettings[ "appId" ] )
.WithClientSecret( ConfigurationManager.AppSettings[ "clientSecret" ] )
.WithTenantId ( ConfigurationManager.AppSettings[ "tenantId" ] )
.Build();
var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
AuthenticationResult authResult = null;
try
{
authResult = cca.AcquireTokenForClient( ewsScopes ).ExecuteAsync().Result;
}
catch( Exception ex )
{
Console.WriteLine( "Error: " + ex );
}
try
{
var ewsClient = new ExchangeService();
ewsClient.Url = new Uri( "https://outlook.office365.com/EWS/Exchange.asmx" );
ewsClient.Credentials = new OAuthCredentials( authResult.AccessToken );
ewsClient.ImpersonatedUserId = new ImpersonatedUserId( ConnectingIdType.SmtpAddress, "ccc#pppsystems.co.uk" );
ewsClient.HttpHeaders.Add( "X-AnchorMailbox", "ccc#pppsystems.co.uk" );
var folders = ewsClient.FindFolders( WellKnownFolderName.MsgFolderRoot, new FolderView( 10 ) );
foreach( var folder in folders )
{
Console.WriteLine( "" + folder.DisplayName );
}
}
catch( Exception ex )
{
Console.WriteLine( "Error: " + ex );
}
}
The Microsoft example code did not work - the async call to AcquireTokenForClient never returned.
By calling AcquireTokenForClient in a separate try catch block catching a general Exception, removing the await and using .Result, this now works - nothing else was changed.
I realise that this is not best practice but, both with and without the debugger, the async call in the original code never returned.
In the Azure set-up:
A client secret text string was used - a x509 certificate was not necessary
The configuration was 'app-only authentication'
Hope this helps someone avoid hours of frustration.
I am new to Azure Data Lake Analytics and am converting a C# batch job to use service to service authentication before submitting stored procedures to Azure Data Lake Analytics.
public void AuthenticateADLUser()
{
//Connect to ADL
// Service principal / appplication authentication with client secret / key
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
cTokenCreds = ApplicationTokenProvider.LoginSilentAsync(strDomain, strWebApp_clientId, strClientSecret).Result;
SetupClients(cTokenCreds, strSubscriptionID);
}
public static void SetupClients(ServiceClientCredentials tokenCreds, string subscriptionId)
{
_adlaClient = new DataLakeAnalyticsAccountManagementClient(tokenCreds);
_adlaClient.SubscriptionId = subscriptionId;
_adlaJobClient = new DataLakeAnalyticsJobManagementClient(tokenCreds);
_adlsFileSystemClient = new DataLakeStoreFileSystemManagementClient(tokenCreds);
}
Even though I have given it the correct ClientId the error comes back with a different ClientID in the error when I execute the following code:
var jobInfo = _adlaJobClient.Job.Create(_adlsAccountName, jobId, parameters);.
The error message is:
The client 'e83bb777-f3af-4526-ae34-f5461a5fde1c' with object id 'e83bb777-f3af-4526-ae34-f5461a5fde1c' does not have authorization to perform action 'Microsoft.Authorization/permissions/read' over scope '/subscriptions/a0fb08ca-a074-489c-bed0-....
Why is the ClientID different than the one I used in the code?
Is this a code issue or a permissions issue? I assume that it is code since the ClientID is not an authorized one that I created.
note: The SubscriptionId is correct.
I assumed you created an Azure Active Directory App and are you the client and domain IDs of this app. If not, you'll need that... If you do have that, then can you check if the App has permissions over your Data Lake Store: https://learn.microsoft.com/en-us/azure/data-lake-store/data-lake-store-authenticate-using-active-directory
Had exactly same symptoms. WebApp was created in AAD in portal originally to access Azure Data Lake Store and same code-snippet worked perfectly. When I decided to re-use same WebApp (clientid/secret) it failed with same error, even though I have given reader/contributor roles on sub/RG/ADLA to the App.
I think the reason is that WebApp underneath has a "service principal" object (thus error msg shows different object id) and ADLA uses it for some reason. Mine didn't have credentials set - empty result:
Get-AzureRmADSpCredential -objectid <object_id_from_error_msg>
Added new password as described here
New-AzureRmADSpCredential -objectid <object_id_from_error_msg> -password $password
Used the pwd as secret in LoginSilentAsync, clientId was left as before - WebApp clientId (not the principal object id shown in the error)
I wasn't able to find this principal info in portal, only PS.
I needed to extend the generated reset password token timeout, so I switched from using TotpSecurityStampBasedTokenProvider to DataProtectorTokenProvider, using the data protection provider from IAppBuilder.GetDataProtectionProvider().
UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create())
{
TokenLifespan = TimeSpan.FromHours(2)
};
But, when doing so I could no longer create the token on one site and use it to create a new password on another. The application uses multiple sites which are hosted on different servers.
var result = userManager.ResetPassword(user.Id, model.ResetPasswordToken, model.Password);
if (!result.Succeeded)
{
// fails when the token is generated on a different site
}
I have tried using the same machine key, but that did not work.
Everything works fine when using TotpSecurityStampBasedTokenProvider, but it does not provide a way to extend the timeout.