Azure web app with access to multiple key vaults - azure-keyvault

I'm wondering if an Azure web application can have access to multiple key vaults? This is because one of our web applications has multiple database connection strings and each of the databases has columns encrypted with always encrypted and the column encryption keys will be stored in their own key vaults per database. If this is possible, how does the web application know which key vault to access to get the column encryption key for the respective database it's connecting to?
Currently the web application has access to one key vault and all of the databases are encrypted with the same column encryption key which is in this key vault. But I want to split this out so each database has it's own key vault and respective column encryption key for better security. The web application obtains access to the key vault via Azure Active Directory with the client id and secret mechanism. I've played around with adding the Azure Active Directory application to the multiple key vaults using the key vault access policy but not sure if this is correct/will work.
The code I'm using to wire up the column encryption key with the web application (which appears to be key vault agnostic):
public static void InitializeAzureKeyVaultProvider()
{
var clientId = ConfigurationManager.AppSettings["AuthClientId"];
var clientSecret = ConfigurationManager.AppSettings["AuthClientSecret"];
_clientCredential = new ClientCredential(clientId, clientSecret);
SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider =
new SqlColumnEncryptionAzureKeyVaultProvider(GetToken);
Dictionary<string, SqlColumnEncryptionKeyStoreProvider> providers =
new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>
{
{SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider}
};
SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
}
private static async Task<string> GetToken(string authority, string resource, string scope)
{
var authContext = new AuthenticationContext(authority);
var result = await authContext.AcquireTokenAsync(resource, _clientCredential);
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the access token");
}
return result.AccessToken;
}
Thoughts?

Related

Initiate and store multiple OAuth2 external authentication challenges in a ASP.NET Core MVC application?

I can authenticate against two separate OAuth authentication schemes but it seems only one can be active at a time. I'd like to compare data from two separate SaaS applications and therefore I need two separate Bearer tokens. How can I initiate multiple OAuth challenges when the user loads the application and then store the Bearer Tokens for each? (e.g. in the Context.User cookie?)
My Startup.cs is as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddScheme1 (options =>
{
options.ClientId = Configuration["Scheme1:ClientId"];
options.ClientSecret = Configuration["Scheme1:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
})
.AddScheme2(options =>
{
options.ClientId = Configuration["Scheme2:ClientId"];
options.ClientSecret = Configuration["Scheme2:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
});...
}
The AuthenticationController calls the Challenge overloaded method from the Microsoft.AspNetCore.Mvc.Core assembly that takes a single provider/scheme (passing multiple schemes in the overloaded method seems to be ignored).
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
...
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
Presumably, you'd prompt the user to sign-into one external application, redirect back to the home page, and then prompt them to sign-into the second one, and then allow them to start using the application proper.
If this is possible - e.g. using a "multiple" Auth cookie - how then would I fetch the correct Bearer token and User values for the given scheme? Currently you just seem to fetch the token with a generic "access_token" name and unique user values:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string userID = User.FindFirstValue(ClaimTypes.NameIdentifier);
There does seem to be some information here regarding using a SignInManager but I'm unable to determine if this is applicable to this problem.
I would aim to start with a standard architecture where the user authenticates with the one and only app, and gets only one set of tokens, issued by your own Authorization Server.
SaaS DATA - OPTION 1
Does the user need to get involved in these connections or can you use a back end to back end flow here?
Your C# code could connect to the SaaS provider with the client credentials grant, using the client ID and secret that you reference above. Provider tokens would then be cached in memory, then used by the back end code to return provider data to the UI. This is a simple option to code.
SaaS DATA - OPTION 2
If the user needs to get involved, because the data is owned by them, you might offer UI options like this. After each click the user is redirected again, to get a token for that provider.
View provider 1 data
View provider 2 data
Aim to emulate the embedded token pattern, where the provider tokens are available as a secondary credential. How you represent this could vary, eg you might prefer to store provider tokens in an encrypted cookie.
CODING AND SIMPLICITY
I would not mix up provider tokens with the primary OAuth mechanism of signing into the app and getting tokens via the .NET security framework, which typically implements OpenID Connect. Instead I would aim to code the SaaS connections on demand.
I think you will find it easier to code the SaaS connections with a library approach, such as Identity Model. This will also help you to deal with SaaS provider differences more easily.
I assume you use OIDC schemes.
First, you need to add two cookie schemes, one for each OIDC authentication scheme as their sign in scheme and set their callback path to different values to stop them competing:
services.AddAuthentication()
.AddCookie("Cookie1")
.AddCookie("Cookie2")
.AddOpenIdConnect("OidcScheme1", opt =>
{
opt.SignInScheme = "Cookie1";
opt.CallbackPath = "/signin-oidc-scheme1";
opt.SaveTokens = true;
})
.AddOpenIdConnect("OidcScheme2", opt =>
{
opt.SignInScheme = "Cookie2";
opt.CallbackPath = "/signin-oidc-scheme2";
opt.SaveTokens = true;
});
This will instruct the OIDC handler to authenticate the user from corresponding cookie.
Second, you need a controller action to challenge the user against each OIDC scheme:
[HttpGet]
[Route("login")]
[AllowAnonymous]
public IActionResult Login([FromQuery]string scheme,
[FromQuery]string? returnUrl)
{
return Challenge(new AuthenticationProperties
{
RedirectUri = returnUrl ?? "/"
}, scheme);
}
From your web app, you need to send the user to the Login endpoint twice with different scheme values:
GET /login?scheme=OidcScheme1
GET /login?scheme=OidcScheme2
Or chain them together using the returnUrl:
GET /login?scheme=OidcScheme1&returnUrl=%2Flogin%3Fscheme%3DOidcScheme2
Once signed in, there should be two cookies in the browser window, for example:
To authenticate the user and restore both identities from two cookies, you can use authorization policy:
[HttpGet]
[Authorize(AuthenticationSchemes = "OidcScheme1,OidcScheme2")]
public async Task<IActionResult> SomeOperation()
{
// Two identities, one from each cookie
var userIdentities = User.Identities;
...
}
To get access token from each authentication scheme, use the method you discovered (GetTokenAsync) and specify authentication scheme:
var token1 = await HttpContext.GetTokenAsync("OidcScheme1", "access_token");
var token2 = await HttpContext.GetTokenAsync("OidcScheme2", "access_token");
It is possible that the access token is not returned from the token endpoint depends on the response_type you used. If this is the case, try set the OpenIdConnectionOptions.ResponseType to OpenIdConnectResponseType.Code and make sure the scope is correct.
I encountered a similar problem where we had microservices that are/were shared across multiple products with each product having a separate IDP tenant (essentially a different token issuer). Perhaps a similar approach might work for your scenario...
The following link helped me with a solution - see here.
Basically I defined a smart authentication scheme
var builder = services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = "smart";
//...
});
Then in the smart policy scheme definition, I decode the JWT coming in to work out the issuer from the iss claim in the JWT, so that I can forward to the correct location for JWT bearer authentication.
builder.AddPolicyScheme("smart", "smart", options =>
{
options.ForwardDefaultSelector = context =>
{
var jwtEncodedString = context.Request.Headers["Authorization"].FirstOrDefault()?.Substring(7);
if (string.IsNullOrEmpty(jwtEncodedString))
return settings.Tenants.First().Key; // There's no authorization header, so just return any.
var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
var issuer = token.Claims.First(c => c.Type == "iss").Value?.TrimEnd('/');
var tenant = settings.Tenants
.Where(pair => pair.Value.Issuer.TrimEnd('/') == issuer)
.Select(pair => pair.Key).FirstOrDefault();
if (tenant == null)
throw new AuthorizationException($"Failed to locate authorization tenant with issuer '{issuer}'.");
return tenant;
};
});
Note: settings.Tenants is just an array of whitelisted tenants (from appsettings) that I configure as follows:
foreach (var tenant in settings.Tenants)
builder.AddJwtBearer(tenant.Key, options => Configure(options, tenant.Value, defaultJwtBearerEvents));

Azure Key Vault Quickstart fails to provide key vault client

I am learning the Azure Key Vault. I am trying to follow https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-net. I have created a vault in the portal, assigned an access policy for a user account. I have seen the vault in the protal, and I have seen that the user has I have set the environment variable value. I use an 'az login' command at a prompt to use that account. From the prompt I run the code in the quickstart. It prompts for a secret. I enter 'bob.' It throws an exception.
"Creating a secret in [...]-key-vault called 'mySecret' with the value 'bob' ...Unhandled exception. Azure.Identity.AuthenticationFailedException: SharedTokenCacheCredential authentication failed: A configuration issue is preventing authentication - check the error message from the server for details.You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS70002: The client does not exist or is not enabled for consumers. If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908."
The exception is at
await client.SetSecretAsync(secretName, secretValue);
I think the problem is coming from
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
The client is not able to send tokens that the vault accepts. I am at a loss. I have had a couple of people with some expertise in using the Vault review this code and they haven't been able to provide insight. Any help?
Here is the code, from the example:
using System;
using System.Threading.Tasks;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
namespace key_vault_console_app
{
class Program
{
static async Task Main(string[] args)
{
const string secretName = "mySecret";
var keyVaultName = Environment.GetEnvironmentVariable("KEY_VAULT_NAME");
var kvUri = $"https://{keyVaultName}.vault.azure.net";
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
Console.Write("Input the value of your secret > ");
var secretValue = Console.ReadLine();
Console.Write($"Creating a secret in {keyVaultName} called '{secretName}' with the value '{secretValue}' ...");
await client.SetSecretAsync(secretName, secretValue);
Console.WriteLine(" done.");
Console.WriteLine("Forgetting your secret.");
secretValue = string.Empty;
Console.WriteLine($"Your secret is '{secretValue}'.");
Console.WriteLine($"Retrieving your secret from {keyVaultName}.");
var secret = await client.GetSecretAsync(secretName);
Console.WriteLine($"Your secret is '{secret.Value}'.");
Console.Write($"Deleting your secret from {keyVaultName} ...");
DeleteSecretOperation operation = await client.StartDeleteSecretAsync(secretName);
// You only need to wait for completion if you want to purge or recover the secret.
await operation.WaitForCompletionAsync();
Console.WriteLine(" done.");
Console.Write($"Purging your secret from {keyVaultName} ...");
await client.PurgeDeletedSecretAsync(secretName);
Console.WriteLine(" done.");
}
}
}
Not Sure the root reason for it. But If you want to use a user account to login to Azure and access your key vault, using UsernamePasswordCredential() could be a workaround here.
To use UsernamePasswordCredential(), you should register a client application in Azure AD: Go to Azure portal =>Azure Active Directory => New registration
Note its application ID:
Go to API permissioms, and grant key vault user_impersonation permission so that users could access key vault via this app.
Click "Grant admin consent for.." to finish the permission grant process.
Go to the "Authentication" blade, turn on "Allow public client flows" so that Azure will consider this app as a public client:
Try the code below to create a secret:
using System;
using System.Threading.Tasks;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
namespace key_vault_console_app
{
class Program
{
static async Task Main(string[] args)
{
const string secretName = "mySecret2";
var keyVaultName = "<your kv name>";
var kvUri = $"https://{keyVaultName}.vault.azure.net";
var userCred = new UsernamePasswordCredential("<user account name>", "<user password>", "<your tenant name/id>", "<client application ID WHCIH we created above>");
var client = new SecretClient(new Uri(kvUri), userCred);
Console.Write("Input the value of your secret > ");
var secretValue = Console.ReadLine();
Console.Write($"Creating a secret in {keyVaultName} called '{secretName}' with the value '{secretValue}' ...");
await client.SetSecretAsync(secretName, secretValue);
Console.WriteLine(" done.");
}
}
}
Result:

Can we use same auth cookie (generated in dot net core api) in a different api applications but in the same domain

I have an application(core 2.2.0) which uses a cookie for authentication, below is the code from the Startup.cs file
services.AddAuthentication(cookieConfig.AuthScheme)
.AddCookie(cookieConfig.AuthScheme, options => {
options.LoginPath = new PathString(cookieConfig.LoginPath);
options.AccessDeniedPath = new PathString(cookieConfig.AccessDeniedPath);
options.Cookie = cookie;
options.Events = cookieEvents;
});
And below code is from the Sign-in API
await HttpContext.SignInAsync(_cookieConfig.AuthScheme, userPrincipal, authProps);
Suppose this application generates a cookie 'ABC', and it has a URL1 - https://somedomain.com/api
and I have another API hosted at URL2 - https://somedomain.com/another_api/whatever
But remember both APIs are a different project.
When I pass this cookie to URL1, in the OnAuthorization() I can see the Identities and the Claims properties with correct values of that user and it authorizes the user perfectly. But when I pass the same cookie to URL2 all claims and identity properties show null and it does not authorize.
My question: Is it possible to share authentication between different domains? If yes, then how? If not possible then please suggest an alternative approach.
Additional Details -
We have the above code which generates (at App1) a cookie ABC=some_encrpyted_value.
And in App2 I am trying to unprotect that cookie in below manner
string cookieValue = context.HttpContext.Request.Cookies["ABC"];
var provider = DataProtectionProvider.Create(new DirectoryInfo(#"C:\temp-keys\"));
var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, "ABC", "v2");
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);
TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookieValue);
I have placed the data protection key from App1 in the folder 'C:\temp-keys'.
I found this code somewhere in StackOverflow itself and it throws 'CryptographicException: The payload was invalid.' exception at line
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
My understanding of the App1 code is that the Identity and Claim values (with some other values) are encrypted and cookie ABC is generated, and when we send a request this cookie is decrypted and the Identity and Claims are get populated in the context.HttpContext.User
I actually wanted it to work the same way in the App2, I want to pass the cookie and the identity values should be populated in the context.
It is possible. To deal with this type of scenarios we have been using ASP.NET machineKey. That is does is it encrypts/descriptor the auth data using the same key so that different sites can share the same authenticated sessions and data.
In Dotnet Core this machineKey concept has evolved and now called - new data protection system. Microsoft.AspNetCore.DataProtection.SystemWeb package is used to implement the data protection system. To read more on this.
There are various ways how you can store and use the key:
ProtectKeysWithAzureKeyVault (if you are using Azure)
PersistKeysToFileSystem (this is the easier one)
ProtectKeysWith*
UnprotectKeysWithAnyCertificate
Details of the individual scenario is described here.

OAuth2 - How to add a list of clientId/ client secret as a JSON file instead of a single client id / client secret?

In order to secure the communications between my services, I created an Authorization server that generates an access token. my application is storing the Client ID and Client Secret and pass those to the Authorization server in exchange for an access token.
The client ID & my client secret are stored in my configure() method:
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws
Exception {
clients.inMemory().withClient("xxx").secret("xxx")
.authorizedGrantTypes("client_credentials").scopes("resource-
server-read", "resource-server-write");
}
My question is how can I created multiple client ID & client secret in a JSON file instead of a single value?

How to store Public Certiticate (.cer file) in Azure Key Vault

How I can upload or store public key (.cer) file in azure keyvault. From the keyvault panel it gives error when I tried to to upload any .cer file where It works for .pfx file.
Loading Public Key Certificates
Azure Key Vault Explorer allows you to load public key certificates (.cer files).
Certificates are stored as keys in the Key Vault using a "standard" format used by that application (since .cer files aren't natively supported by Azure Key Vault).
Accessing Public Key Certificates
Once you have loaded public keys into the Azure Key Vault, they can then be accessed programatically as follows:
// load certificate based on format used by `Azure Key Vault Explorer`
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
var certBundle = await kv.GetSecretAsync(secretIdentifier).ConfigureAwait(false);
byte[] certBytes = null;
if (certBundle.ContentType == "application/x-pkcs12")
{
certBytes = Convert.FromBase64String(certBundle.Value);
}
else if (certBundle.ContentType == "application/pkix-cert")
{
certBytes = certBundle?.Value.FromJson<PublicKeyCertificate>()?.Data;
}
if (certBytes != null && certBytes.Length > 0)
{
return new X509Certificate2(certBytes,
"",
X509KeyStorageFlags.Exportable |
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet);
}
return null;
...
// class used to access public key certificate stored in Key Vault
public class PublicKeyCertificate
{
public byte[] Data;
}
You should consider if Key Vault is the appropriate solution for your scenario. The public key (by nature) is not confidential data, you don't need a secure place to store it. You can use a general purpose storage service for it.
If you still need to use Key Vault, you can store it as a secret. Key Vault secrets are octet sequences with a maximum size of 25k bytes each.

Resources