AWS AmazonSimpleSystemsManagementClient cannot read credentials in .NET Framework application - asp.net

I have .NET Framework application where I try to read data from AWS parameter store using AmazonSimpleSystemsManagementClient on my local environment. Besides I have credentials generated by AWS CLI and located in
Users/MyUser/.aws
folder. When I try to connect to the parameter store from CMD using the creds it works fine. Though the AmazonSimpleSystemsManagementClient in the application with default constructor, it throws exception "Unable to get IAM security credentials from EC2 Instance Metadata Service." When I tried to pass BasicAWSParameters to the client with hardcoded working keys I got another exception "The security token included in the request is invalid".
Also I tried installing EC2Config, initializing AWS SDK Store from Visual Studio AWS Toolkit. Though it didn't change the game.
I would want to avoid using environment variables or hardcoding the keys since keys are generated and valid only 1 hour. Then I should regenerate so copying them somewhere every time is not convenient for me.
Please advice how to resolve the issue.
Some code
_client = new AmazonSimpleSystemsManagementClient()
public string GetValue(string key)
{
if (_client == null)
return null;
var request = new GetParameterRequest
{
Name = $"{_baseParameterPath}/{key}",
WithDecryption = true,
};
try
{
var response = _client.GetParameterAsync(request).Result;
return response.Parameter.Value;
}
catch (Exception exc)
{
return null;
}
}
credentials file looks as following (I removed key values not to expose):
[default]
aws_access_key_id= KEY VALUE
aws_secret_access_key= KEY VALUE
aws_session_token= KEY VALUE
[MyProfile]
aws_access_key_id= KEY VALUE
aws_secret_access_key= KEY VALUE
aws_session_token= KEY VALUE

As long as you have your creds in .aws/credentials, you can create the Service client and the creds will be located and used. No need to create a BasicAWSParameters object.
Creds in a file named credentials:
[default]
aws_access_key_id=Axxxxxxxxxxxxxxxxxxxxxxxxxxx
aws_secret_access_key=/zxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
This .NET code works.
using System;
using System.Threading.Tasks;
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
namespace ConsoleApp1 {
class Program {
static async Task Main(string[] args) {
var client = new AmazonSimpleSystemsManagementClient();
var request = new GetParameterRequest()
{
Name = "RDSConnection"
};
var response = client.GetParameterAsync(request).GetAwaiter().GetResult();
Console.WriteLine("Parameter value is " + response.Parameter.Value);
}
}
}

Related

Azure Storage Static website Hosted Standalone Blazor WASM appsettings

I am hosting my standalone Blazor WASM app in an Azure Storage Account Static website and wondering how to handle switching between development and production API endpoints using settings in appsettings.json/appsettings.staging.json. The documentation I've found talks more about App Service hosted apps.
I cannot get this Blazor.start() method to work.
I must admit I haven't tried the option to inject an IConfiguration and use HttpClient but would like to check if there's a simple method.
I can't claim to have come up with this myself but I cannot find my reference right now. Anyway I ended up adding this method to my Program.cs.
Program.cs
private static async Task ConfigureApiEndpoint(WebAssemblyHostBuilder builder)
{
var http = new HttpClient()
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};
string apiEndpoint = String.Empty;
if (builder.HostEnvironment.BaseAddress.Contains("localhost"))
apiEndpoint = "api-endpoint-staging.json";
else if (builder.HostEnvironment.BaseAddress.Contains("mysubdomain.domain.com"))
apiEndpoint = "api-endpoint-staging.json";
else
apiEndpoint = "api-endpoint-production.json";
using var response = await http.GetAsync(apiEndpoint);
using var stream = await response.Content.ReadAsStreamAsync();
builder.Configuration.AddJsonStream(stream);
}
api-endpoint-staging.json
Put this in wwwroot folder. Set as Content build action. Do not copy to output.
{
"SirenApi": {
"BaseAddress": "https://mysubdomain.domain.com/api/"
}
}

How to work with Key Vault when developing locally in .net framework?

I'm wondering if there is a way I can setup a .net mvc app the same way a .net core app can be setup with Key Vault.
https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-2.2#secret-storage-in-the-development-environment
I want to be able to pull secrets in my local development environment without provisioning an Azure Key Vault.
In a .net core app I was able to follow the above link and get everything working.
// ran this command in powershell
dotnet user-secrets set "db-connection-string" "db-connection-string-value"
Setup the key vault with an empty endpoint in Program.cs (this is just for testing).
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((ctx, builder) =>
{
var keyVaultEndpoint = GetKeyVaultEndpoint();
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
builder.AddAzureKeyVault(
keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
}
}
).UseStartup<Startup>()
.Build();
private static string GetKeyVaultEndpoint() => "";
And I am able to access my secret locally using the below code.
public void OnGet()
{
Message = "My key val = " + _configuration["db-connection-string"];
}
In my asp.net app I am able to pull secrets from an Azure Key Vault. I just don't want to have to do that when developing locally since each developer could have a slightly different connection string to their local database, and requiring them to use a Key Vault in Azure is going to be annoying and cause frustration. Maybe we shouldn't use Key Vault for "legacy" applications. Any help is appreciated.
This seems similar to our development environment.
We wanted developers to use some values from the key vault, but to be able to override particular values for their local environment. (particularly the database connection string).
In the ConfigureAppConfiguration method you can register multiple configuration providers. If two configuration providers write to the same configuration key, the last registered provider gets precedence.
Knowing this, you can have a local file appSettings.json, which defines the database connection string:
{
"db-connection-string": "Data Source = DEVELOPMENT_PC;Initial Catalog=MyLocalDatabase;Integrated Security=True"
}
Then register this provider after you have registered the key vault provider.
.ConfigureAppConfiguration((ctx, builder) =>
{
var keyVaultEndpoint = GetKeyVaultEndpoint();
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback
)
);
builder.AddAzureKeyVault(
keyVaultEndpoint,
keyVaultClient,
new DefaultKeyVaultSecretManager()
);
}
var appSettingsFilePath = GetJsonAppSettingsFile(); // Hardcoded, or add some other configuratin logic
if (!string.IsNullOrEmpty(appSettingsFilePath))
{
builder.AddJsonFile(appSettingsFilePath);
}
}
Obviously, the production environment will have an empty or non-existent appSettings.json file, and all values will be derived from the key store. Or alternatively, you use the appSettings.json file in your production environment, but just for configuration values that do not contain secrets.

Can MSI work with EF CodeFirst?

I've gotten stuck on this for quite a while now, with no luck advancing it on my own.
I am trying to connect from an Azure App Service to a EF CodeFirst managed database, using an MSI token.
When I deployed the App Service using ARM I produced an Output that ensured that it created a Service Principal:
{
"principalId":"98f2c1f2-0a86-4ff1-92db-d43ec0edxxxx","
tenantId":"e6d2d4cc-b762-486e-8894-4f5f440dxxxx",
"type":"SystemAssigned"
}
In Kudu the environment variables show that it is being installed:
MSI_ENDPOINT = http://127.0.0.1:41239/MSI/token/
MSI_SECRET = 7C1B16Fxxxxxxxxxxxxx
I have provided in the Azure Portal a connection string as follows:
Data Source=nzmoebase0000bt.database.windows.net;Initial Catalog=nzmoebase0001bt;Connect Timeout=300;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=300;
I've added the principal to the database as an Owner.
Note: I cannot do the same for the master db.
The token is added to the DbContext as follows:
The token is being added using:
static async Task AttachAccessTokenToDbConnection(IDbConnection dbConnection)
{
SqlConnection sqlConnection = dbConnection as SqlConnection;
if (sqlConnection == null)
{
return;
}
string msiEndpoint = Environment.GetEnvironmentVariable("MSI_ENDPOINT");
if (string.IsNullOrEmpty(msiEndpoint))
{
return;
}
var msiSecret = Environment.GetEnvironmentVariable("MSI_SECRET");
if (string.IsNullOrEmpty(msiSecret))
{
return;
}
// To get around:
// "Cannot set the AccessToken property if 'UserID', 'UID', 'Password', or 'PWD' has been specified in connection string."
var terms = new[] {"UserID","Password","PWD=","UID=" };
string connectionString = dbConnection.ConnectionString;
foreach (var term in terms)
{
if (connectionString.Contains(term, StringComparison.InvariantCultureIgnoreCase))
{
return;
}
}
string accessToken = await AppCoreDbContextMSITokenFactory.GetAzureSqlResourceTokenAsync();
sqlConnection.AccessToken = accessToken;
}
With tracing on, the token is:
.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI....
Which decoded using jwt.io gave:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "FSimuFrFNoC0sJXGmv13nNZceDc",
"kid": "FSimuFrFNoC0sJXGmv13nNZceDc"
}.{
"aud": "https://database.windows.net/",
"iss": "https://sts.windows.net/e6d2d4cc-b762-486e-8894-4f5f440dxxxx/",
"iat": 1522783025,
"nbf": 1522783025,
"exp": 1522786925,
"aio": "Y2NgYPjNdyJd9zrzpLavJSEzNIuPAAA=",
"appid": "d1057cea-461b-4946-89a9-d76439c2xxxx",
"appidacr": "2",
"e_exp": 262800,
"idp": "https://sts.windows.net/e6d2d4cc-b762-486e-8894-4f5f440dxxxx/",
"oid": "98f2c1f2-0a86-4ff1-92db-d43ec0edxxxx",
"sub": "98f2c1f2-0a86-4ff1-92db-d43ec0edxxxx",
"tid": "e6d2d4cc-b762-486e-8894-4f5f440dxxxx",
"uti": "59bqKWiSL0Gf0bTCI0AAAA",
"ver": "1.0"
}.[Signature]
I Added Persist Security Info = True as per several recommendations on the net, but that did nothing detectable.
Data Source=nzmoebase0000bt.database.windows.net;Initial Catalog=nzmoebase0001bt;MultipleActiveResultSets=False;Persist Security Info = True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
The error I am getting is:
[InvalidOperationException: This operation requires a connection to the 'master' database. Unable to create a connection to the 'master' database because the original database connection has been opened and credentials have been removed from the connection string. Supply an unopened connection.]
Has anyone gotten a connection to a database, using CodeFirst, with Migrations, and MSI? At this point, after several weeks of being really stuck, I'm starting to wonder if it is possible.
Thanks for any help -- even if just proof that it can work, for starters.
Unfortunately, to my knowledge, no. A major stumbling block to a project that had to fall back to unsecure Username/password loaded connection strings.
You can set the access token on the sql connection like that:
Install the Microsoft.Azure.Services.AppAuthentication nuget package
set up your context class like that:
public class MyDatabaseContext : DbContext
{
public MyDatabaseContext(DbContextOptions<MyDatabaseContext> options)
: base(options)
{
// Get the db connection
var connection = (SqlConnection)Database.GetDbConnection();
// Add the access token
connection.AccessToken = new AzureServiceTokenProvider()
.GetAccessTokenAsync("https://database.windows.net/")
.ConfigureAwait(false).GetAwaiter().GetResult();
}
public DbSet<MyTable> MyTable { get; set; }
}

Could not create SSL/TLS secure channel when connecting through WSS, only on Azure

I am building a Web Api (using ASP.NET Web API), that connects via Secure WebSockets to an endpoint that our client exposed (wss://client-domain:4747/app/engineData). They gave me their certificates all in .pem format (root.pem and client.pem), and a private key (client_key.pem).
In order to get this done I did the following:
1) Converted client.pem and client_key.pem to a single .pfx file (used this here: Convert a CERT/PEM certificate to a PFX certificate)
2) I used the library System.Net.WebSockets, and wrote the following code:
private void InitWebSockesClient()
{
client = new ClientWebSocket();
client.Options.SetRequestHeader(HEADER_KEY, HEADER_VALUE); //Some headers I need
AddCertificatesSecurity();
}
private void AddCertificatesSecurity()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12;
// I KNOW THIS SHOULDNT BE USED ON PROD, had to use it to make it
// work locally.
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
X509Certificate2 x509 = new X509Certificate2();
// this is the pfx I converted from client.pem and client_key
byte[] rawData = ReadFile(certificatesPath + #"\cert.pfx");
x509.Import(rawData, "123456", X509KeyStorageFlags.UserKeySet);
X509Certificate2Collection certificateCollection = new X509Certificate2Collection(x509);
client.Options.ClientCertificates = certificateCollection;
}
And when I want to connect I call:
public async Task<bool> Connect()
{
Uri uriToConnect = new Uri(URL);
await client.ConnectAsync(uriToConnect, CancellationToken.None);
return client.State == WebSocketState.Open;
}
This works fine locally. But whenever I deploy my Web Api on Azure (App Service) and make an HTTP request to it, it throws:
System.Net.WebSockets.WebSocketException - Unable to connect to the remote server.
And the inner exception:
System.Net.WebException - The request was aborted: Could not create SSL/TLS secure channel.
I enabled WebSockets on the AppService instance.
If I delete the line that always return true for the certificate validation, it doesn't work even locally, and the message says something like:
The remote certificate is invalid according to the validation procedure.
So definitely I got something wrong with the certificates, those three .pem files are being used right now in a similar [![enter image description here][1]][1]app in a node.js and work fine, the WSS connection is established properly. I don't really know what usage give to each one, so I am kind of lost here.
These are the cipher suites of the domain I want to connect: https://i.stack.imgur.com/ZFbo3.png
Inspired by Tom's comment, I finally made it work by just adding the certificate to the Web App in Azure App Service, instead of trying to use it from the filesystem. First I uploaded the .pfx file in the SSL Certificates section in Azure. Then, in the App settings, I added a setting called WEBSITE_LOAD_CERTIFICATES, with the thumbprint of the certificate I wanted (the .pfx).
After that, I modified my code to do work like this:
private void InitWebSockesClient()
{
client = new ClientWebSocket();
client.Options.SetRequestHeader(HEADER_KEY, HEADER_VALUE); //Some headers I need
AddCertificateToWebSocketsClient();
}
private void AddCertificateToWebSocketsClient()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12;
// this should really validate the cert
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
// reading cert from store
X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection =
certStore.Certificates.Find(X509FindType.FindByThumbprint,
CERTIFICATES_THUMBPRINT,
false);
if (certCollection.Count > 0)
{
client.Options.ClientCertificates = certCollection;
}
else
{
// handle error
}
certStore.Close();
}
Where CERTIFICATES_THUMBPRINT is a string (thumbsprint of your certificate, the one you saw on Azure).
In case you want to make it work locally, you just need to install the certificate on your computer, as otherwise it won't obviously find it on the store.
Reference for all this in Azure docs: https://learn.microsoft.com/en-us/azure/app-service/app-service-web-ssl-cert-load.

ResetPassword Token How and where is it stored?

I've been trying to understand how the reset password & account confirmation works in ASP.NET Identity. I'd just like to know if the Tokens are being stored and if so, where?
The links I receive when I'm using the password reset feature look something like this
http://localhost:1470/Account/ResetPassword?userId=a8b1389c-df93-4dfc-b463-541507c1a4bc&code=yhUegXIM9SZBpPVbBtv22kg7NO7F96B8MJi9MryAadUY5XYjz8srVkS5UL8Lx%2BLPYTU6a6jhqOrzMUkkMyPbEHPY3Ul6%2B%2F0s0qQvtM%2FLLII3s29FgkcK0OnjX46Bmj9JlFCUx53rOH%2FXMacwnKDzoJ1rbrUyypZiJXloIE50Q6iPuMTUHbX9O%2B3JMZtCVXjhhsHLkTOn9IVoN6uVAOMWNQ%3D%3D
My guess is that the tokens are stored in the link itself since I cannot find any trace of it anywhere else. Maybe someone knows for sure?
As I mentioned in the comment
"Tokens are generated using the SecurityStamp and validating against the SecurityStamp and not storing anywhere in database or local file storage. If you update the SecurityStamp, then previous tokens are no longer valid."
#DSR is correct but I would like to add some information to this as well.
If you have set up a Web project with Individual User Accounts go to:
App_Start -> IdentityConfig.cs
There you will see code like this:
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
The description for DataProtectorTokenProvider<TUser, TKey> gives the information:
Represents a token provider that uses an IDataProtector to generate
encrypted tokens based off of the security stamp.
https://learn.microsoft.com/en-us/previous-versions/aspnet/dn613280(v%3dvs.108)
We can however try to dig a bit deeper how it really works. The token verification will fail if different Application Pool Identities are used for creating and validating a token on a single server. This points to that the actual protection mechanism would look something like this:
System.Security.Cryptography.ProtectedData.Protect(userData, entropy, DataProtectionScope.CurrentUser);
Given that it works if all sites use the same Application Pool Identity points to this as well. Could also be DataProtectionProvider with protectionDescriptor "LOCAL=user". It should have worked with different Application Pool Identities if LOCAL=machine was set.
new DataProtectionProvider("LOCAL=user")
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotector?view=netframework-4.7.2
https://learn.microsoft.com/en-us/uwp/api/windows.security.cryptography.dataprotection.dataprotectionprovider
dataProtectionProvider is of type IDataProtectionProvider.
It is injected in Startup.Auth.cs like this:
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
CreatePerOwinContext is located in the assembly Microsoft.AspNet.Identity.Owin -> AppBuilderExtensions.cs. Both ASP.NET Identity and ASP.NET Core Identity are open source and can be viewed at GitHub.
public static IAppBuilder CreatePerOwinContext<T>(this IAppBuilder app,
Func<IdentityFactoryOptions<T>, IOwinContext, T> createCallback,
Action<IdentityFactoryOptions<T>, T> disposeCallback) where T : class, IDisposable
{
if (app == null)
{
throw new ArgumentNullException("app");
}
if (createCallback == null)
{
throw new ArgumentNullException("createCallback");
}
if (disposeCallback == null)
{
throw new ArgumentNullException("disposeCallback");
}
app.Use(typeof (IdentityFactoryMiddleware<T, IdentityFactoryOptions<T>>),
new IdentityFactoryOptions<T>
{
DataProtectionProvider = app.GetDataProtectionProvider(),
Provider = new IdentityFactoryProvider<T>
{
OnCreate = createCallback,
OnDispose = disposeCallback
}
});
return app;
}
https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Owin/Extensions/AppBuilderExtensions.cs
https://archive.codeplex.com/?p=aspnetidentity#src/Microsoft.AspNet.Identity.Owin/Extensions/AppBuilderExtensions.cs
app.GetDataProtectionProvider() is in turn located in assembly Microsoft.Owin.Security that is also Open Source.
public static IDataProtectionProvider GetDataProtectionProvider(this IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException("app");
}
object value;
if (app.Properties.TryGetValue("security.DataProtectionProvider", out value))
{
var del = value as DataProtectionProviderDelegate;
if (del != null)
{
return new CallDataProtectionProvider(del);
}
}
return null;
}
https://github.com/aspnet/AspNetKatana/blob/release/src/Microsoft.Owin.Security/DataProtection/AppBuilderExtensions.cs
We can also see that CreateDataProtector has a fallback to the implementation DpapiDataProtectionProvider.
private static IDataProtectionProvider FallbackDataProtectionProvider(IAppBuilder app)
{
return new DpapiDataProtectionProvider(GetAppName(app));
}
When reading about DpapiDataProtectionProvider(DPAPI stands for Data Protection Application Programming Interface) the description says:
Used to provide the data protection services that are derived from the
Data Protection API. It is the best choice of data protection when you
application is not hosted by ASP.NET and all processes are running as
the same domain identity.
The Create method purposes are described as:
Additional entropy used to ensure protected data may only be
unprotected for the correct purposes.
The protector class itself then looks like this:
using System.Security.Cryptography;
namespace Microsoft.Owin.Security.DataProtection
{
internal class DpapiDataProtector : IDataProtector
{
private readonly System.Security.Cryptography.DpapiDataProtector _protector;
public DpapiDataProtector(string appName, string[] purposes)
{
_protector = new System.Security.Cryptography.DpapiDataProtector(appName, "Microsoft.Owin.Security.IDataProtector", purposes)
{
Scope = DataProtectionScope.CurrentUser
};
}
public byte[] Protect(byte[] userData)
{
return _protector.Protect(userData);
}
public byte[] Unprotect(byte[] protectedData)
{
return _protector.Unprotect(protectedData);
}
}
}
https://learn.microsoft.com/en-us/previous-versions/aspnet/dn253784(v%3dvs.113)

Resources